Lambda at scale: concurrency and batching

Amazon Web Services · Advanced · 18 min

Production serverless systems fail at the edges: throttles, retries, duplicate events and cold starts.

Concurrency is the number of execution environments serving requests at once. Accounts have a regional limit (1,000 by default, raisable) shared by all functions.

  • Reserved concurrency guarantees a function capacity and also caps it, which protects downstream systems such as a database. Setting it to 0 pauses the function.
  • Provisioned concurrency keeps environments initialised ahead of time to remove cold starts, billed while configured.
  • Beyond the limit, synchronous calls are throttled (429) and async events are retried.

Cold starts are the init phase of a new environment. Reduce them with small deployment packages, lazy imports, arm64, avoiding heavy frameworks at import time, and SnapStart (Java, Python, .NET), which restores from a snapshot of an initialised environment.

SQS event source mappings poll for you. Tune BatchSize, MaximumBatchingWindowInSeconds and ScalingConfig.MaximumConcurrency (to limit how many functions the queue drives). By default, one failure fails the whole batch and every message is retried; enable ReportBatchItemFailures and return the failed message IDs so only those return to the queue. Set the queue's visibility timeout to at least six times the function timeout.

Async invocations retry up to two times; configure maximum event age and on-failure destinations (SQS, SNS, EventBridge, S3) to capture what still fails.

Because every source can deliver duplicates, make handlers idempotent: dedupe on a business key with a conditional DynamoDB write, or use the idempotency utility in Powertools for AWS Lambda, which also provides structured logging, metrics and tracing.

Example

import json

def process(order):
    if order["total"] < 0:
        raise ValueError("invalid total")
    # ... idempotent work keyed on order["id"] ...

def handler(event, context):
    failures = []
    for record in event["Records"]:
        try:
            process(json.loads(record["body"]))
        except Exception:
            failures.append({"itemIdentifier": record["messageId"]})
    return {"batchItemFailures": failures}   # only these return to the queue

if __name__ == "__main__":
    batch = {"Records": [{"messageId": "m1", "body": '{"id": 1, "total": 5}'},
                         {"messageId": "m2", "body": '{"id": 2, "total": -1}'}]}
    print(handler(batch, None))   # {'batchItemFailures': [{'itemIdentifier': 'm2'}]}

# aws lambda create-event-source-mapping --function-name worker \
#   --event-source-arn arn:aws:sqs:eu-west-1:111122223333:orders \
#   --batch-size 10 --function-response-types ReportBatchItemFailures \
#   --scaling-config MaximumConcurrency=20

Remember

  • Reserved concurrency guarantees and caps; provisioned concurrency pre-warms
  • Use ReportBatchItemFailures so one bad message doesn't retry the whole batch
  • Visibility timeout at least 6x the function timeout
  • Every event source can duplicate: build idempotent handlers

Watch out for

  • Letting a queue scale Lambda to hundreds of concurrent functions that exhaust a relational database's connections.

Check yourself

  1. A batch of 10 SQS messages has one bad message and partial batch responses are not enabled. What happens?
  2. You must ensure a function never runs more than 20 copies at once, to protect a database. What do you set?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html