Lambda enforced the timeout you configured. The function was still running and was killed. The question is where the time went.
Read the REPORT line
Every invocation logs one:
REPORT RequestId: abc-123 Duration: 3010.00 ms Billed Duration: 3010 ms
Memory Size: 128 MB Max Memory Used: 127 MB Init Duration: 890.12 ms
Four things to read:
| Field | What it tells you |
|---|---|
Duration | Handler execution, excluding init |
Init Duration | Cold start. Only present on a cold invocation |
Max Memory Used | Near Memory Size means memory pressure and CPU starvation |
Billed Duration | What you pay for |
aws logs filter-log-events --log-group-name /aws/lambda/api \
--filter-pattern "REPORT" --max-items 20 --query 'events[].message' --output text
Memory is CPU
This is the single most misunderstood thing about Lambda. CPU is allocated in proportion to memory. At 128 MB you get a small fraction of a vCPU; you reach roughly one full vCPU at 1,769 MB.
So a CPU-bound function at 128 MB is slow because it is CPU-starved, not because of anything in the code:
aws lambda update-function-configuration --function-name api --memory-size 1024
Counter-intuitively this often reduces cost. A function that takes 6,000 ms at 128 MB may take 700 ms at 1,024 MB. Billing is memory multiplied by duration, so 8x the memory for less than 1/8th the time is cheaper as well as faster.
AWS Lambda Power Tuning, a published state machine, measures this for your function across memory settings and shows the cost and duration curve. It is worth running once for anything invoked frequently.
The VPC trap
aws lambda get-function-configuration --function-name api --query 'VpcConfig'
{ "SubnetIds": ["subnet-0private1"], "SecurityGroupIds": ["sg-0abc"] }
A Lambda in a VPC has no internet access. Attaching it to a VPC replaces the default networking, and a private subnet with no NAT gateway means:
- Calls to any public API hang until the timeout
- Calls to AWS services without a VPC endpoint hang too, including S3, DynamoDB, Secrets Manager and STS
This is the most common cause of a function that worked in development and times out after being moved into a VPC. The symptom is a hang rather than a connection error, because the packets go nowhere and nothing answers.
The fix is a NAT gateway for internet access, or VPC endpoints for AWS services. S3 and DynamoDB have free gateway endpoints; the rest are interface endpoints with an hourly cost.
Also ask whether the function needs the VPC at all. It is only required to reach private resources such as an RDS instance. A function that only calls public AWS APIs is better off outside a VPC entirely.
Cold starts
A large Init Duration means initialisation, not your handler:
Init Duration: 3890.12 ms
Things that inflate it:
- A large deployment package
- Heavy imports at module scope, especially the full AWS SDK v2 or a large Java or .NET framework
- Establishing a database connection during init
- VPC attachment, though Hyperplane ENIs reduced this from seconds to tens of milliseconds
Move work that is not needed for every invocation out of module scope, and import only what you use:
# slow
import boto3
s3 = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")
sns = boto3.client("sns")
# faster: create clients lazily
import boto3
_s3 = None
def s3():
global _s3
if _s3 is None:
_s3 = boto3.client("s3")
return _s3
Provisioned concurrency removes cold starts for a fixed cost. SnapStart does the same for Java at no extra charge.
Set the timeout deliberately
aws lambda update-function-configuration --function-name api --timeout 30
The default is 3 seconds and the maximum is 15 minutes. Set it a little above the observed p99, not at the maximum: a generous timeout turns a hung dependency into a function that burns fifteen minutes of billed time per invocation and keeps concurrency occupied.
Behind API Gateway the effective ceiling is API Gateway's own 29 second integration timeout, so a Lambda timeout above that cannot help.
Client timeouts inside the function
Set your SDK and HTTP client timeouts below the Lambda timeout, so a slow dependency produces a handled error with a useful log line rather than an abrupt kill:
from botocore.config import Config
cfg = Config(connect_timeout=2, read_timeout=5, retries={"max_attempts": 2})
A Lambda killed at its timeout gets no chance to log anything, which is why these failures are often invisible.
Runtime.ExitError
Runtime exited with error: signal: killed
Different failure: the runtime process was killed, usually out of memory rather than time. Check Max Memory Used against Memory Size in the REPORT line and raise memory.
A checklist
- Read the
REPORTline. Duration, Init Duration, Max Memory Used. - Large
Init Duration→ cold start. Trim imports and module-scope work. - CPU-bound → raise memory, which raises CPU and often lowers cost.
Max Memory Usednear the limit → raise memory regardless.- In a VPC → confirm a NAT gateway or the right VPC endpoints exist.
- Ask whether the function needs a VPC at all.
- Set SDK client timeouts below the Lambda timeout so failures are logged.
Runtime.ExitError: signal: killedis memory, not time.
Frequently Asked Questions
Why does increasing Lambda memory make my function faster?
Because Lambda allocates CPU in proportion to memory. At 128 MB you get a small fraction of a vCPU, and you reach roughly one full vCPU at 1,769 MB. A CPU-bound function at low memory is therefore throttled on processor time rather than limited by anything in your code. Raising memory frequently reduces cost as well as duration, because billing multiplies memory by time: eight times the memory for a tenth of the duration is cheaper overall.
Why does my Lambda time out after I attached it to a VPC?
Attaching a Lambda to a VPC removes its default internet access, and a private subnet with no NAT gateway has no route out. Calls to public APIs, and to AWS services that have no VPC endpoint in that subnet, simply hang until the timeout rather than failing fast. Add a NAT gateway, or create VPC endpoints for the services you use. It is also worth checking whether the VPC is needed at all: it is only required to reach private resources such as an RDS instance.
What is Init Duration and how do I reduce it?
Init Duration is the cold start: the time spent loading your code and running everything at module scope before the handler is called. Reduce it by shrinking the deployment package, importing only the specific SDK clients you need rather than whole libraries, and deferring expensive work such as database connections until first use. Provisioned concurrency eliminates cold starts for a fixed cost, and SnapStart does the same for Java runtimes without one.
What timeout value should I use?
Slightly above the observed p99 duration, not the maximum. A 15 minute timeout on a function that normally takes 200 milliseconds means a hung dependency burns fifteen minutes of billed time and holds a concurrency slot for the duration, which can starve the rest of your traffic. Behind API Gateway there is a hard ceiling anyway, since API Gateway's integration timeout is 29 seconds, so a longer Lambda timeout cannot help those invocations.
What is the difference between a timeout and Runtime.ExitError?
A timeout means the invocation was still running when the configured limit was reached, so the problem is duration. Runtime exited with error: signal: killed means the runtime process itself was terminated, which is usually the out-of-memory killer rather than the clock. Compare Max Memory Used against Memory Size in the REPORT line: if they are close, raise the memory allocation. The two are easy to confuse because both end the invocation abruptly with little in the logs.