Build1 publisherNot yet confirmed elsewhere3 min readPublished
Partial batch failures: the handler is fine, the mapping decides
A correct-looking batchItemFailures handler is inert unless the event source mapping agrees. One dev.to write-up grades the code-versus-mapping mismatch higher than the missing setting, and says why.
The Engineer · Build desk
What happened
- A commenter, Mads Hansen, told the author that the trigger shape is only the first contract and that delivery and retry semantics are the second.
- A handler that builds a batchItemFailures array is ignored unless FunctionResponseTypes on the event source mapping contains ReportBatchItemFailures.
- Both analyzers fire only on an explicit false value for the setting.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost Nine tenths of every replay is work the system already paid for once, plus its side effects, repeated until the redrive limit intervenes.
- exposure Consumers without idempotency put the bill in front of customers rather than in a dashboard, and a passing test suite gives no warning.
- constraint No amount of local tooling can validate the half of the contract that decides runtime behaviour, so review of the handler alone cannot clear it.
- precedent Assistants that produce the visible half and call it finished bias new code toward the high-severity direction of the mismatch rather than the medium one.
The grading is the part worth stealing. A mapping with a batch size above 1 and no ReportBatchItemFailures is scored medium in the author's scanner, because the handler may genuinely not need per-record reporting, and a batch of one is skipped outright since it has nothing to partially fail [9]. The mismatch, where the code builds a batchItemFailures array and the mapping has the setting off, is scored high [10]. The analyzer description is blunt about the reason: "The code reads as correct and its unit tests pass; only the mapping tells the truth" [11]. The distinction being drawn is between an absence and an active false belief [15]. Somebody wrote that array on purpose, and whatever they concluded about idempotency downstream was concluded on the assumption that the nine good records would be deleted.
That assumption fails quietly. One throwing record sends the whole batch back, the nine that already ran to completion included, and it keeps replaying until the queue's retry limit [4]. Nine of ten records in each replay are re-executions, which puts 90 percent of the retried work in the duplicate column [17]. If the handler is not idempotent, the author's phrasing is fair: double-charged customers rather than a log line [5]. A unit test that feeds ten records and asserts one entry in the failure array passes the entire time [6].
Neither side of the check is expensive, which is the uncomfortable bit. Both scanners match a name: ts-morph on a property or shorthand property called batchItemFailures, the Python scanner on the same dict key, with no control-flow tracing to a return statement, on the reasoning that an array by that name has no other purpose in a Lambda handler [12]. The infrastructure side is close to free, because FunctionResponseTypes already arrives on the ListEventSourceMappings response that trigger extraction pages through anyway [13]. So this class of defect does not survive because it is hard to find. It survives because the two halves are never in the same place: a Terraform module in another repo, a CDK stack another team owns, or a checkbox someone clicked in a console eighteen months ago, none of which your editor or test suite can see [7].
One design choice deserves reading closely. Both checks fire only on an explicit false [14]. The write-up's explanation of that rule breaks off mid-comment in the material supplied, so how an unreadable or absent mapping is treated is not stated. Either way, a finding means the two halves disagree, and silence cannot be read as agreement.
Then the assistant path, which is how the high-severity direction gets manufactured at volume. Asked to add partial batch failure handling to a consumer, an assistant writes exactly the correct-looking handler and reports the job done, not hallucinating, just writing the only half it can see [8]. The cheap half to generate is the half that is graded high when it is wrong. On that basis the mismatch population should grow faster than the plain-absence population, and it will grow in repositories where the mapping is owned by someone who was never in the pull request.
What to watch
- Whether the explicit-false rule is extended to mappings the scanner cannot read or resolve; the write-up's reasoning on that point is cut off in the supplied text.
- Whether IaC modules start defaulting FunctionResponseTypes to ReportBatchItemFailures whenever batch size is above 1, which would kill the medium-graded finding at source.
- Whether coding assistants asked for partial batch handling begin touching the event source mapping as well as the handler.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence54
- Adoption12
- Hype gap+8
- Incentives74
- Confidence56
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
In a comment on the author's earlier post about an AI assistant missing an SQS trigger on a Lambda, Mads Hansen said the trigger shape is the first contract and delivery and retry semantics are the second.
- [2]
The example handler loops over event.Records, catches per record rather than around the loop, pushes record.messageId as itemIdentifier, and returns { batchItemFailures }.
- [3]
Whether Lambda honours the returned batchItemFailures array is decided by the event source mapping: if FunctionResponseTypes on the mapping does not contain ReportBatchItemFailures, Lambda discards the array and marks the whole batch failed.
- [4]
With the setting off, all ten records are redelivered including the nine that already ran to completion, and one poison message keeps replaying its batch until the queue's retry limit; the failure mode is duplicate processing.
- [5]
If the handler is not idempotent, the result is double-charged customers, not a log line.
- [6]
A unit test that feeds the handler ten records and asserts one entry in batchItemFailures passes.
- [7]
The half of the contract that makes the handler work lives in a Terraform module in a different repository, a CDK stack a different team owns, or a checkbox someone clicked in the console eighteen months ago, and nothing in the editor, linter or test suite has visibility into it.
- [8]
An AI assistant asked to add partial batch failure handling to the consumer writes exactly that handler and reports the job done; it is not hallucinating, it wrote the only half it can see.
- [9]
MissingPartialBatchResponseAnalyzer looks for an sqs, kinesis or dynamodb trigger with a batch size above 1 and no ReportBatchItemFailures; a batch of one is skipped because it has nothing to partially fail; the finding is graded medium because the handler may genuinely not need it.
- [10]
BatchResponseMismatchAnalyzer fires when the code builds a batchItemFailures array and the mapping for that trigger has the setting off, and is graded high.
- [11]
The mismatch analyzer's description reads: "The code reads as correct and its unit tests pass; only the mapping tells the truth."
- [12]
Code-side detection is name matching only: ts-morph looks for a property or shorthand property called batchItemFailures and the Python scanner matches the same dict key, with no control-flow tracing to a return statement, because building an array by that name has no other purpose in a Lambda handler.
- [13]
FunctionResponseTypes rides on the ListEventSourceMappings response that trigger extraction already pages through, so the whole check is one field carried forward plus two analyzers.
- [14]
Both checks fire only on an explicit false; the write-up's explanation of that rule is cut off mid-comment in the supplied text.
- [15]
The author frames a missing setting as an absence and a code/mapping mismatch as an active false belief, since the handler was written on purpose in the belief that per-record reporting was on, and downstream idempotency decisions rest on that belief.
- [16]
The author built the check into a tool called Infrawise, at which point the mismatch became visible as having two directions of unequal severity.
- [17]
In the ten-record example, nine of the ten records in each replay are re-executions of work that already completed, so 90 percent of the redelivered work is duplicate.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toYour retry logic is correct and does nothing
1 article · August 24, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
Entities
- InfrawiseFollow
- AWS LambdaFollow
- Amazon SQSFollow
- Amazon KinesisFollow
- Amazon DynamoDB (streams trigger)Follow
- MissingPartialBatchResponseAnalyzerFollow
- BatchResponseMismatchAnalyzerFollow
- ts-morphFollow
- TerraformFollow
- AWS Cloud Development KitFollow
- Mads HansenFollow
- Amazon Web ServicesFollow