Design Lambda–SQS Retries Around the Records That Failed
A four-record batch is enough to expose the reliability problem. In a synthetic test, record A can complete its business effect, record B can fail validation before any effect, record C can commit an external operation but lose the response, and record D can terminate the invocation before the handler returns. The interesting question is not whether the Lambda invocation is green or red. It is: what is known about each record, what business effect may already exist, and what action is safe next?
Amazon Simple Queue Service (SQS) event source mappings are explicitly an at-least-once mechanism: Lambda polls the queue, invokes the function synchronously with a batch, and duplicate processing can occur. AWS therefore recommends idempotent processing. A successful batch lets Lambda delete its messages; a failed batch normally becomes visible again after the visibility timeout. AWS, Using Lambda with Amazon SQS, living documentation; publication date not asserted; accessed September 17, 2026.
This playbook uses a synthetic standard-queue lab in the AWS commercial partition with the supported nodejs24.x Lambda runtime pinned for the fixture. AWS lists Node.js 24 as supported; preview runtimes are deliberately excluded from the main lab. AWS, Lambda runtimes, living documentation; publication date not asserted; research cutoff September 17, 2026. The tests below are proposed unless explicitly described as documented service behavior; no cloud execution result is claimed.
Define the record-level recovery contract
The operating invariant is simple: every delivered record must have a business identity, an observed outcome, and an approved next action. That is a stronger review standard than “the handler returned successfully,” because the Lambda response settles only the event-source mapping’s acknowledgement decision. It does not atomically commit or roll back a database write, an entitlement grant, an email send, or an external API call.
This is also why the scope stays narrow. The mechanism here is a Lambda SQS event source mapping, not a Lambda asynchronous invocation destination, a Kinesis checkpoint, an SNS delivery policy, or a direct API Gateway request. Those mechanisms have different acknowledgement and retry contracts. General serverless API architecture is useful context, but the recovery decision here belongs at the record/effect boundary, not at the provider-overview level.
Boundary | What the platform or application knows | What it does not prove | Recovery question |
SQS delivery | A message was received with transport metadata | That this is the first delivery | Is this business operation already known? |
Lambda invocation | A batch entered one handler invocation | That every record reached the same processing stage | Which records started, finished, or were interrupted? |
Application processing | Validation and business logic reached recorded states | That an external side effect committed | What durable evidence exists? |
External effect | Destination may confirm, reject, or leave the result ambiguous | That Lambda will acknowledge the record | May the operation safely be issued again? |
The contract deliberately produces three decisions rather than one: go when repeating work is proven safe or unnecessary, hold when evidence is missing, and reconcile when an authoritative destination or business owner must resolve ambiguity. “Retryable” is therefore a business conclusion backed by evidence, not a synonym for “the function threw.”
Inventory the mapping before changing the handler
Before writing partial-batch code, capture the deployed system. Source control can tell you what somebody intended; recovery depends on what Lambda and SQS are actually configured to do. The inventory should be timestamped and attached to the change or incident record so reviewers can later explain why a message was retried, deleted, or dead-lettered.
At minimum, record the event-source mapping UUID and source Amazon Resource Name (ARN), queue type, Lambda runtime and deployed code revision, enabled function response types, batch size and batching window, function timeout, queue visibility timeout, source/DLQ retention, redrive maxReceiveCount, reserved or mapping concurrency controls in use, and the identity of the deployment artifact. The exact concurrency design is workload-specific; the point is to record it because throttling and capacity limits alter the retry timeline.
Inventory field | Example fixture value | Why recovery needs it |
Mapping UUID | esm-EXAMPLE-001 | Identifies the mapping whose response contract is active |
Source | synthetic-orders-standard | Distinguishes standard from FIFO behavior |
Runtime | nodejs24.x | Pins the executable fixture |
Function response types | ReportBatchItemFailures or empty | Determines whether record failures can be reported |
Batch settings | size 4, window 2s | Bounds what can share one invocation |
Timeout / visibility | example only: 20s / 122s | Supports visibility review; values are not universal |
Redrive | synthetic DLQ, example maxReceiveCount=5 | Explains when poison records leave the source queue |
Code revision | immutable deployment ID | Connects behavior to code actually running |
AWS documents the event-source setting as FunctionResponseTypes and the partial-batch value as ReportBatchItemFailures; AWS also exposes the mapping configuration for read-back. The broader documentation point is the same as with event-driven API contracts: a schema describes a message, but it does not by itself establish acknowledgement, retry, or business-effect semantics.
Separate transport identity from business identity
Do not use “message ID” as shorthand for every identity in the system. SQS assigns a messageId to a message. A receipt handle belongs to a receive action, and AWS documents that a newly received copy gets a different receipt handle; the latest receipt handle is needed for deletion. AWS, Amazon SQS queue and message identifiers, living documentation; publication date not asserted; accessed September 17, 2026.
Record four identifiers separately where they exist: SQS messageId; current receipt handle, retained only where operationally appropriate; a producer event identifier inside the payload; and a domain operation key such as entitlement:account-41:feature-alpha:v3. The domain key is what lets the application recognize the same intended business operation if a producer sends a second SQS message with a different transport identifier.
Give every record an evidence state
Use a proposed durable ledger rather than reconstructing history from log lines. A compact state model is enough:
State | Meaning | Permitted next action |
RECEIVED | Transport record observed | Start after identity and payload checks |
STARTED | Business processing claimed | Resume only under concurrency rules |
EFFECT_CONFIRMED | Authoritative evidence says effect exists | Do not reissue; acknowledge when possible |
EFFECT_UNKNOWN | Request may have committed but confirmation was lost | Reconcile; never blindly repeat |
FAILED_BEFORE_EFFECT | Failure occurred before side-effect boundary | Retry after cause is acceptable |
RESPONSE_EMITTED | Handler included record in final response decision | Compare with mapping behavior |
Assign an owner to EFFECT_UNKNOWN. An ambiguous record that nobody owns is not merely “pending”; it is a replay hazard.
Build a small synthetic batch with deliberate failure points
Use four synthetic records whose failure modes can be switched deterministically. The business operation is an invented “feature entitlement grant,” chosen because it has an observable destination state without involving customer data or money. The destination for unit tests is a local fake that can confirm a write, reject before writing, or persist a write and then close the connection before sending confirmation.
Record | Domain key | Injected behavior | Expected evidence before handler response |
A | entitlement:41:alpha:v3 | Commit and confirm | EFFECT_CONFIRMED |
B | entitlement:42:beta:v3 | Schema rule rejects before destination call | FAILED_BEFORE_EFFECT |
C | entitlement:43:gamma:v3 | Commit, then simulate lost response | EFFECT_UNKNOWN until lookup |
D | entitlement:44:delta:v3 | Invocation interruption switch | Evidence stops wherever interruption occurs |
Prerequisites are deliberately bounded: an isolated non-production stack in a commercial AWS Region; a standard SQS source queue and DLQ containing only synthetic payloads; a Lambda function pinned to nodejs24.x; least-privilege execution permissions; a durable test effect ledger; and a fake or disposable destination whose state can be queried. Do not put credentials in messages, logs, test fixtures, or incident exports.
Run the fixture in two layers. First, use the local fake to prove application transitions. Assert that A writes once and reaches EFFECT_CONFIRMED; B makes zero destination calls and reaches FAILED_BEFORE_EFFECT; C writes once but reaches EFFECT_UNKNOWN; and a second attempt of C performs reconciliation before any second write. This verifies the application model only. It does not prove how the managed event-source mapping interprets a response.
Second, send equivalent synthetic records to the disposable AWS queue. With the interruption switch disabled, validate partial response behavior. With it enabled, terminate or time out the invocation in a separate test and observe which records reappear. Capture message IDs, approximate receive counts where available, Lambda request IDs, ledger states, response payloads that were actually emitted, and queue observations. The expected assertion is not “no duplicates”; it is “every redelivery has a defensible next action.”
Cleanup is part of reproducibility. Disable the test mapping, export only the synthetic identifiers and state transitions needed for evidence, drain or purge only the disposable queues, delete the temporary queues/function/ledger, and remove the fake destination data. A mock passing locally is evidence about code paths; a disposable managed-service run is the evidence for mapping behavior.
Make each test repeatable from a manifest rather than from hand-edited messages. The manifest should pin the four payloads, the destination fault mode, the expected ledger transition, and whether interruption is enabled. Reset the fake destination and effect ledger between cases so record C’s prior commit cannot accidentally make a later run look idempotent. For the AWS layer, use a fresh fixture namespace or record the prior state explicitly. The assertion should compare the expected and observed sequence of evidence, not only the final queue location.
One useful negative control is to run A twice under two distinct SQS messages carrying the same domain key. The expected application assertion is one business effect and two observed transport identities. That test demonstrates why SQS message idempotency and business idempotency are different problems without depending on a rare duplicate delivery from the service itself.
Validate the partial-response contract at both ends
Partial reporting has two prerequisites: the event-source mapping must have ReportBatchItemFailures enabled, and the handler must return the documented response shape. AWS states that, by default, a batch error makes every message in the batch visible again. With ReportBatchItemFailures, Lambda can make only the reported failed messages visible again. A function-level exception is different: AWS treats the entire batch as failed. AWS, Handling errors for an SQS event source in Lambda, living documentation; publication date not asserted; accessed September 17, 2026.
A minimal unexecuted Node.js 24 handler for the response mechanics looks like this. It is intentionally not an idempotency implementation:
// Unexecuted fixture code: response mechanics only.
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
await processRecord(record); // synthetic test implementation
} catch (error) {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
The response validator should test documented cases separately. AWS treats an empty batchItemFailures list, a null list, an empty event response, or a null event response as complete success. It documents invalid JSON, empty or null itemIdentifier, a bad key name, and a non-existent message ID as complete failure. Do not substitute an HTTP status code; this invocation contract is not an HTTP API response.
Test input | Expected assertion |
{"batchItemFailures":[]} | Documented complete success |
{} with no batchItemFailures field | Documented complete success as an empty event response |
Valid list containing B’s real messageId | With mapping enabled, B is the reported failed item |
Malformed JSON | Documented complete failure |
itemIdentifier:"" | Documented complete failure |
{ "batchItemFailures": [ {} ] } with missing itemIdentifier | Lab question; verify rather than infer an undocumented exact result |
Unknown message ID | Documented complete failure |
Handler throws after processing records | Documented whole-batch failure |
Then read back the deployed mapping and prove ReportBatchItemFailures is active. A unit test that serializes the right JSON is necessary but insufficient: if the mapping setting is absent, the intended record-level acknowledgement contract is not active. Any response edge case not explicitly settled in the primary documentation should remain a lab question, not become a production assumption.
Make a repeated business operation safe or explicitly unresolved
SQS can redeliver; Lambda can retry; and a destination can commit an operation before the caller receives a response. Those are separate facts. The practical objective is therefore repeat-safe business execution, not a global exactly-once claim. AWS explicitly warns that SQS event-source mappings process events at least once and duplicates can occur.
Treat the business effect as its own state machine. A proposed ledger row can contain a domain operation key, payload fingerprint, state, current claim owner or lease, destination reference, confirmation timestamp, last SQS message ID, observed receive count, and reconciliation owner. The ledger must be durable enough to survive invocation failure and must support an atomic conditional claim; a cache lookup followed by an unprotected destination write leaves a race in which two workers can both observe “not done” and both perform the effect.
Ledger observation on delivery | Meaning | Handler behavior |
No row | Operation not yet claimed | Atomically claim, then process |
IN_PROGRESS owned by live worker | Concurrent work exists | Do not issue duplicate effect; defer according to policy |
EFFECT_CONFIRMED | Destination effect is known | Skip effect and treat business work as complete |
EFFECT_UNKNOWN | Commit status unresolved | Reconcile first; do not reissue |
FAILED_BEFORE_EFFECT | Prior attempt never crossed effect boundary | Retry may be safe after cause review |
Same key, different payload fingerprint | Conflicting intent | Hold and escalate; never silently overwrite |
This complements guidance on reliable external API integration, but the record consumer needs stronger replay evidence: the queue acknowledgement and the destination commit are not one transaction.
Choose the idempotency scope before the storage tool
Define the domain key first. For the fixture, entitlement:43:gamma:v3 means “apply this specific entitlement version to this account,” independent of SQS delivery identity. Define what constitutes a conflicting replay: if the same key arrives with a different entitlement payload, the correct action is a conflict, not “already processed.” Define who owns concurrent execution, how abandoned claims expire, and how the owner is recovered after a process dies.
Retention is an application requirement. Keep the effect evidence for at least the period in which the business operation can credibly be retried, redriven, manually replayed, or corrected. That window may exceed queue visibility or message retention. No universal number is asserted here; choose it from the application’s replay and audit model, then test expiry behavior explicitly.
Reconcile a committed effect with a lost response
Record C is the critical test. The fake destination commits the entitlement and then simulates a connection reset before the client receives confirmation. The first attempt must persist EFFECT_UNKNOWN. On redelivery, the consumer must not call “grant” again. It first queries an authoritative destination using the business key or recorded destination reference.
The decision table is intentionally conservative:
Reconciliation result | Safe next action |
Destination proves effect exists and matches payload | Mark EFFECT_CONFIRMED; acknowledge without reissuing |
Destination proves effect does not exist | Reissue only if the business contract permits it |
Destination cannot determine outcome | Keep EFFECT_UNKNOWN; hold/retry reconciliation or escalate |
Destination shows conflicting effect | Stop automated replay and investigate |
If the destination has no authoritative lookup and no reliable idempotency facility, that is a product constraint, not something Lambda’s batch response can solve. Preserve the uncertainty and route it to documented business escalation rather than relabeling “unknown” as “failed.”
Budget visibility and invocation time from measured work
Visibility must be designed from measured batch behavior, but two AWS statements must not be blurred together. AWS recommends setting the source queue visibility timeout to at least six times the function timeout. Separately, Lambda validates that the function timeout is less than or equal to the queue visibility timeout when the event-source mapping is created or updated. If a non-zero batch window is used, AWS recommends six times the function timeout plus MaximumBatchingWindowInSeconds. AWS, Creating and configuring an Amazon SQS event source mapping, living documentation; publication date not asserted; accessed September 17, 2026.
That distinction matters in reviews. “Configuration accepted” proves the enforced inequality, not that the queue has adequate retry headroom for throttling or long-tail work. Conversely, the six-times guidance is a recommendation, not a promise that every workload needs or succeeds with one universal setting.
Worksheet input | Evidence to collect | Decision use |
Function timeout | Deployed configuration | Enforced visibility comparison |
Batching window | Deployed mapping | Add to AWS-recommended visibility calculation when non-zero |
Batch size | Deployed mapping and observed records/invocation | Relates per-record work to invocation duration |
p50/p95/p99 batch duration | Controlled measurements | Finds long-tail execution risk |
Dependency timeout/retry budget | Client configuration | Prevents nested retries consuming whole invocation |
Throttling/capacity observations | Queue and Lambda telemetry | Tests whether retry headroom is adequate |
For illustration only, a synthetic 20s function timeout with a 2s batching window gives 6 × 20 + 2 = 122s under AWS’s documented recommendation. That is neither a measured result nor a production default. Replace the numbers with measured work and the deployed batch window.
Also model what happens when work approaches visibility expiry. A message can become available for another receive while an earlier worker still has uncertain progress, which is another reason an atomic business claim matters. Set explicit stop conditions for rollout: unexpected duplicate effects, rapidly increasing oldest-message age, timeouts approaching the configured function limit, or evidence that dependency retries consume the safety margin. The service owner who controls queue and function configuration must be identified before the test, not during an incident.
Measure batches by shape as well as duration. Four fast records do not validate four records that each fan out to a slow dependency. Record payload class, destination latency, client retry count, and time remaining in the Lambda context when the final record starts. The purpose is not to invent a throughput target; it is to know whether the configured batch can finish with margin under realistic slowness. If the measured tail no longer fits the operating envelope, reduce work per invocation, adjust timeouts under change control, or pause expansion.
Give FIFO a separate ordered-failure path
Do not reuse the standard-queue loop unchanged for a first-in, first-out (FIFO) queue. AWS’s partial-batch guidance says that with FIFO, the function should stop processing after the first failure and return all failed and unprocessed messages in batchItemFailures to help preserve ordering. That changes both code shape and test assertions.
Use a synthetic message group G-7 containing records 01, 02, and 03. Record 01 succeeds. Record 02 fails before its effect. Record 03 is valid but must not be processed after the failure in 02. The expected response reports 02 and 03: one failed, one unprocessed.
FIFO record | Processing outcome | Response membership |
G-7/01 | Effect confirmed before failure | Not reported failed |
G-7/02 | First failure | Report in batchItemFailures |
G-7/03 | Deliberately unprocessed | Also report in batchItemFailures |
The test must prove two things: 03 never crossed its business-effect boundary, and the response includes every remaining unprocessed record. A loop that keeps processing after 02 may be convenient for throughput, but it contradicts the conservative FIFO guidance used in this playbook.
Dead-letter handling also needs a FIFO-specific business decision. AWS warns against using a DLQ with FIFO when moving a failed message out would break the exact order of messages or operations. AWS, Using dead-letter queues in Amazon SQS, living documentation; publication date not asserted; accessed September 17, 2026. This warning does not mean every FIFO workload forbids DLQs. It means the application owner must state whether a gap in the domain sequence is tolerable before configuring one.
For a domain where sequence is mandatory, hold the group and repair the blocking record rather than letting later operations leapfrog it through a convenience replay route. For a domain where ordering is only local or advisory, document that narrower requirement. “FIFO queue” describes transport semantics; it does not, by itself, specify the business consequence of skipping an operation.
Test invocation-wide failure and backpressure
Partial batch reporting is not a shield against invocation failure. AWS documents that if the function throws an exception, the entire batch is a complete failure. It also documents different backoff behavior for function-code errors and throttling, while noting that when ReportBatchItemFailures is activated Lambda does not scale down message polling merely because function invocations fail. That makes queue-level evidence essential.
Run five separate synthetic experiments so one failure mode does not hide another:
Experiment | Injection | Evidence to preserve | Question to answer |
Handler exception | Throw outside per-record catch | Ledger states, invocation error, returning records | Did whole-batch failure override partial intent? |
Process termination | Exit after A, before return | Durable ledger only; no assumed response | Which effects may already exist? |
Timeout | Delay beyond configured timeout | Last durable state and queue reappearance | Which records were interrupted? |
Dependency outage | Fake destination rejects all calls | Failure classification and polling behavior | Are retries amplifying an outage? |
Capacity saturation | Intentionally constrained lab capacity | Queue age/depth and throttling evidence | Is backpressure visible where expected? |
Do not infer success from a lower Lambda error count. A handler can catch record failures and emit a syntactically successful invocation while the queue accumulates failed records. Conversely, an invocation error can coexist with a business effect that committed before the crash. Observe both the function and the queue, then join those signals to the effect ledger.
For A–D, the process-termination test is particularly valuable. If A reached EFFECT_CONFIRMED before D triggers termination, the invocation may fail as a whole and A may be delivered again. The correct second-attempt behavior is to recognize A’s business key and skip the already-confirmed operation. That is why the replay safety test is stronger than the transport response test.
During a dependency outage, avoid converting every retry into a fresh external write attempt. Records in EFFECT_UNKNOWN should perform reconciliation; known pre-effect failures can remain retryable; confirmed records should short-circuit. Stop the experiment when safety evidence degrades rather than “letting the queue prove it.” Capacity tests belong in disposable or tightly isolated environments with explicit quotas and blast-radius controls.
Recover dead-letter messages without blind replay
A dead-letter queue (DLQ) is evidence that normal processing exhausted the source queue’s redrive policy; it is not evidence that every business operation failed. SQS uses maxReceiveCount to decide when a repeatedly received message moves to the DLQ, and AWS recommends choosing it high enough to allow sufficient retries. AWS’s Lambda mapping guide separately recommends a value of at least five for this integration; that is guidance, not a universal production requirement.
Before any redrive, build an incident manifest. It should contain only the identifiers and state needed to make the replay decision, not a copy of every payload.
Manifest field | Required question |
Domain operation key | What business action does this record represent? |
SQS message ID / observed receive count | Which transport history was seen? |
Effect state | Confirmed, unknown, or failed before effect? |
Failure cause | Has the trigger for failure been mitigated? |
Retention runway | How long until evidence/message expiry matters? |
Destination readiness | Can the dependency safely accept work now? |
Reconciliation evidence | Can ambiguous outcomes be resolved? |
Approver / owner | Who accepts replay risk and watches stop conditions? |
Start with a bounded cohort whose effects are understood. Compare domain outcomes, not merely queue depth. A replay that empties the DLQ but creates duplicate entitlements is an operational failure even if every Lambda invocation succeeds.
Classify the cohort before moving anything. “Confirmed” records normally need no business re-execution; “failed before effect” records can be candidates once the original cause is fixed; “unknown” records go through reconciliation first; and conflicting-key records stay on hold. This classification prevents the DLQ from becoming a second scheduling system where transport position silently overrides business state. It also makes poison message isolation useful: a record can remain isolated while the healthy population proceeds, without granting blanket permission to replay the isolated operation.
Preserve the evidence before changing retention
SQS retention semantics differ between standard and FIFO DLQs. For a standard queue, AWS says message expiration remains based on the original enqueue timestamp when the message moves to the DLQ; ApproximateAgeOfOldestMessage in the DLQ reflects when it moved there, not its original age. For FIFO, the enqueue timestamp resets on the move, and the age metric reflects time in the DLQ. AWS recommends making a standard DLQ’s retention longer than the source queue’s retention.
Therefore, “the oldest DLQ message is three days old” is not a complete processing history for a standard queue. Preserve the manifest and ledger evidence before a retention change or incident cleanup. Export synthetic identifiers in this lab; in production, apply data-minimization rules and never place secrets or customer payloads in public incident notes.
Approve redrive in small accountable cohorts
Treat redrive as a controlled change with explicit gates. Native SQS redrive is a queue recovery mechanism, not a record-level business-approval system. Where an exact reviewed subset is required, a separately controlled replay path may be easier to audit than assuming the native operation can express arbitrary per-record selection. Any such replay tool must preserve or reconstruct the domain key because a newly enqueued message can have a new transport identity.
Gate | Proposed criterion |
Enter | Root cause mitigated; destination healthy; each record classified; approver named |
Continue | No new duplicate effect; unknown-effect count does not grow; queue telemetry remains explainable |
Stop | Any conflicting effect, new schema mismatch, unexplained identity loss, or unexpected queue-age growth |
Complete | Cohort drained; every business outcome confirmed or assigned for reconciliation |
Do not automate “DLQ not empty, therefore redrive all.” For each cohort, record start/end time, approved domain keys, code revision, mapping configuration, effect-ledger snapshot, and final disposition. Redrive ends when the business outcomes are settled, not merely when the source counter returns to zero.
Measure record outcomes as well as invocations
Invocation metrics tell you whether Lambda ran; they do not tell you whether a record’s business effect is confirmed. Add application counters whose names are explicitly yours, not AWS metric names. The denominator matters as much as the count: an “unknown effects” total without the number of started records is hard to interpret during a rollout.
Proposed application metric | Suggested denominator / use |
records_received_total | Base count of delivered records |
records_retryable_failure_total | Divide by records evaluated for a retry decision |
effects_confirmed_total | Compare with operations started |
effects_unknown_total | Divide by operations that crossed or may have crossed the effect boundary |
records_replayed_total | Base for replay outcome review |
outcomes_reconciled_total | Compare with records previously unknown |
Keep these separate from AWS service metrics. AWS specifically points to SQS NumberOfMessagesDeleted and ApproximateAgeOfOldestMessage as useful signals when checking partial batch failure reporting. A sharp queue-age increase can be a reason to investigate, but no universal alert threshold is asserted here; baseline it against the workload and the rollout stage.
A compact dashboard should show four layers side by side: queue backlog/age; Lambda invocations, errors and duration; record outcome rates from the application; and effect-ledger state counts. The dashboard contract should say which time window and denominator each panel uses. That prevents an incident review from comparing a five-minute function error rate with a one-hour count of effect states and drawing a false causal conclusion.
The same principle extends broader API security and observability: useful correlation should not require logging secrets. For a privacy-safe example, derive an opaque correlation token from the domain operation key with a server-side keyed hash, then log the token, SQS message ID, Lambda request ID, ledger transition, and outcome class. Keep raw customer data out of routine telemetry.
Finally, alert on unknown state, not only exceptions. A rising effects_unknown_total with low Lambda errors is precisely the condition a conventional invocation dashboard can miss. The owner of that metric needs the authority and destination access to reconcile it; otherwise observability stops at detection.
Decide whether to proceed, hold or reconcile
Turn the lab into an acceptance decision. Every test should name its evidence, owner, and action. A passing JSON unit test is a prerequisite for partial reporting; it is not proof that business replay is safe.
Test | Required evidence | Owner | Decision |
Response-shape tests | Documented success/failure cases pass | Function owner | Proceed to mapping test |
Mapping activation | Deployed read-back shows ReportBatchItemFailures | Platform owner | Proceed only when verified |
Known committed effect replay | Second delivery performs zero duplicate writes | Application owner | Go if confirmed |
Lost-response case | Authoritative lookup settles C before reissue | Integration owner | Reconcile until settled |
Invocation interruption | Ledger and queue explain every returning record | Reliability owner | Hold on unexplained state |
FIFO first-failure case | Unprocessed tail included and not executed | FIFO domain owner | Hold if not verified |
DLQ cohort rehearsal | Domain outcomes match approved manifest | Incident approver | Go, hold, or reconcile |
The hard holds are deliberate. Stop if a message cannot be tied to a domain operation key, if an external effect is unknown and no reconciliation path exists, if a payload conflicts with a previously claimed key, or if FIFO order behavior has not been demonstrated for the workload’s actual grouping rules. Those conditions can turn a mechanically valid retry into a duplicate or out-of-order business action.
“Proceed” also has a bounded meaning. It means the tested class of work can run under the recorded configuration and evidence model. It does not certify exactly-once execution, prove all dependency failures are covered, or make future payload versions safe by inheritance. Re-run the relevant test when the business key, destination contract, batch settings, runtime, or mapping behavior changes materially.
Roll out the response contract as a reversible change
The dangerous rollout is to deploy a handler that catches record errors and returns batchItemFailures before verifying that the event-source mapping honors partial responses. Under the default batch contract, swallowing an exception can convert a record failure into an apparently successful invocation and allow deletion of work you intended to retry. The code and mapping therefore need a compatibility sequence.
Use a dual-mode application flag in the proposal. In legacy mode, collect per-record failures for telemetry but throw at the end if any record failed, preserving whole-batch failure semantics. Enable ReportBatchItemFailures on the mapping, read the mapping back, and only then switch the handler into partial mode. On rollback, switch the handler back to whole-batch failure first; a function-level exception is documented to fail the whole batch even while the mapping still supports partial reporting.
Change step | Gate before next step | Reversal |
Inventory deployed state | UUID, settings, code revision captured | None; observation only |
Deploy dual-mode code with partial mode off | Legacy failure test still returns whole-batch failure | Restore prior code |
Enable ReportBatchItemFailures | Mapping read-back proves setting | Remove setting if code still legacy-safe |
Enable partial mode for controlled exposure | Record/effect metrics explain test cohort | Turn partial mode off first |
Expand exposure | No unknown-effect growth or duplicate business effects | Reduce exposure / legacy mode |
Rehearse recovery | DLQ and rollback evidence complete | Hold rollout |
A proposed 30-day rollout can keep the work paced without pretending AWS requires this schedule. Days 1–5: inventory configuration, domain keys and effect boundaries. Days 6–10: run the fake-destination and response-shape tests. Days 11–15: validate the disposable AWS mapping, including malformed response and invocation-wide failures. Days 16–20: expose a small, low-risk production cohort under change control. Days 21–25: rehearse reconciliation and bounded DLQ recovery. Days 26–30: review evidence and exercise rollback.
During exposure, retain the effect ledger even if partial reporting appears healthy. Rollback changes retry granularity: returning to whole-batch failure can make already successful records reappear, so removing the ledger at the same time would discard precisely the duplicate-protection evidence rollback needs. Preserve the affected record manifest across code and configuration changes.
Treat configuration drift as a rollback trigger, too. If the mapping read-back no longer matches the reviewed batch size, batching window, response type, source ARN, or enabled state, stop interpreting new observations through the old test evidence. Likewise, a runtime or dependency-client change can alter timeout and error behavior even when the business code is unchanged. Re-establish the minimum fixture before expanding exposure. Recoverability depends on knowing which contract was active for each cohort, not merely knowing that a deployment pipeline reported success.
This is an application of broader cloud development practices, but with a narrower reliability rule: configuration and code must move in an order that never assumes an acknowledgement contract that has not been verified. The change ticket should include stop criteria, rollback owner, and the read-back evidence, not just a deployment success message.
Build the cloud-development foundations behind the runbook
This playbook assumes skills that sit below the Lambda–SQS specialization: cloud architecture, deployment, infrastructure configuration, security, containers, monitoring, and operational diagnosis. The Refonte Learning Cloud Development Program page lists a three-month program at 12–15 hours per week and names cloud architecture, Docker, Kubernetes, infrastructure as code, cloud security, monitoring and optimization, plus building and deploying cloud applications. Those are useful foundations for understanding why this recovery design spans code, platform configuration and evidence.
Use a two-item readiness check before applying the runbook:
Readiness question | Minimum answer |
Can you trace one record across queue delivery, handler execution and the external effect? | You can produce the identities and ledger transitions without relying on memory. |
Can you explain why a retry is safe? | You can show confirmed, absent or reconciled effect evidence rather than pointing only to a Lambda status. |
Dedicated Lambda–SQS partial-failure labs are not claimed here as program curriculum. The CTA is for the verified general cloud-development foundations, not a promise of this exact exercise or toolchain.
Keep the replay decision with the evidence
The durable result of a partial-batch design is not a promise that duplicates disappear. It is a record-level explanation that survives retry, interruption and dead-letter recovery.
Question | Answer |
Does partial reporting remove duplicates? | No. It can reduce unnecessary retries of records reported successful, while Lambda–SQS at-least-once delivery still permits duplicate processing. |
Does a successful invocation prove every effect? | No. The batch response and an external commit are not one atomic transaction. |
What blocks replay? | Lost business identity, unknown side effects without reconciliation, unmitigated failure causes, or unverified FIFO ordering behavior. |
The decision is therefore documented as go, hold, or reconcile. Replay only the work whose evidence supports it; preserve uncertainty until it can be resolved.
