Filter concepts by levelShowing all levels.

AWS · Section 44

Serverless Architecture

Level
advanced
Read
30 min
Concepts
3

Serverless applications are assembled from a few repeating shapes. Synchronously: API Gateway in front of Lambda in front of DynamoDB, where the client sees failures and owns retries. Asynchronously: an S3 notification, an EventBridge rule, or an SQS queue invoking Lambda, where the event source's own rules decide retries and where failures land — which makes choosing the source a reliability decision rather than a wiring one. A queue plus reserved concurrency is the standard protection for any downstream that cannot scale the way Lambda does, a relational connection pool most of all. The limits are boundaries the architecture sits inside: 900 seconds, 128 to 10,240 MB with CPU allocated in proportion (one vCPU at 1,769 MB), 1,000 concurrent executions per Region by default, and 6 MB synchronous / 1 MB asynchronous payloads — and AWS itself points out that API Gateway's 10,000 rps default admits more than Lambda's 1,000 concurrency can serve. Asynchronous delivery is at-least-once, so handlers must be idempotent through an atomic conditional write rather than a read-then-act check. And serverless has an edge: workloads over fifteen minutes, saturated all day, holding long-lived connections, or paying a heavy initialization on every cold start belong on containers, and working around those limits usually costs more complexity than moving would have.

What is true here

  1. The event source determines retry and failure semantics — choose it deliberately.
  2. A queue plus reserved concurrency turns a scaling spike into latency instead of a downstream outage.
  3. 900 s / 10,240 MB / 1,000 concurrency / 6 MB sync payloads are design boundaries, not tunables.
  4. At-least-once async delivery makes idempotency a requirement; use a conditional write.
  5. Long-running, saturated, connection-holding, or slow-init workloads belong on containers.

What you will be able to do

  • Assemble the standard synchronous and asynchronous serverless shapes and say what fails where
  • Protect a bounded downstream with a queue and reserved concurrency
  • Recite the limits that shape a serverless design and what each one forces
  • Write an idempotent handler that is safe under concurrent duplicate delivery
  • Recognise the workloads where serverless is the wrong tool, and price the alternative
Composing serverless, inside its limits, until it stops fitting
bounded bywhicheventually says

Composition patterns

Limits, concurrency, idempotency

When to reach for a container instead

  • Composition patterns
    • leads to Limits, concurrency, idempotency (bounded by)
  • Limits, concurrency, idempotency
    • leads to When to reach for a container instead (which eventually says)
  • When to reach for a container instead

Serverless Architecture

The composition patterns, the limits and delivery semantics that shape them, and the workloads that belong elsewhere.

Composition Patterns: API Gateway, DynamoDB, S3, EventBridge, SQS

coreadvanced

Serverless architectures are assembled from a small set of repeating shapes. A synchronous API is API Gateway in front of Lambda in front of DynamoDB. Asynchronous work is an event source — S3, EventBridge, or SQS — invoking Lambda. The important design question in each shape is what happens when the function fails, because that answer differs per event source.

Think of it as

Lambda is glue between managed services, and the event source is what decides the retry and failure semantics. Choosing the source is therefore choosing the reliability model — not just choosing what triggers the code.

What we're doing: Stop a traffic spike from exhausting a relational database through Lambda.

queue-in-front.txttext
Direct: API Gateway -> Lambda -> RDS.
A spike arrives. Lambda scales to hundreds of concurrent environments,
each opening a database connection. max_connections is reached, and
every request — including the ones that were fine — starts failing.

Buffered: API Gateway -> Lambda (accept + enqueue) -> SQS
                     -> Lambda (worker, reserved concurrency 10) -> RDS
The spike lands in the queue. Ten workers drain it at the rate the
database can take. Latency rises; nothing fails.
1
Lambda's scaling is a feature everywhere except in front of something that does not scale the same way. A connection pool is exactly that.
6
Reserved concurrency turns Lambda into a bounded worker pool. The number comes from the downstream's capacity, not from the incoming rate.

Why this works: The most common serverless failure is not Lambda breaking — it is Lambda working perfectly and overwhelming something that cannot follow. A queue plus reserved concurrency converts an availability failure into a latency increase, which is almost always the better trade.

Chaining Lambdas synchronously to build a workflow

Wrong

text
# Lambda A invokes Lambda B synchronously, which invokes Lambda C

Better

text
# Use Step Functions for the workflow, or an event/queue between the
# stages so each one retries independently

What you see: You pay for A and B to sit idle while C runs, the whole chain is bounded by one 15-minute timeout, and a failure in C means re-running everything.

Why: A synchronous invoke blocks the caller and bills for the waiting time, so a three-deep chain triples the cost of the slowest step. It also collapses three independent retry boundaries into one, which is exactly what an orchestrator or a queue exists to keep separate.

The two halves of a serverless application
HTTPSinvokesreads/writeschangeeventsinvokes

Client

API Gateway

auth, throttling, routing

Lambda (sync)

client waits; failures are visible

DynamoDB

single-digit-ms reads and writes

EventBridge / SQS / S3 event

the asynchronous half

Lambda (async)

retries + DLQ decide correctness

  • Client
    • leads to API Gateway (HTTPS)
  • API Gateway — auth, throttling, routing
    • leads to Lambda (sync) (invokes)
  • Lambda (sync) — client waits; failures are visible
    • leads to DynamoDB (reads/writes)
  • DynamoDB — single-digit-ms reads and writes
    • leads to EventBridge / SQS / S3 event (change events)
  • EventBridge / SQS / S3 event — the asynchronous half
    • leads to Lambda (async) (invokes)
  • Lambda (async) — retries + DLQ decide correctness

What each event source gives you

What each event source gives you
SourceInvocationFailure handling
API GatewaySynchronousThe error goes back to the client; nothing retries for you
S3 event notificationAsynchronousLambda retries, then an on-failure destination or DLQ
EventBridge ruleAsynchronousRetries per the rule, then a dead-letter queue on the target
SQS (event source mapping)Poll-based batchesMessage returns to the queue after the visibility timeout; DLQ after maxReceiveCount
DynamoDB / Kinesis streamPoll-based, ordered per shardA failing batch blocks its shard until it succeeds or is bisected/expired

Together

text
# The queue-in-front pattern, protecting a bounded downstream
API Gateway -> Lambda (validate, enqueue) -> SQS
            -> Lambda (worker, reserved concurrency = 10) -> RDS
# 10 is chosen from the connection pool, not from the traffic

Remember: Synchronous: API Gateway → Lambda → DynamoDB, and the client owns retries. Asynchronous: an event source invokes Lambda and the source's rules own retries. Put a queue in front of anything that cannot scale like Lambda, and bound it with reserved concurrency.

See also: serverless operational limits · when not to use serverless · invocation models

Cold Starts, Limits, Concurrency, Retries, and Idempotency

coreadvanced

Serverless removes servers, not limits. A function runs for at most fifteen minutes with at most 10,240 MB of memory, an account starts with a thousand concurrent executions per Region, and payloads are capped at 6 MB synchronous and 1 MB asynchronous. Asynchronous invocations are retried, which makes idempotency a requirement rather than a nicety.

Think of it as

Every limit here is a boundary the architecture has to sit inside, not a knob to turn later. The two that reshape designs most often are the fifteen-minute ceiling — which rules out long jobs — and at-least-once delivery, which rules out non-idempotent handlers.

What we're doing: Make an asynchronous handler safe under at-least-once delivery.

idempotent_handler.pypython
def handler(event, context):
    for record in event["Records"]:
        body = json.loads(record["body"])
        try:
            # The conditional write IS the idempotency check.
            table.put_item(
                Item={"pk": f"order#{body['order_id']}", "status": "charged"},
                ConditionExpression="attribute_not_exists(pk)",
            )
        except ClientError as exc:
            if exc.response["Error"]["Code"] == "ConditionalCheckFailedException":
                continue          # already processed — not an error
            raise

        charge_card(body["order_id"], body["amount_cents"])
6
The write and the "have I seen this?" check are the same operation, so there is no window between checking and acting.
13
A duplicate is a normal outcome under at-least-once delivery, not a failure. Treating it as an error would send a perfectly-processed message to the dead-letter queue.

Why this works: Retries are the platform doing its job — a network blip on the response, a timeout, a redelivery after the visibility timeout. Since duplicates are guaranteed rather than unlikely, the handler is where correctness has to be established, and a conditional write is the cheapest place to establish it.

Checking "have I already processed this?" with a read, then acting

Wrong

python
if not table.get_item(Key=key).get("Item"):
    charge_card(...)
    table.put_item(Item={...})

Better

python
table.put_item(Item={...},
               ConditionExpression="attribute_not_exists(pk)")
charge_card(...)   # only reached when the write won

What you see: Two concurrent deliveries of the same message both read "not processed", both charge the card, and both write the record.

Why: A read followed by a write is two operations with a gap between them, and Lambda runs duplicates concurrently rather than sequentially. A conditional write is atomic at the database, which closes the gap the read-then-act pattern leaves open.

Limits that shape the design, outermost first

Account: 1,000 concurrent executions per Region

A soft quota, but shared by every function in the account

Function: 900 s timeout, 128–10,240 MB

CPU scales with memory; one vCPU at 1,769 MB

Invocation: 6 MB sync / 1 MB async payload

Large payloads go in S3, with the key in the event

Delivery: at-least-once for async

The handler must be idempotent

  1. Account: 1,000 concurrent executions per Region — A soft quota, but shared by every function in the account
  2. Function: 900 s timeout, 128–10,240 MB — CPU scales with memory; one vCPU at 1,769 MB
  3. Invocation: 6 MB sync / 1 MB async payload — Large payloads go in S3, with the key in the event
  4. Delivery: at-least-once for async — The handler must be idempotent

The limits worth memorizing, and what each one forces

The limits worth memorizing, and what each one forces
LimitValueDesign consequence
Function timeout900 secondsLong jobs go to ECS, Batch, or Step Functions
Memory128 MB – 10,240 MBMemory is also the CPU dial — 1 vCPU at 1,769 MB
Concurrency (default)1,000 per RegionReserved concurrency protects downstreams and other functions
Payload (sync)6 MB request and responsePass an S3 key, not the object
Payload (async)1 MBSame, and stricter
/tmp512 MB – 10,240 MBScratch space only — it is not shared or durable

Together

text
# Reserved concurrency does two jobs at once
aws lambda put-function-concurrency \
  --function-name order-worker --reserved-concurrent-executions 10
# 1. caps this function at 10 (protects the database)
# 2. guarantees it 10 out of the account pool (protects it from others)

Remember: 15-minute timeout, 128–10,240 MB (CPU scales with it), 1,000 default concurrency per Region, 6 MB sync / 1 MB async payloads. Async delivery is at-least-once, so handlers must be idempotent — use a conditional write, not a read-then-act check.

See also: serverless composition patterns · when not to use serverless · cold starts and concurrency

When Not to Force Serverless

standardadvanced

Serverless suits work that is short, bursty, and stateless. It suits long-running processes, sustained high-throughput compute, and anything that needs a persistent connection or an unusual runtime much less well. The signal is not philosophical: the workload starts fighting one of Lambda's documented limits, and the workarounds get more complex than a container would have been.

Think of it as

Lambda bills per millisecond of execution, so it is cheap when idle and expensive when constantly busy. Somewhere between "a few requests a minute" and "saturated all day", a container that is always running becomes both cheaper and simpler — and that crossing point is the decision.

text
# The decision, as three questions
1. Can every unit of work finish in under 15 minutes?
2. Is the workload bursty or idle much of the time?
3. Is it stateless between invocations?
# Three yeses -> serverless fits. A no -> price the container option.

Working around the 15-minute timeout with self-invocation

Wrong

text
# At 14 minutes, the function invokes itself with a "continue from here"
# cursor and exits

Better

text
# Step Functions with a Map state over the work items, or an ECS task
# that simply runs to completion

What you see: The job has no single execution to observe, failures leave it half-done with no obvious resume point, and a bug in the cursor logic silently reprocesses or skips a range.

Why: Self-invocation reimplements orchestration — checkpointing, resumption, failure handling — inside application code, without the execution history that would make it debuggable. Step Functions provides exactly that machinery, and a container simply does not have the limit being worked around.

Which limit the workload hits first

Which limit the workload hits first
WorkloadLimit it fightsBetter fit
A 40-minute nightly report15-minute timeoutECS scheduled task, or AWS Batch
Video transcoding at scaleDuration + sustained costECS/EC2, or a purpose-built media service
A WebSocket server holding sessionsNo persistent execution environmentECS behind an ALB, or API Gateway WebSocket + external state
A service saturated 24/7Per-millisecond billing at 100% utilizationRight-sized ECS or EC2 with reserved capacity
Large model inferenceCold start + package/memory limitsA container service, or a managed inference endpoint
Thousands of concurrent VPC-attached functionsENIs per VPCA container service, or VPC endpoints and fewer VPC attachments

Together

text
# The honest cost comparison, per month
Lambda:  invocations x duration x memory, only while working
Fargate: vCPU-hours x 24 x 30, whether busy or not
# Bursty and idle -> Lambda wins. Saturated all day -> it usually does not.

Remember: Serverless fits short, bursty, stateless work. Fighting the 15-minute timeout, holding a long-lived connection, running saturated all day, or paying a long initialization on every cold start are all signals to price a container instead. Measure the cost comparison rather than arguing it.

See also: serverless operational limits · when lambda fits · when to use ecs

Advertisement