Topic 5.3
Lambda: The Execution Model
In one line
Lambda runs your function in response to events inside short-lived execution environments that are reused between invocations — understanding cold starts, concurrency, timeouts, and what survives between calls is what makes Lambda code correct and fast.
Think of it like this
A food truck that only opens when a customer walks up. The first customer waits while the grill heats (a COLD START). Customers right after are served instantly on the hot grill (WARM invocations). A queue of customers at once? More trucks appear, each needing its own warm-up (CONCURRENCY).
Key ideas
- 01
Lambda runs a HANDLER per event (an HTTP request, an S3 upload, an SQS batch, a schedule). You configure memory (128 MB–10 GB — CPU scales proportionally with it), a timeout (max 15 minutes), and an execution role (its permissions). You pay per request and per GB-second of duration.
- 02
An EXECUTION ENVIRONMENT is created on a cold start: download code, start the runtime, run your INIT code (everything outside the handler). It then serves many invocations, ONE AT A TIME, until it's idle long enough to be reclaimed. Put SDK clients, DB connections, and config loading OUTSIDE the handler so warm invocations reuse them.
- 03
CONCURRENCY = number of environments running simultaneously. Each concurrent request needs its own environment, so a burst of 500 requests means ~500 environments (and cold starts). The account has a regional concurrency limit (1,000 by default); RESERVED concurrency caps one function (protecting downstream databases), and PROVISIONED concurrency keeps environments pre-initialized to eliminate cold starts for latency-critical paths.
- 04
Invocation types matter for errors: SYNCHRONOUS (API Gateway) returns errors to the caller; ASYNCHRONOUS (S3, EventBridge) retries twice then sends to a failure destination/DLQ; POLL-BASED (SQS, Kinesis, DynamoDB Streams) retries per batch — use partial batch responses so one bad message doesn't reprocess the whole batch. Because retries happen, handlers must be IDEMPOTENT.
- 05
Limits to design around: 15-minute max runtime, 6 MB synchronous payload,
/tmpup to 10 GB (ephemeral), no long-lived background work after the handler returns. Package as a zip (with LAYERS for shared deps) or a container image up to 10 GB.
In your stack
- →
JVM cold starts are the slowest of the common runtimes — often 1–several seconds with frameworks like Spring because of class loading and dependency-injection setup. Enable SnapStart (restores from a snapshot of the initialized JVM), prefer lightweight frameworks or none, and raise memory to get more CPU during init.
- →
With SnapStart, anything unique captured during init (random seeds, cached credentials, open connections) is duplicated across restored environments — re-establish connections and re-seed in a restore hook.
Code & diagrams
Client created once per execution environment, reused across warm invocations; partial batch failure for SQS.
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({});
export const handler = async (event) => {
const failures = [];
for (const record of event.Records) {
try {
const order = JSON.parse(record.body);
await ddb.send(new PutItemCommand({
TableName: process.env.TABLE,
Item: { pk: { S: `ORDER#${order.id}` }, total: { N: String(order.total) } },
ConditionExpression: "attribute_not_exists(pk)",
}));
} catch (err) {
if (err.name !== "ConditionalCheckFailedException") {
failures.push({ itemIdentifier: record.messageId });
}
}
}
return { batchItemFailures: failures };
};aws lambda update-function-configuration --function-name orders-consumer \
--memory-size 1024 --timeout 30
# Cap concurrency so the function can never overwhelm the database
aws lambda put-function-concurrency --function-name orders-consumer \
--reserved-concurrent-executions 50
# Pre-warm 10 environments on the latency-critical alias
aws lambda put-provisioned-concurrency-config --function-name checkout-api \
--qualifier live --provisioned-concurrent-executions 10
# Tail logs
aws logs tail /aws/lambda/orders-consumer --followExplain it without notes
Why should database connections and SDK clients be created outside the handler function?
An S3-triggered Lambda sometimes processes the same file twice. Why, and how should you handle it?
Practice
A Lambda behind API Gateway has p50 latency of 40 ms but p99 of 2.5 s. What's the likely cause and two ways to reduce it?
A queue-consuming Lambda scaled to 800 concurrent executions and took down the Postgres database. How do you prevent this?
Trade-offs
- ↔
Lambda scales to zero and to thousands with no capacity planning, but you accept cold starts, a 15-minute ceiling, per-invocation isolation that complicates connection reuse and caching, and costs that exceed a container once traffic is high and constant.
Done when you can
I put clients and connections in init code, outside the handler.
I understand cold starts and when to use provisioned concurrency or SnapStart.
I use reserved concurrency to protect downstream systems.
I write idempotent handlers and use partial batch responses for SQS.