Skip to content

Lambda

13 min read · main part only; folded sections are optional

After this you can answer: How does a queued booking become a reliable Lambda invocation, and which limits actually control it?

Read left to right: a queue supplies work, Lambda runs code, and AWS APIs perform the effects. An event source mapping is a Lambda resource that polls a source and invokes a function. An execution role is the IAM role whose temporary credentials authorize the function and its SQS poller. IAM is AWS’s permissions system; an action such as sqs:ReceiveMessage names an allowed API operation.

flowchart LR
Q["SQS jobs"]:::aws -->|"poll + invoke: mapping; sqs:ReceiveMessage"| L["Lambda worker"]:::service
L -->|"assume: trust lambda.amazonaws.com"| R["minitickets-dev-worker-role"]:::security
L -->|"deduplicate: dynamodb:GetItem / PutItem"| D["DynamoDB processed-jobs"]:::aws
L -->|"send: ses:SendEmail; FromAddress condition"| E["SES"]:::aws
L -->|"log: logs:CreateLogStream / PutLogEvents"| C["CloudWatch Logs"]:::aws

Legend: purple = this service; teal = app code; blue = AWS; amber = IAM; red = failure; dashed = designed, not built.

Open in the map →

The arrow abbreviates sqs:ReceiveMessage; deletion also needs sqs:DeleteMessage. The shared connection table preserves the full action names and code references. The short path chapter gives the same flow in a few minutes.

  1. The API’s outbox relay publishes a committed booking event to SNS. An outbox is a database table of events saved in the same transaction as the booking. SNS delivers separate copies to the email and ticket queues.
  2. Lambda’s SQS poller receives a batch. SQS hides those messages for the visibility timeout, the time before undeleted messages can be received again.
  3. Lambda finds an available execution environment or prepares another one. An execution environment is the isolated runtime, memory and temporary disk in which our code runs.
  4. The Node.js runtime loads module-level code during initialization, or Init. Then it calls handler(event, context). The event contains SQS records; the context contains invocation information such as awsRequestId.
  5. handleBatch validates each message, checks its stable business identity, sends email and records completion. Invalid or unsuccessful messages are reported individually.
  6. The handler returns batchItemFailures, containing the SQS message IDs that failed. The mapping has ReportBatchItemFailures enabled, so Lambda deletes the other messages.
  7. Failed messages become visible again after their current visibility delay. After the queue’s receive threshold, SQS moves them to its dead-letter queue, or DLQ, where failed messages can be investigated.
sequenceDiagram
participant Q as SQS jobs
participant P as Lambda poller
box rgb(237,233,254) Lambda
participant H as worker handler
end
participant D as DynamoDB
participant E as SES
P->>Q: ReceiveMessage, worker role
Q-->>P: Batch, messages temporarily hidden
P->>H: Invoke with SQS records
H->>D: GetItem stable job ID
H->>E: SendEmail if not completed
H->>D: Conditional PutItem completion
H-->>P: batchItemFailures: failed IDs only
P->>Q: DeleteMessage for successful IDs
Q-->>P: Failed IDs visible again, retry or DLQ

Legend: blue = AWS participants; purple group = this service’s handler; arrows show the batch and its effects.

Idempotency means repeating an operation has the same intended effect as doing it once. Our completion record reduces duplicate email, but it does not make SES and DynamoDB one transaction. If SES accepts an email and the completion write fails, a retry can send another email. Two simultaneous workers can also both pass the initial read. Explain this limitation in an interview.

Lambda must prepare an environment before it can run the handler. It initializes extensions, the runtime and static function code. A cold start is the additional preparation needed for a new environment. A warm invocation reuses an initialized environment. Module-level SDK clients in apps/worker/src/lambda.ts survive ordinary reuse, so we avoid constructing clients for every message. Reuse is an optimization, not a durability promise. Lambda can replace an environment at any time. Never put the only copy of a processed-job record in memory. AWS explains the environment lifecycle.

Finish awaited work before returning. A background promise is not a reliable way to send the email later. The environment may freeze after the invocation, and unrelated work can resume during a later call. Keep request-specific data local to the handler to avoid accidentally reusing another customer’s data. Measure initialization separately from handler duration before deciding that every slow invocation is a cold start.

Three invocation models, three retry owners

Section titled “Three invocation models, three retry owners”
ModelWhat happens firstWho owns retries?
SynchronousCaller waits for the function resultCaller or calling integration; Lambda does not automatically retry the function error
AsynchronousLambda accepts the event into its internal queueLambda’s asynchronous delivery configuration
Event source mappingLambda polls SQS or a stream and invokes a batchMapping and source-specific behavior

For asynchronous invocation, the default for a function error is two further attempts, normally after one and two minutes. Throttling and service errors use a different retry path, with an event age of up to six hours by default. A destination receives an invocation record, including outcome information. An asynchronous DLQ instead captures the discarded event with failure attributes. Configure and monitor destination permissions too; failure capture can itself fail. These are the asynchronous retry rules and destination rules.

Our SQS workers use the third model. They do not use Lambda’s asynchronous retry count or function-level asynchronous DLQ. Kinesis and DynamoDB Streams retain ordered records in shards, which are subdivisions of a stream. Their mappings track progress through stream positions. Batch retries can delay later records in a shard; configure retry limits, record age and failure capture for that source. SQS messages instead become visible again and eventually follow the queue’s redrive policy. Start failure analysis by identifying the source and invocation model. AWS distinguishes the retry owners.

Concurrency is how many invocations run at once. In the default execution model used here, an environment handles one invocation at a time. Rough required concurrency is arrival rate multiplied by average execution duration: 20 batches per second lasting 0.25 seconds require about five concurrent invocations. That is an estimate for capacity planning, not a guarantee about burst or tail latency.

Reserved concurrency sets aside capacity for one function and also caps that function. It does not initialize environments. Provisioned concurrency maintains initialized environments for a published version or alias and costs money while allocated. A maximum concurrency on an SQS mapping caps that mapping’s concurrent batches without reserving account capacity. These settings answer different questions. A queue cap cannot guarantee capacity when other functions consume the account pool. AWS documents the concurrency controls.

Our recorded account limit is 10, not the commonly quoted default of 1,000. It is a historical account fact, not a live reading today. The worker’s optional reservation remains unset. Its code records a refused reservation under that low quota. AWS’s general documentation describes keeping 100 units unreserved for ordinary larger quotas; do not generalize our account’s historical minimum of 10 to other accounts. Read the current account setting before proposing reservations. The recorded case preserves the evidence boundary.

More configured memory gives proportionally more CPU. A faster render can offset a higher per-millisecond memory charge, so benchmark the complete operation. /tmp is local temporary disk. It can survive environment reuse, but is neither shared between environments nor durable storage. Use it for replaceable intermediate data, never the authoritative ticket or job state.

A zip deployment package contains code and dependencies for a Lambda runtime. A container image packages the runtime and code in an image stored in ECR, AWS’s container registry. The image must implement Lambda’s runtime interface; an arbitrary web-server image is not sufficient. Our one worker image has two entry points, selected per function, and targets arm64. An image architecture mismatch can cause exec format error. AWS describes the image requirements.

A Lambda layer is a separately versioned zip archive of dependencies mounted under /opt. Layers apply to zip functions. For our image functions, dependencies are included in the image instead. An image’s filesystem layers are a different mechanism. Lambda’s layer documentation makes this distinction explicit.

A published version captures immutable function code and most configuration. $LATEST is the mutable unpublished function. An alias is a named pointer to a published version; weighted routing can divide traffic between two versions. This allows a small rollout and a quick routing rollback, but it cannot undo database side effects. MiniTickets currently updates unqualified functions; it does not build version/alias rollout resources. See versions and aliases.

Attaching Lambda to our VPC, an isolated customer network, gives it private-network access through managed network interfaces. It does not give the function a public IP, even in a public subnet. Internet access then needs appropriate routing, usually a NAT gateway, or private service endpoints where supported. Our workers need SQS, DynamoDB, SES, S3 and Secrets Manager APIs, not Postgres or Valkey. They therefore have no vpcConfig; this means outside our customer VPC, not outside AWS’s managed network. AWS documents VPC access and internet routing.

The ticket function reads its signing key from Secrets Manager using secretsmanager:GetSecretValue. Its environment contains the secret’s identifier, not the key. The code caches a successful read within the process. A rotation design must account for that cache; a long-lived warm environment does not automatically reread a changed secret. Never log a signing key, session token or full message containing private data.

At the time of writing; check the AWS docs. These numbers concern the default Lambda function model used here. AWS also documents Managed Instances and durable functions with different behavior; do not apply these bounds to every newer Lambda execution model.

SettingValueSource
Invocation timeoutUp to 900 secondsLambda quotas
Memory; one vCPU equivalent128–10,240 MB; at 1,769 MBLambda quotas
Temporary disk512–10,240 MBLambda quotas
Package sizesZip: 50 MB direct upload, 250 MB expanded including layers; image: 10 GB expandedLambda quotas
PayloadSynchronous request/response: 6 MB each; asynchronous: 1 MBLambda quotas
Per-function scaling rateUp to 1,000 new environments per 10 seconds, subject to available capacityScaling
Account concurrencyGeneral default 1,000; ours recorded as 10, current value unverifiedAWS quotas, our case
Email worker256 MB; 10-second timeout; mapping batch 10, concurrency cap 5worker-lambda.ts, queue-trigger.ts
Ticket worker512 MB; 30-second timeout; mapping batch 1, concurrency cap 2infra/src/tickets/lambda.ts
VisibilityEmail 60 seconds; tickets 180 secondsinfra/src/jobs/queues.ts (createJobQueues for both kinds)

Built in the repository; environment currently switched off. These are implementation facts, not a claim that either function is running now.

Read infra/src/jobs/worker-lambda.ts → createWorkerLambda for the email function and infra/src/jobs/queue-trigger.ts → createQueueTrigger for the polling configuration. Then read apps/worker/src/lambda.ts → handler and apps/worker/src/batch.ts → handleBatch for execution. SDK calls use short connection/request timeouts; whole-batch worst cases can still exceed ten seconds. The code handles records sequentially, so increasing batch size can increase timeout risk.

infra/src/tickets/lambda.ts → createTicketLambda selects ticket-lambda.handler from the same image. Ticket IDs and object keys are deterministic, so retrying unfinished generation can converge on the same ticket state. Email’s send-before-record sequence has a wider duplicate window. Reliability depends on the effect, not merely on using the same queue mechanism.

Our local runner calls the same handler, but local success cannot establish AWS IAM permissions, account quotas or SES delivery. The committed infra/Pulumi.dev.yaml has data, compute and entry disabled. No AWS settings were changed for this page.

The trust policy lets lambda.amazonaws.com assume the worker role. The permission policy authorizes the API calls. The role is attached to minitickets-dev-worker; iam:PassRole controls who may attach it during deployment. Lambda pulls the image through the ECR repository policy, not through an ECS-style second execution role.

From → toWhat happensWhat allows it
Worker Lambda → CloudWatch alarmsonly when computeEnabled = trueErrors > 0 in 5 minutes → worker-errors
  • metric AWS/Lambda Errors, Sum, period 300 salarmSpecs
Worker Lambda → ECR repositoriesonly when computeEnabled = truethe Lambda service pulls the worker image when the function is created or updated
  • resource-policy repository policy LambdaPullWorkerImage on minitickets-dev-worker: lambda.amazonaws.com may ecr:BatchGetImage and ecr:GetDownloadUrlForLayer when aws:SourceArn is function:minitickets-dev-*allowLambdaPull
Worker Lambda → Worker Lambda execution roleonly when computeEnabled = truethe worker function runs with this role
Worker Lambda → CloudWatch log groupsonly when computeEnabled = trueworker lines go to /minitickets/dev/worker
  • iam worker role WriteWorkerLogs: logs:CreateLogStream, logs:PutLogEvents on /minitickets/dev/worker:*createWorkerRole
  • config loggingConfig logGroup /minitickets/dev/worker (not the default /aws/lambda/…)createWorkerLambda
Jobs SQS queue → Worker Lambdaonly when computeEnabled = truethe event source mapping polls the queue and calls the worker with batches
  • trigger event source mapping: batchSize 10, functionResponseTypes ReportBatchItemFailures, maximumConcurrency 5createQueueTrigger
  • iam worker role ReadJobsQueue: sqs:ReceiveMessage, sqs:DeleteMessage, sqs:ChangeMessageVisibility, sqs:GetQueueAttributes on minitickets-dev-jobscreateWorkerRole
Worker Lambda → Processed-jobs tableonly when computeEnabled = trueGetItem before sending; a conditional PutItem after sending
  • iam worker role ProcessedJobsTable: dynamodb:GetItem, dynamodb:PutItem on minitickets-dev-processed-jobscreateWorkerRole
Worker Lambda → SES email identitiesonly when computeEnabled = trueSendEmail from no-reply@minitickets.theversehub.in
  • iam worker role SendBookingEmails: ses:SendEmail on our identity ARNs, condition ses:FromAddress = no-reply@minitickets.theversehub.increateWorkerRole
  • config env MAILER ses and MAIL_FROM no-reply@minitickets.theversehub.increateWorkerLambda

The ticket function has a separate role for its queue, ticket table, tickets/* S3 writes and the signing secret. No inbound security group rule is needed to invoke our worker. Queue polling and role permissions are the relevant connection controls.

flowchart TB
T["Handler timeout or failed message"]:::failure -->|"not deleted: mapping failure response"| V["SQS visibility expires"]:::aws
V -->|"receive again: redrive policy"| N{"Receive count exceeds 3?"}:::aws
N -->|"no: mapping polls"| R["Invoke worker again"]:::service
N -->|"yes: SQS redrive"| D["jobs-dlq"]:::failure
D -->|"visible count above 0: CloudWatch metric"| A["jobs-dlq-not-empty alarm"]:::aws
A -->|"publish: SNS topic policy"| S["SNS alerts"]:::aws
S -->|"email: confirmed subscription"| O["Operator email"]:::app

Legend: red = failure; blue = AWS queue and alarm behavior; purple = Lambda; teal = notification recipient.

What you seeWhyFix
Task timed out after 10.00 seconds, or REPORT status timeoutBatch, SDK retries or dependency latency exceeds the invocation budgetBound dependency calls; reduce batch work; measure before increasing timeout and visibility together
Throttles rises; queue age growsAccount pool, reserved concurrency or scaling rate limits executionRead current quota and competing use; align mapping caps and downstream capacity
Failed messages disappear after a normal returnMapping lacks ReportBatchItemFailures, or handler incorrectly returns an empty success responseVerify mapping and response contract; malformed or nonexistent failed IDs instead fail the whole batch and cause retry
AccessDeniedException or no pollingRole permission, resource policy or mapping state is wrongCheck exact denied action/resource; verify trust and mapping state; do not add broad permissions
Duplicate email despite successful processingCrash after send or concurrent processing before completionUse business IDs; accept/document the remaining window or adopt a provider-supported deduplication design

The current worker-errors alarm uses Lambda’s Errors metric. A caught per-message error returned in a successful partial response need not increase that metric. Queue age and application logs are essential. A DLQ can contain failures even when invocation-level error counts look healthy. The built jobs-dlq-not-empty alarm checks visible DLQ messages above zero over one minute and publishes through the alerts topic. It keeps its state when data is missing. A confirmed email subscription is required; no current live subscription status is claimed.

At 10x, start with arrival rate, batch duration and oldest-message age. Five concurrent email batches can still exceed the SES sandbox’s sending rate. Concurrency is simultaneous work, not emails per second. Throttle the actual dependency calls or obtain appropriate production quotas. More memory may shorten CPU work, but it does not remove a mail-service rate limit.

At 100x, account concurrency and downstream capacity require a coordinated plan. Separate workers by dependency and keep queue caps explicit. Use business-level deduplication, bounded retries and a replay runbook before raising throughput. Watch durable outbox rows as well as SQS; a failed publisher can leave queues empty while bookings wait in Postgres. For long-running or consistently busy processing, compare ECS tasks with Lambda using measured cost and operational needs. Avoid unbounded parallel requests inside one invocation: they bypass the practical protection suggested by a function concurrency cap.

  • “SQS invokes Lambda asynchronously, so Lambda retries twice.” The mapping polls SQS. Source visibility and redrive govern retry, not the asynchronous invocation retry setting.
  • “Reserved concurrency removes cold starts.” It reserves capacity. Provisioned concurrency initializes environments.
  • “A maximum concurrency of five reserves five.” It caps that mapping but reserves nothing in the account pool.
  • “A successful function return means every message succeeded.” Partial responses can report record failures without an invocation error.
  • “Idempotency means exactly one email.” Our completion record cannot atomically commit an external email send.
  • “A VPC makes Lambda secure by default.” Private routing and IAM solve different problems; a public subnet does not give Lambda a public IP.

A ticket sale creates 10,000 bookings in a minute. How do you process confirmations without delaying checkout?

Commit the booking and outbox event together. Publish a stable event identity, then fan out to independently buffered email and ticket work. Use Lambda mappings with separate batch sizes and concurrency caps. Budget capacity from measured duration and dependency quotas. Deduplicate by booking/job identity; deterministic ticket keys allow retries to repair incomplete generation. Accept the documented duplicate-email window or choose a delivery integration with a stronger idempotency contract. Return the booking result before waiting for either worker. Expose pending ticket state honestly. Use the existing email DLQ, age and worker-error alarms; add equivalent ticket-pipeline coverage before scaling that path.

flowchart LR
A["Booking transaction"]:::app -->|"commit booking + event: SQL transaction"| O["Outbox"]:::app
O -->|"publish: api task role sns:Publish"| S["SNS booking-events"]:::aws
S -->|"deliver: subscription + queue policy"| Q["Independent SQS queues"]:::aws
Q -->|"poll: mappings + queue IAM"| L["Lambda workers"]:::service
L -->|"write/send: scoped execution roles"| D["DynamoDB / S3 / SES"]:::aws

Legend: teal = app transaction; blue = managed services; purple = this service; arrows name the mechanism and permission.

Why create SDK clients outside the handler?
An environment can reuse clients and connections. Durable state still belongs in a data store, because reuse is not guaranteed.
What if the handler times out after sending?
There may be no completion record or partial response. SQS can redeliver, so an email can be sent twice.
Would provisioned concurrency help this queue?
It can reduce startup delay, but costs money and does not fix dependency limits. Buffered email normally tolerates initialization delay.
How would you access private RDS?
Attach the function to suitable VPC subnets, allow the database security-group connection, authenticate, and bound connection use. Also provide routes for any external APIs it needs.
Why not use ECS for the worker?
ECS can suit sustained or longer work and gives process control. Lambda handles bursty bounded batches without an always-running worker; compare measured costs and operating constraints.
  • Identify the invocation model before explaining retries.
  • Reserved concurrency reserves and caps; provisioned concurrency initializes.
  • SQS partial failure reporting needs both mapping configuration and valid failed message IDs.
  • Keep visibility comfortably above the function timeout; AWS recommends at least six times it plus the batch window.
  • A repeatable business effect matters more than an “exactly once” label.
Advanced: Console checks, safe CLI and deeper runtime details

Lambda console → the function → Configuration → Triggers, Concurrency, Permissions and Environment variables. Read metadata only; do not copy secret values. Monitor → Metrics shows Duration, Errors, Throttles and ConcurrentExecutions. The REPORT line includes memory use and can include initialization duration. Our JSON lines carry correlation and invocation IDs.

Terminal window
aws lambda get-account-settings --region us-east-1 --query 'AccountLimit.ConcurrentExecutions'
aws lambda list-event-source-mappings --function-name minitickets-dev-worker --region us-east-1 --query 'EventSourceMappings[].{State:State,Batch:BatchSize,Failures:FunctionResponseTypes,Scaling:ScalingConfig}'
aws lambda get-function-configuration --function-name minitickets-dev-worker --region us-east-1 --query '{Memory:MemorySize,Timeout:Timeout,Architecture:Architectures,Package:PackageType,Vpc:VpcConfig,Logs:LoggingConfig}'

These reads can return not-found while the committed compute switch is off. They were not executed for this page. Source-specific batching and the visibility recommendation are in AWS’s SQS configuration guide. SnapStart, durable functions and Managed Instances are separate choices with their own runtime, region and pricing constraints. This implementation uses none of them. Do not claim all Lambda functions necessarily share the single-invocation environment model described here.

Answer these without scrolling up. If one feels hard, reread that section.

  1. Who polls SQS, and which role authorizes it?
  2. Which invocation model has the built-in two-retry default?
  3. How do reserved, provisioned and mapping concurrency differ?
  4. Where can duplicate email still occur?
  5. Why do our functions have no customer VPC configuration?

Redraw it from memory: SQS, Lambda poller, handler, effects and delete versus retry

Try this: Read the mapping configuration in code and identify the batch size, cap and partial-response setting.

Read this book online: https://book.theversehub.in/