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=20Remember
- 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.