AWS Lambda
How AWS Lambda works in practice: the execution model, memory and CPU, cold starts, concurrency limits, timeouts, retries and the costs that surprise teams.
Lambda runs your code in response to events without you managing servers. You supply a function, AWS supplies the compute, and you pay for the time it runs rather than for an instance sitting idle.
The model is genuinely different from a server, and most Lambda problems come from carrying server assumptions into it.
Lambda: Event-Driven Execution
Lambda scales to zero. You pay only for the milliseconds your code actually runs
The execution model
An execution environment is a small, isolated sandbox that runs one invocation at a time. Ten simultaneous requests mean ten environments. There is no in-process concurrency to share.
What matters operationally is that AWS reuses environments. After an invocation finishes, the environment is kept warm for a while and reused for the next request. That has two consequences:
Code outside the handler runs once per environment, not once per invocation. This is the single most useful optimisation available:
import boto3
# Runs once per environment. Reused across invocations
s3 = boto3.client("s3")
def handler(event, context):
# Runs every invocation
return s3.get_object(Bucket="acme", Key=event["key"])
Creating the client inside the handler adds its initialisation cost to every request.
State persists between invocations in the same environment. Warm environments retain globals and /tmp contents. That is excellent for connection reuse and caching, and a real source of bugs when a function accidentally carries state, a mutated global, a leftover file, into an unrelated request.
Memory is how you buy CPU
Lambda has one performance dial, from 128 MB to 10,240 MB, and CPU scales proportionally with it. Around 1,769 MB you get one full vCPU; beyond that, more.
This means the intuitive cost trade-off is often backwards. Doubling memory doubles the per-millisecond price but can more than halve the duration, so the invocation ends up cheaper as well as faster:
128 MB × 8,000 ms = 1,024 MB-s
512 MB × 1,600 ms = 819 MB-s ← faster AND cheaper
1024 MB × 900 ms = 922 MB-s
Do not guess at this. AWS Lambda Power Tuning runs your function across memory settings and produces the cost and duration curve, and it routinely finds that the lowest memory setting was the most expensive choice.
Cold starts
A cold start happens when no warm environment is available: AWS provisions a sandbox, downloads your code, starts the runtime, and runs your initialisation. Typically tens to a few hundred milliseconds; considerably more for heavy runtimes and large dependency trees.
What genuinely helps:
- Smaller deployment packages. Bundle only what you use.
- Lazy-load anything not needed on every path.
- Increase memory: initialisation gets more CPU too.
- Choose a lighter runtime where you have the choice. Python, Node and Go start much faster than a JVM cold.
- SnapStart for Java, which snapshots an initialised environment and restores it, removing most of the JVM penalty.
- Provisioned concurrency keeps a set number of environments permanently warm. It works, and it is billed hourly whether used or not, so it suits a predictable latency-sensitive endpoint, not a general remedy.
What usually does not help: a scheduled "warmer" invocation. It keeps one environment alive while a traffic spike needs fifty.
For most asynchronous and event-driven work, cold starts do not matter. They matter for user-facing synchronous requests, and that is where to spend effort.
Concurrency, and the limit that causes incidents
The account has a default concurrent execution limit of 1,000 across all functions in a region. Exceeding it throttles invocations.
Two controls:
Reserved concurrency caps a function and guarantees it that capacity. Setting it protects the rest of the account from one runaway function. The classic incident being a function triggered by S3 events that consumes the whole account limit and takes down unrelated APIs.
aws lambda put-function-concurrency \
--function-name image-processor \
--reserved-concurrent-executions 100
Setting it to 0 disables a function entirely, which is the fastest way to stop a misbehaving one without deleting it.
Provisioned concurrency pre-warms environments, at an hourly cost.
aws lambda get-account-settings --query 'AccountLimit.ConcurrentExecutions'
Watch the Throttles metric. Throttling looks like intermittent failures rather than a clear error, so it is easy to misdiagnose as an application bug.
Timeouts and retries
Maximum timeout is 15 minutes, default 3 seconds. Set it deliberately. Too high and a hung function burns money, too low and legitimate work is cut off mid-way.
Retry behaviour depends on how the function was invoked, and this catches people:
| Invocation | On failure |
|---|---|
| Synchronous (API Gateway, direct) | No retry (the caller decides) |
| Asynchronous (S3, SNS, EventBridge) | Retried twice automatically, then sent to a dead-letter queue or destination if configured |
| Stream (Kinesis, DynamoDB Streams) | Retried until success or expiry (blocking the shard) |
| SQS | Returned to the queue until maxReceiveCount, then the DLQ |
That third row is a genuine outage generator: one poison-pill record in a Kinesis shard is retried indefinitely and nothing behind it processes. Set MaximumRetryAttempts and an OnFailure destination on the event source mapping.
Because retries exist, functions must be idempotent. The same event will be delivered more than once, and a handler that charges a card or sends an email needs a deduplication key.
Lambda in a VPC
A function only needs VPC configuration to reach private resources. An RDS instance, an internal service. If it only calls public AWS APIs, leave it out; a VPC-attached function has no internet access by default.
That last point surprises people regularly: attaching a Lambda to a VPC removes its default internet access. Reaching a public endpoint then requires a NAT gateway, or a VPC endpoint for the AWS service in question.
The old penalty of many seconds of cold-start ENI setup is gone. VPC networking now uses shared network interfaces and adds very little. The connectivity trade-off remains.
Cost
Billing is requests plus GB-seconds of duration, with a perpetual free tier that covers a great deal of light usage.
Where costs actually go wrong:
- A function waiting on something. You pay for wall-clock duration, so a Lambda polling or sleeping is paying to wait. Use Step Functions for orchestration and waiting.
- Over-provisioned memory on a short function.
- Under-provisioned memory making everything slow, often more expensive, as above.
- Provisioned concurrency left on for a function that no longer needs it.
- Recursive invocation. A function writing to the S3 bucket that triggers it creates an infinite loop. AWS now detects and stops some recursive patterns, but not all. This has produced genuinely large bills.
When not to use Lambda
Worth stating plainly. Lambda is a poor fit for long-running processes beyond 15 minutes, sustained high-throughput workloads where an always-on container is cheaper, applications needing large local state or persistent connections, and anything with hard sub-10ms latency requirements. ECS Fargate or EKS is often the better answer.
Go deeper in this section
| Sub-topic | What you will learn |
|---|---|
| Lambda Functions | Writing, packaging and deploying functions |
Quick reference
| Task | Command |
|---|---|
| Invoke | aws lambda invoke --function-name <n> --payload '{}' out.json |
| Tail logs | aws logs tail /aws/lambda/<n> --follow |
| Update code | aws lambda update-function-code --function-name <n> --zip-file fileb://fn.zip |
| Change memory/timeout | aws lambda update-function-configuration --function-name <n> --memory-size 512 --timeout 30 |
| Cap concurrency | aws lambda put-function-concurrency --function-name <n> --reserved-concurrent-executions 100 |
| Account limit | aws lambda get-account-settings |
| List versions | aws lambda list-versions-by-function --function-name <n> |
Key takeaways
- Code outside the handler runs once per environment. Initialise clients there, not per invocation.
- Memory buys CPU. More memory is frequently faster and cheaper. Measure with Power Tuning.
- The account concurrency limit is shared, so set reserved concurrency to stop one function starving the rest.
- Retry behaviour depends on invocation type, and a poison record can block a Kinesis shard indefinitely.
- Functions must be idempotent. Duplicate delivery is normal, not exceptional.
- A VPC-attached Lambda loses default internet access: it needs NAT or a VPC endpoint.
- You pay for waiting. Orchestrate with Step Functions rather than sleeping in a function.
Check your understanding
Cheaper at higher memory
A function at 128 MB runs for 8 seconds. At 512 MB it runs for 1.6 seconds. Which costs less per invocation?
A stream that stopped processing
A Lambda consuming a Kinesis stream stops making progress. One record consistently causes an unhandled error. What is happening?
Frequently Asked Questions
What causes Lambda cold starts and how do I reduce them?
A cold start occurs when no warm execution environment is available, so AWS must create a sandbox, load your code and run initialisation. Reduce them by shrinking the deployment package, lazy-loading dependencies that are not needed on every code path, increasing memory, which also increases the CPU available during initialisation, and choosing a lighter runtime where possible. Java benefits enormously from SnapStart. Provisioned concurrency keeps environments permanently warm and eliminates cold starts for that capacity, at an hourly charge regardless of traffic.
How much memory should I give my Lambda function?
More than feels intuitive. CPU is allocated in proportion to memory, so a CPU-bound function at 128 MB may run eight times longer than at 512 MB, and since you are billed for memory multiplied by duration, the higher setting can be both faster and cheaper. Use AWS Lambda Power Tuning, which runs your function across a range of settings and plots cost against duration. Guessing low to save money is the most common and most expensive Lambda mistake.
Why is my Lambda function being throttled?
You have hit a concurrency limit. The account default is 1,000 concurrent executions across all functions in a region, shared between them, so one busy function can throttle others. If the function has reserved concurrency, it is capped at that number regardless of account headroom. Check the Throttles CloudWatch metric and aws lambda get-account-settings for the account limit. Setting reserved concurrency on high-volume functions protects everything else from being starved.
Does Lambda retry failed invocations?
It depends entirely on how it was invoked. Synchronous invocations, API Gateway, or a direct call, are not retried; the caller decides. Asynchronous invocations from S3, SNS or EventBridge are retried twice automatically before going to a dead-letter queue or failure destination. Stream sources such as Kinesis and DynamoDB Streams retry the batch until it succeeds or the data expires, blocking that shard meanwhile. SQS returns messages to the queue until maxReceiveCount is reached. Because duplicates are normal in all of these, functions must be idempotent.
Should I put my Lambda function in a VPC?
Only if it needs to reach private resources such as an RDS instance or an internal service. Attaching a function to a VPC removes its default internet access, so calls to public endpoints then require a NAT gateway or a VPC endpoint for the relevant AWS service. A frequent surprise. The cold-start penalty that used to make VPC Lambda painful is essentially gone thanks to shared network interfaces, so connectivity, not performance, is the deciding factor now.
When should I use ECS or EKS instead of Lambda?
When the workload runs longer than the 15-minute maximum, when throughput is high and sustained enough that always-on compute is cheaper than per-invocation billing, when the application needs persistent connections or significant local state, or when you need latency guarantees that cold starts cannot meet. Lambda is strongest for event-driven, spiky and intermittent work where paying nothing while idle is the dominant advantage. A steady workload running continuously is usually cheaper on Fargate.