bigonotes
CloudCompute

AWS Lambda: Programming Model and Concurrency

How Lambda receives events, reuses execution environments, counts and caps concurrency, and retries failed events.

8 min readLast updated

AWS Lambda is AWS's function-as-a-service platform. You upload code, connect it to an event source, and Lambda runs it on demand, scaling from zero to many parallel copies. To use it well you need three mental models: the handler contract, the execution environment that runs your code, and how concurrency and retries work. For the general idea of serverless, see Serverless and FaaS.

The pieces of a Lambda setup#

Before the three models, it helps to see what a working function is made of. It involves a few connected parts.

Part Role
Function Your code plus its configuration (memory, timeout, environment variables)
Runtime The language environment, such as Python, Node.js or Java, or a custom runtime
Event source What triggers the function: API Gateway, S3, SQS, EventBridge and others
Execution role An IAM role that grants the function permission to call other services
Downstream resources Services the function calls, such as DynamoDB or S3
Logs Output written to CloudWatch Logs, one log stream per execution environment

The handler, event and context#

The first mental model is the contract between Lambda and your code. The handler is the function Lambda calls for each invocation. You name it in the configuration, for example app.handler for a function handler in app.py. Lambda passes two arguments.

  • event: the input data. Its shape depends on the source: an S3 event lists the bucket and key, and an API Gateway event carries the HTTP method, path, headers and body.
  • context: runtime information, such as the request ID, function name, memory limit and remaining time.

The return value goes back to the caller. Its required shape also depends on the source; API Gateway, for example, expects a status code and a body.

Here's a handler for an API Gateway route that looks up an order in DynamoDB. Notice where the table client is created.

import json
import logging
import os

import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Init code runs once per execution environment, then gets reused.
TABLE = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])


def handler(event, context):
    logger.info("request %s", context.aws_request_id)

    order_id = event["pathParameters"]["id"]
    item = TABLE.get_item(Key={"id": order_id}).get("Item")

    if context.get_remaining_time_in_millis() < 1000:
        logger.warning("close to timeout")

    if item is None:
        return {"statusCode": 404, "body": json.dumps({"error": "not found"})}
    return {"statusCode": 200, "body": json.dumps(item, default=str)}

Three habits show in this example:

  • Create clients outside the handler. Init code runs once per environment, so later invocations reuse the connection.
  • Read config from environment variables. Lambda encrypts them at rest. For real secrets, fetch them from Secrets Manager or Parameter Store rather than storing them as plain variables.
  • Log through the standard logger. Lambda sends stdout and stderr to CloudWatch Logs automatically.

Execution environments: cold and warm starts#

The example's comment about init code hints at the second mental model. Lambda runs your code in an execution environment: an isolated micro-VM with your runtime, code and a /tmp directory. An environment goes through three phases.

  1. Init: Lambda creates the environment, starts the runtime and runs your module-level code.
  2. Invoke: Lambda calls the handler with an event. After it returns, Lambda freezes the environment and may reuse it for the next event.
  3. Shutdown: after a period with no work, or when Lambda chooses, the environment is destroyed.

A cold start is an invocation that has to wait for the Init phase. A warm start reuses an existing environment and skips Init. Cold starts happen on the first call, after a code or config update, after idle periods, and whenever traffic grows past the warm environments you have. Lambda decides when to reuse or replace environments, so never rely on reuse for correctness.

Warm starts are why the init habit pays off. These things survive between warm invocations of one environment:

  • Module-level objects, such as clients and caches.
  • Files in /tmp.
  • Background threads (frozen between calls).

Treat these as a cache, not as storage. Different environments do not share them, and any environment can disappear.

Reducing cold start impact#

Cold starts last from well under a second to several seconds, depending on runtime, package size and init work. You can't remove cold starts, but several levers shrink their impact:

  • Keep init light: smaller packages, fewer imports, lazy-load rarely used libraries.
  • Provisioned concurrency: keeps a set number of environments initialized and ready (see below).
  • SnapStart: for supported runtimes (Java, and newer Python and .NET versions), Lambda snapshots an initialized environment and resumes new ones from it.

You may see an older trick in existing code: "keeping warm" by pinging a function on a schedule. It is dated: it keeps only one or a few environments warm and does nothing for bursts. Prefer provisioned concurrency or SnapStart.

Resources and limits#

Init time is one cost of each environment. Its size is another, and you control it through memory. You choose a memory size, and Lambda allocates CPU and network in proportion to it. More memory often makes CPU-bound functions faster, which can lower total cost.

Setting Range
Memory 128 MB to 10,240 MB
CPU Scales with memory; about one full vCPU at 1,769 MB
/tmp storage 512 MB by default, configurable up to 10,240 MB
Timeout Up to 15 minutes

Work longer than 15 minutes belongs in a container service or a workflow that chains several functions.

Concurrency#

The third mental model covers what happens when many events arrive at once. Concurrency is the number of requests your function is handling at the same moment. Each execution environment handles one request at a time, so if 50 requests are in flight, Lambda runs 50 environments. That removes threading bugs inside one environment, but it means bursts create many environments.

That makes concurrency easy to estimate from numbers you already track:

concurrency≈requests per second×average duration in seconds\text{concurrency} \approx \text{requests per second} \times \text{average duration in seconds}

At 200 requests per second with a 0.25-second average duration, you need about 50 concurrent environments.

Concurrency is capped per account per Region. The default quota is 1,000 concurrent executions, and you can request more. All functions in the Region share that pool. When a function cannot get more concurrency, Lambda throttles: synchronous callers get a 429 TooManyRequestsException, and asynchronous events wait and retry.

Reserved vs provisioned concurrency#

The account pool is shared, so one busy function can starve the others. Two per-function settings help, and they solve different problems.

Reserved concurrency Provisioned concurrency
What it does Sets aside part of the account pool for this function and caps it there Keeps a number of environments initialized before requests arrive
Fixes Noisy neighbors, and protects downstream systems from overload Cold start latency
Extra cost None Yes, you pay while it is configured
Special case Setting it to 0 stops all invocations Can follow a schedule or target with Application Auto Scaling

Use reserved concurrency to cap a function that writes to a small database, or to guarantee capacity for a critical one. Use provisioned concurrency for latency-sensitive APIs.

Invocation types and retries#

Concurrency covers how many copies run. The last piece is what happens when one of them fails, and that depends on how the function was invoked.

Invocation Examples Who retries
Synchronous API Gateway, direct Invoke calls The caller; Lambda returns the error
Asynchronous S3 notifications, SNS, EventBridge Lambda, from an internal queue
Event source mapping (polling) SQS, Kinesis, DynamoDB Streams Lambda's poller, following the source's rules

Asynchronous retries, DLQs and destinations#

For asynchronous invocations, Lambda queues the event and returns success to the sender at once. If the function fails, Lambda retries it two more times by default, waiting longer between attempts. You can lower the retry count to 0 or 1 and set a maximum event age.

If every attempt fails, the event is dropped unless you capture it:

  • A dead-letter queue (DLQ), an SQS queue or SNS topic, receives the failed event.
  • An on-failure destination receives the event plus error details. Destinations can be SQS, SNS, another Lambda function or EventBridge. An on-success destination can route successful results too.

Destinations carry more context than a DLQ, so they are the better default for new functions.

For SQS sources, a failed message goes back to the queue after its visibility timeout and is retried. Configure a redrive policy on the queue itself so poison messages move to a DLQ.

Retries mean the same event can run more than once. Make handlers idempotent: record a processed event ID, or use conditional writes, so a repeat has no extra effect.

Pitfalls#

Most Lambda bugs come from treating environments as long-lived servers.

  • Heavy work in the handler instead of init repeats setup on every call.
  • Relying on /tmp or globals for state breaks when Lambda starts a new environment.
  • No concurrency cap in front of a small database lets a burst open thousands of connections.
  • Async failures with no DLQ or destination vanish silently.
  • Non-idempotent handlers double-charge or double-send on retry.

Key takeaways#

  • Lambda calls your handler with an event (input) and a context (runtime info); put setup code outside the handler.
  • Environments are reused for warm starts; a cold start runs the Init phase first. Never depend on reuse.
  • Memory goes up to 10,240 MB with CPU in proportion; /tmp is configurable up to 10,240 MB; timeout is at most 15 minutes.
  • One environment serves one request at a time; concurrency is roughly rate times duration.
  • Reserved concurrency caps and guarantees; provisioned concurrency pre-warms and costs extra.
  • Async invocations retry twice by default; capture failures with a DLQ or on-failure destination and keep handlers idempotent.
Revision notes · 2 min

Core idea#

Lambda runs your handler on demand in isolated execution environments. One environment serves one request at a time, and Lambda adds environments as concurrency grows.

Key points#

  • The handler gets event (input, shaped by the source) and context (request ID, remaining time, memory).
  • The return value goes to the caller. API Gateway expects a status code and body.
  • Create clients outside the handler. Init code runs once per environment and is reused.
  • Read config from environment variables. Fetch real secrets from Secrets Manager or Parameter Store.
  • An execution role grants permissions. Logs go to CloudWatch Logs.

Execution environment#

  • Phases: Init, Invoke (then freeze), Shutdown.
  • A cold start waits for Init. A warm start reuses an environment.
  • Cold starts follow first calls, updates, idle periods and traffic growth.
  • Globals and /tmp survive warm calls. Treat them as a cache; never rely on reuse.
  • Reduce cold starts: light init, provisioned concurrency, SnapStart on supported runtimes.
  • Scheduled keep-warm pings are dated.

Limits#

Setting Range
Memory 128 MB to 10,240 MB
CPU Scales with memory; one vCPU at 1,769 MB
/tmp 512 MB default, up to 10,240 MB
Timeout Up to 15 minutes

Concurrency#

  • Concurrency ≈ requests per second × average duration in seconds.
  • Default quota: 1,000 concurrent executions per account per Region, shared by all functions.
  • Throttled synchronous calls get 429 TooManyRequestsException.
Reserved Provisioned
Does Caps and guarantees a share Keeps environments initialized
Fixes Noisy neighbors, downstream overload Cold start latency
Extra cost None Yes

Retries#

Invocation Who retries
Synchronous The caller
Asynchronous Lambda: 2 retries by default
Event source mapping The poller, per source rules
  • Capture failed async events with a DLQ or an on-failure destination. Destinations carry error details.
  • For SQS, set a redrive policy on the queue.

Pitfalls#

  • State in /tmp or globals breaks on a new environment.
  • No concurrency cap can overload a small database.
  • Async failures without a DLQ or destination vanish.
  • Non-idempotent handlers double-apply on retry.

Say it in an interview#

  • "Reserved concurrency caps; provisioned concurrency pre-warms."
  • "Async retries twice, then the destination. Handlers are idempotent."

Found a mistake? Email ask@bigonotes.com

Further reading#