Filter concepts by levelShowing all levels.

AWS · Section 17

Lambda — Serverless Compute

Level
intermediate
Read
30 min
Concepts
5

Lambda runs your handler inside a managed execution environment that moves through Init, Invoke, and Shutdown — code outside the handler runs once per environment, handler code runs per request. This section covers that core vocabulary (including versions vs aliases), the three genuinely different invocation models (synchronous, asynchronous, event source mapping), cold starts and the reserved-vs-provisioned-concurrency distinction, why at-least-once delivery makes idempotency a requirement rather than a nicety, and when Lambda is a strong fit versus a long-running service.

What is true here

  1. Init runs once per execution environment (bootstrap + outside-handler code); Invoke runs per request, bounded by the function timeout.
  2. Synchronous invocation blocks the caller; asynchronous queues and returns immediately; an event source mapping has Lambda itself poll the source.
  3. Reserved concurrency caps capacity without pre-warming; provisioned concurrency pre-warms environments to eliminate cold starts, at extra cost.
  4. At-least-once delivery and automatic retries mean handlers must be idempotent, not assume exactly-once execution.
  5. Lambda fits bursty, event-driven, short-lived work; a long-running service fits steady high throughput, workloads over 15 minutes, or specialized networking.

What you will be able to do

  • Place setup code correctly relative to the handler to benefit from environment reuse
  • Identify which invocation model a given AWS event source uses and what that implies for retries
  • Choose between reserved and provisioned concurrency based on whether the goal is capacity or latency
  • Design a Lambda handler to be safely idempotent under at-least-once delivery
  • Decide when Lambda is the right compute choice versus ECS/EC2
From a Lambda invocation to a fit decision
triggered byscales viaimpliesinforms

Init → Invoke → Shutdown

the core lifecycle

Sync, async, or polled

Cold start, concurrency

Retries → idempotency

Right fit?

  • Init → Invoke → Shutdown — the core lifecycle
    • leads to Sync, async, or polled (triggered by)
  • Sync, async, or polled
    • leads to Cold start, concurrency (scales via)
  • Cold start, concurrency
    • leads to Retries → idempotency (implies)
  • Retries → idempotency
    • leads to Right fit? (informs)
  • Right fit?

Lambda — Serverless Compute

The execution lifecycle and core vocabulary, invocation models, cold starts and concurrency, retries and idempotency, and when Lambda fits.

Lambda Core Vocabulary

coreintermediate

A Lambda function runs your handler code in a managed execution environment that goes through Init (bootstrap + your outside-handler code), Invoke (runs the handler per request), and Shutdown (cleanup) phases. Memory (128 MB–10,240 MB) also scales CPU. A version is an immutable snapshot; an alias is a mutable pointer to a version, letting you shift traffic without changing the invoker.

Think of it as

The execution environment is a kitchen that gets set up once (Init — hire staff, stock shelves) and then serves many orders (Invoke) before eventually closing (Shutdown). A version is a signed, dated recipe card that never changes; an alias is a "today's special" sign you can repoint to a different recipe card without reprinting the menu.

What we're doing: See code placement determine whether it runs once (Init) or on every request (Invoke).

handler.pypython
import boto3
s3 = boto3.client('s3')  # Init phase: runs once per execution environment

def handler(event, context):  # Invoke phase: runs on every request
    return s3.list_buckets()
1
The import runs during Init — once per execution environment, not once per request.
2
Creating the S3 client here means every subsequent warm invocation reuses the same client instead of recreating it.
4
Only code inside the handler runs during the Invoke phase, bounded by the function's configured timeout.

Why this works: Placing expensive setup (client creation, DB connections) outside the handler is what makes warm invocations fast — that code runs once during Init and is reused across every subsequent Invoke on the same execution environment.

Creating a new SDK client inside the handler on every invocation

Wrong

python
def handler(event, context):
    s3 = boto3.client('s3')  # recreated every single invocation
    return s3.list_buckets()

Better

python
s3 = boto3.client('s3')  # created once, during Init

def handler(event, context):
    return s3.list_buckets()

What you see: Every warm invocation still pays the cost of re-establishing an SDK client (and, for a database, a new connection), even though the execution environment is already running and could reuse one.

Why: Code inside the handler runs on every Invoke, regardless of whether the environment is warm — only code placed outside the handler, at module scope, benefits from the Init-once/Invoke-many-times lifecycle.

Execution environment lifecycle
readyfinishesyes — warmstartno —eventually

Init

bootstrap + outside-handler code

Invoke

handler runs per request

Reused?

warm start if yes

Shutdown

cleanup, eventually

  • Init — bootstrap + outside-handler code
    • leads to Invoke (ready)
  • Invoke — handler runs per request
    • leads to Reused? (finishes)
  • Reused? — warm start if yes
    • leads to Invoke (yes — warm start)
    • leads to Shutdown (no — eventually)
  • Shutdown — cleanup, eventually

Remember: Init runs once per environment (bootstrap + outside-handler code); Invoke runs per request, bounded by timeout. Version = immutable snapshot; alias = mutable pointer callers actually target.

See also: invocation models · cold starts and concurrency

Synchronous, Asynchronous, and Event Source Mappings

coreintermediate

Synchronous invocation makes the caller wait for a response (API Gateway calling Lambda). Asynchronous invocation queues the event and returns immediately (S3, SNS calling Lambda) — Lambda retries on failure separately from the caller. An event source mapping is Lambda itself polling a source (SQS, DynamoDB Streams, Kinesis) and invoking your function with batches.

Think of it as

Synchronous is a phone call — you wait on the line for the answer. Asynchronous is dropping a letter in a mailbox — you walk away immediately, and delivery/retry is someone else's problem now. An event source mapping is Lambda itself checking the mailbox on a schedule and walking incoming letters over to you in batches.

bash
--invocation-type RequestResponse   # synchronous (default)
--invocation-type Event             # asynchronous

What we're doing: See the CLI-level difference between invoking synchronously and asynchronously.

invoke.shbash
aws lambda invoke --function-name my-fn --payload '{}' out.json
# waits for the function to finish, out.json holds the real response
aws lambda invoke --function-name my-fn --invocation-type Event --payload '{}' out.json
# returns immediately with StatusCode 202, out.json is empty
1
Default invocation type is RequestResponse — synchronous. The CLI call blocks until the function finishes.
3
Event invocation type is asynchronous — the CLI call returns as soon as Lambda accepts the event into its internal queue, not when the function finishes.

Why this works: The StatusCode 202 on the async call confirms only that Lambda accepted the event for processing — it says nothing about whether the function later succeeded, which is exactly why async invocations need their own retry and failure-handling configuration.

Assuming a 202 response from an async invoke means the function succeeded

Wrong

text
# "The CLI returned StatusCode 202, so the function ran successfully."

Better

text
# 202 only means Lambda accepted the event into its queue — configure a
# dead-letter queue or on-failure destination to observe actual outcomes

What you see: A function silently fails on every invocation for days because nothing was watching whether the asynchronously-queued events actually succeeded.

Why: Asynchronous invocation deliberately decouples "accepted" from "succeeded" — the caller gets an immediate 202 the moment the event is queued, before Lambda has even started running the function, so success has to be observed through a separate mechanism.

Who controls delivery, and does the caller wait
Synchronous (API Gateway)
caller blocks for the response
Asynchronous (S3, SNS)
queued, returns 202 immediately
Event source mapping (SQS, Kinesis)
Lambda polls, invokes with a batch
  • Synchronous (API Gateway): caller/source pushes, caller waits — caller blocks for the response
  • Asynchronous (S3, SNS): caller/source pushes, returns immediately — queued, returns 202 immediately
  • Event source mapping (SQS, Kinesis): Lambda itself polls, returns immediately — Lambda polls, invokes with a batch

Which invocation model a source uses

Which invocation model a source uses
TriggerModel
API Gateway, ALBSynchronous
S3 event notifications, SNSAsynchronous
EventBridge rule targetAsynchronous
SQS queueEvent source mapping (polling)
DynamoDB Streams, KinesisEvent source mapping (polling, ordered per shard)

Together

bash
aws lambda invoke --function-name my-fn --invocation-type Event --payload '{"key":"value"}' out.json

Remember: Synchronous: caller waits, errors return directly. Asynchronous: Lambda queues and returns immediately, retries happen separately. Event source mapping: Lambda itself polls the source and controls batching.

See also: lambda core vocabulary · retries and idempotency

Cold Starts, Reserved and Provisioned Concurrency

coreintermediate

A cold start happens when Lambda has to create and initialize a new execution environment before it can run your handler — typically under 1% of invocations, from under 100ms to over 1 second. Reserved concurrency sets a hard min/max on how many concurrent executions a function can use. Provisioned concurrency pre-initializes environments so requests never hit a cold start, at extra cost.

Think of it as

Reserved concurrency is a locked reservation of parking spots — guaranteed available for this function, capped at that number, unusable by anyone else. Provisioned concurrency is those reserved spots with a car already idling in each one, so there's no startup delay when a driver arrives.

bash
aws lambda put-function-concurrency --function-name my-fn --reserved-concurrent-executions 100
aws lambda put-provisioned-concurrency-config --function-name my-fn --qualifier LIVE --provisioned-concurrent-executions 20

What we're doing: See why reserved concurrency alone does not remove cold starts.

concurrency.shbash
aws lambda put-function-concurrency --function-name checkout --reserved-concurrent-executions 50
# a burst of 50 concurrent requests still cold-starts each new environment
1
Reserved concurrency guarantees up to 50 concurrent execution slots exist for this function — it says nothing about whether those environments are pre-initialized.
2
Each of the 50 environments is still created on demand, so a sudden burst still pays the cold-start cost for every environment Lambda has to stand up.

Why this works: Reserved concurrency answers "how many can run at once," while provisioned concurrency answers "are they ready before the request arrives" — they solve different problems and are often used together.

Setting reserved concurrency expecting it to eliminate cold starts

Wrong

text
# "We set reserved concurrency to 100, so latency-sensitive requests should be fast now."

Better

text
# Reserved concurrency controls capacity/isolation; add provisioned
# concurrency specifically to pre-warm environments for latency-sensitive paths

What you see: A latency-sensitive endpoint still shows occasional multi-hundred-millisecond spikes after reserved concurrency was configured, because new environments are still being created on demand.

Why: Reserved concurrency reserves capacity from the account pool and caps how far the function can scale — it never pre-initializes anything, so cold starts on new environments still occur exactly as before.

A cold start, end to end
  1. Request arrives

    No warm environment available

    triggers a new environment

  2. +0ms

    Download code

    part of the cold-start cost

  3. +~50-900ms

    Start environment + run Init

    the bulk of the delay

  4. Handler runs

    Request finally processed

    environment stays warm for reuse

  1. Request arrives: No warm environment available — triggers a new environment
  2. +0ms: Download code — part of the cold-start cost
  3. +~50-900ms: Start environment + run Init — the bulk of the delay
  4. Handler runs: Request finally processed — environment stays warm for reuse

Reserved vs provisioned concurrency

Reserved vs provisioned concurrency
PropertyReservedProvisioned
What it guaranteesMax + min concurrency slotsPre-warmed environments
Cold startsStill possibleNot possible while within the provisioned amount
CostNo additional chargeAdditional charge
Effect on other functionsRemoves that capacity from the shared poolNone — separate from reserved

Remember: Cold start = new environment created on demand, under 1% of invocations. Reserved concurrency = capacity floor/ceiling, no effect on cold starts. Provisioned concurrency = pre-warmed environments, extra cost, eliminates cold starts up to that count.

See also: invocation models · retries and idempotency

Retries and Idempotency

standardintermediate

Lambda's asynchronous invocations retry automatically on failure, and most event sources deliver at-least-once — so the same event can genuinely reach a handler more than once. A handler needs to produce the same end result whether it runs once or twice for the same event.

Think of it as

AWS handling the infrastructure means the function itself must assume it can be invoked more than once for the same logical event — because retries, at-least-once delivery, and asynchronous redelivery are the price of not managing that infrastructure yourself.

text
At-least-once delivery + automatic retries → handler must be idempotent, not "run exactly once"

What we're doing: See why a non-idempotent handler breaks under normal Lambda retry behavior.

charge_handler.pypython
def handler(event, context):
    charge_customer(event['order_id'], event['amount'])  # not idempotent
    return {'status': 'charged'}
2
If this invocation succeeds but the confirmation is lost (network blip, timeout), Lambda or the upstream source may redeliver the same event, charging the customer twice.

Why this works: Nothing about this handler is unusual — it is the normal, expected retry/at-least-once behavior of Lambda's invocation models that turns a non-idempotent side effect into a duplicate charge, not a rare edge case.

Remember: Lambda's managed infrastructure comes with at-least-once delivery and automatic retries on failure — write handlers to be idempotent (safe to run twice for the same event), not to assume exactly-once execution.

See also: invocation models · cold starts and concurrency

When Lambda Is a Strong Fit

standardintermediate

Lambda is a strong fit for event-driven, bursty, short-lived work, where per-invocation billing and zero server management outweigh cold starts and the 15-minute execution ceiling. A long-running service (ECS/EC2) fits steady, sustained, or longer-duration workloads better.

Think of it as

Lambda fits event-driven, bursty, short-lived work where per-invocation billing and zero server management outweigh cold starts and a 15-minute execution ceiling. A long-running service fits steady, sustained, or long-duration workloads where those constraints stop paying off.

text
Event-driven + bursty + short-lived → Lambda. Steady/sustained + long-running + specialized networking → ECS/EC2.

What we're doing: Decide between Lambda and ECS for two different workloads.

decision.txttext
Image thumbnail generation on S3 upload, bursty, seconds per job
→ Lambda: event-driven, short-lived, scales to zero when idle.

24/7 video transcoding pipeline, sustained high throughput
→ ECS/EC2: steady utilization makes per-invocation billing costlier.
1
Bursty, event-driven, short jobs are exactly Lambda's strength — no idle cost, automatic scaling per S3 event.
3
Sustained high throughput is where a continuously-running, right-sized service usually costs less than per-invocation Lambda billing at the same volume.

Why this works: The same underlying capability (run code) is available either way — the deciding factor is whether the workload's shape (bursty vs steady, short vs long) matches what each pricing and execution model rewards.

Remember: Lambda: event-driven, bursty, short-lived, zero idle cost. A long-running service fits steady high throughput, workloads over 15 minutes, or specialized networking needs — know the crossover before defaulting to either.

See also: cold starts and concurrency · ecs on ec2 vs fargate

Advertisement