I deployed a CloudFormation stack with a custom resource backed by a Lambda function. The stack creation started normally but then hung for exactly one hour before rolling back. The Lambda function was running, but CloudFormation never received a success or failure response. I realized the custom resource handler wasn’t sending the required callback response to CloudFormation. In this post, I’ll walk through exactly what causes this and how to fix it.
The Problem
CloudFormation custom resources require Lambda to send an HTTPS response back to a pre-signed S3 URL. If Lambda times out, throws an unhandled exception, or simply never sends the response, CloudFormation waits until its own timeout (1 hour by default) before rolling back the entire stack.
The stack shows:
Stack CREATE_FAILED or UPDATE_ROLLBACK_FAILED
Custom resource [LogicalId] failed to receive a response
CloudFormation logs show timeout after exactly 3600 seconds (1 hour). In the Lambda logs, you might see:
Task timed out after 300 seconds
or
Unhandled exception in Lambda handler
or
No response sent to CloudFormation
Why Does This Happen?
CloudFormation custom resources work by invoking a Lambda function and expecting it to send a response to a pre-signed S3 URL within the Lambda timeout. If that response never arrives, CloudFormation doesn’t know if the operation succeeded or failed.
- Lambda times out — The Lambda timeout (default 3 seconds, max 15 minutes) is shorter than the actual operation time. Lambda terminates without sending a response.
- Unhandled exception — The Lambda handler throws an exception that isn’t caught. The function exits without calling the response callback.
- Missing cfnresponse.send() — The code never invokes the
cfnresponsemodule to send SUCCESS or FAILED status. - Invalid ResponseURL — The event’s
ResponseURLis malformed or the pre-signed URL has expired. - Network issue — Lambda can’t reach the S3 endpoint to send the response (VPC misconfiguration, no internet access).
The Fix
Always wrap the custom resource handler in a try/except block and send a response—success or failure—before exiting.
Option 1: Use cfnresponse Module (Python)
import cfnresponse
import json
def lambda_handler(event, context):
try:
request_type = event['RequestType']
physical_resource_id = event.get('PhysicalResourceId', event['LogicalResourceId'])
if request_type == 'Delete':
# Handle deletion
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_resource_id)
return
# Your custom logic here
custom_output = {
'Message': 'Resource created successfully',
'ResourceId': 'my-resource-123'
}
cfnresponse.send(event, context, cfnresponse.SUCCESS, custom_output, physical_resource_id)
except Exception as e:
print(f"Error: {str(e)}")
cfnresponse.send(event, context, cfnresponse.FAILED, {}, event.get('PhysicalResourceId', 'FAILED'))
Option 2: Manual Response with boto3 (for all languages)
import json
import urllib3
import os
http = urllib3.PoolManager()
def send_response(event, status, data):
response_url = event['ResponseURL']
response_body = {
'Status': status,
'PhysicalResourceId': event.get('PhysicalResourceId', event['LogicalResourceId']),
'StackId': event['StackId'],
'RequestId': event['RequestId'],
'LogicalResourceId': event['LogicalResourceId'],
'Data': data
}
http.request(
'PUT',
response_url,
body=json.dumps(response_body),
headers={'Content-Type': ''}
)
def lambda_handler(event, context):
try:
# Your custom logic here
result = do_something()
send_response(event, 'SUCCESS', {'Result': result})
except Exception as e:
send_response(event, 'FAILED', {'Error': str(e)})
Option 3: Set Appropriate Lambda Timeout
When creating the Lambda function, set a reasonable timeout (max 5 minutes for custom resources):
# Create or update Lambda with 5-minute timeout
aws lambda create-function \
--function-name my-custom-resource \
--runtime python3.11 \
--role arn:aws:iam::123456789012:role/lambda-role \
--handler index.lambda_handler \
--timeout 300 \
--zip-file fileb://function.zip
Option 4: Test Custom Resource Locally
Before deploying, test the custom resource with a simulated CloudFormation event:
# test_custom_resource.py
import json
from index import lambda_handler
mock_event = {
'RequestType': 'Create',
'ResponseURL': 'https://s3.amazonaws.com/mock-bucket/path',
'StackId': 'arn:aws:cloudformation:us-east-1:123456789012:stack/test/guid',
'RequestId': '12345',
'LogicalResourceId': 'MyResource',
'ResourceType': 'Custom::MyResource',
'ResourceProperties': {
'Key': 'value'
}
}
class MockContext:
def __init__(self):
self.function_name = 'test'
lambda_handler(mock_event, MockContext())
How to Run This
- Wrap your Lambda handler in a try/except block that catches all exceptions
- Use the
cfnresponsemodule (Python) or manually PUT to theResponseURL - Always send a response in both the success and failure paths
- Test the custom resource locally before deploying
- Set Lambda timeout to 5 minutes:
--timeout 300 - Deploy and monitor:
aws cloudformation describe-stack-events --stack-name my-stack --query "StackEvents[?ResourceType=='AWS::CloudFormation::CustomResource']"
Is This Safe?
Yes. Proper response handling is essential for custom resources. Always sending a response—even on failure—prevents CloudFormation from hanging and waiting for the timeout.
Key Takeaway
CloudFormation custom resources hang indefinitely if Lambda doesn’t send a response callback. Wrap your handler in try/except, use cfnresponse.send(), set appropriate timeouts, and always respond with either SUCCESS or FAILED status.
Have questions or ran into a different CloudFormation issue? Connect with me on LinkedIn or X.