{"slug": "your-retry-logic-is-correct-and-does-nothing", "title": "Your retry logic is correct and does nothing", "summary": "Developer Siddharth (Sidd27) built Infrawise, an open-source tool that detects a subtle AWS Lambda bug where batch consumers correctly build a batchItemFailures array but the event source mapping lacks ReportBatchItemFailures, causing all records to be redelivered and duplicate processing. The tool includes two analyzers: MissingPartialBatchResponseAnalyzer for missing settings and BatchResponseMismatchAnalyzer for mismatches, graded medium and high respectively.", "body_md": "A comment on one of my posts sat open as an issue for two weeks before I understood how bad the thing it described actually was.\n\nThe post was about an AI assistant missing the SQS trigger on a Lambda. Mads Hansen replied with a sentence I have not been able to unthink since: the trigger shape is the first contract, delivery and retry semantics are the second. Getting `event.Records[0].body`\n\nright tells you how to read a message. It tells you nothing about what happens when one of ten messages in a batch throws.\n\nI filed it as [issue #87](https://github.com/Sidd27/infrawise/issues/87). The first item on that list is a bug no code reviewer can catch by reading the code, because the code is fine.\n\nHere is a batch consumer. It is the shape you get if you read the AWS docs and follow them.\n\n``` js\nexport const processOrders = async (event: { Records: { messageId: string }[] }) => {\n  const batchItemFailures: { itemIdentifier: string }[] = [];\n  for (const record of event.Records) {\n    try {\n      await handleOrder(record);\n    } catch {\n      batchItemFailures.push({ itemIdentifier: record.messageId });\n    }\n  }\n  return { batchItemFailures };\n};\n```\n\nNine records succeed, one throws, and the handler reports exactly the one that failed. That is the entire point of a partial batch response: the nine that worked are deleted from the queue, the one that failed comes back on its own.\n\nExcept this handler does not do that, because whether anyone listens to that return value is not decided in this file. It is decided by the event source mapping. If `FunctionResponseTypes`\n\non the mapping does not contain `ReportBatchItemFailures`\n\n, Lambda discards the array. The whole batch is marked failed. All ten records are redelivered, including the nine that already ran to completion.\n\nThe failure mode is duplicate processing. Nine orders get handled twice, or twenty times, because one poison message keeps replaying its batch until the queue's retry limit. If your handler is not idempotent, that is double-charged customers, not a log line.\n\nRead that handler again with a critical eye. There is nothing wrong with it. It builds the array correctly, it uses `messageId`\n\nas the identifier, it catches per record rather than around the loop. A unit test that feeds it ten records and asserts one entry in `batchItemFailures`\n\npasses.\n\nThat is what makes this different from an ordinary bug. The code is not wrong. It is inert. The half of the contract that would make it work lives in a Terraform module in a different repository, or in a CDK stack a different team owns, or in a checkbox someone clicked in the console eighteen months ago. Nothing in your editor, your linter, or your test suite has any visibility into it.\n\nAnd an AI assistant, asked to \"add partial batch failure handling to this consumer,\" writes exactly the handler above and reports the job done. It is not hallucinating. It wrote the only half it can see.\n\nWhen I built the check into [Infrawise](https://github.com/Sidd27/infrawise), it became clear the mismatch has two directions and they are not equally bad.\n\nThe first is the mapping-side gap. `MissingPartialBatchResponseAnalyzer`\n\nlooks for an `sqs`\n\n, `kinesis`\n\n, or `dynamodb`\n\ntrigger with a batch size above 1 and no `ReportBatchItemFailures`\n\n. A batch of one has nothing to partially fail, so it is skipped. This one is graded **medium**: it is a missing capability, but the handler may genuinely not need it.\n\nThe second is the mismatch. `BatchResponseMismatchAnalyzer`\n\nfires when the code builds a `batchItemFailures`\n\narray and the mapping for that trigger has the setting off. That is graded **high**, and the description says why:\n\nThe code reads as correct and its unit tests pass; only the mapping tells the truth.\n\nA missing setting is an absence. A mismatch is an active false belief — someone wrote the handler on purpose, believing per-record reporting was on. Every downstream decision they made about idempotency rests on that belief.\n\nDetecting the code side is cheap. Both scanners just match the name: ts-morph looks for a property or shorthand property called `batchItemFailures`\n\n, and the Python scanner matches the same dict key. There is no control-flow tracing to a return statement, because building an array by that name has no other purpose in a Lambda handler.\n\nThe infrastructure side costs nothing either. `FunctionResponseTypes`\n\nrides on the `ListEventSourceMappings`\n\nresponse that trigger extraction already pages through, so the whole check is a field carried forward plus two analyzers.\n\nOne detail mattered more than the analyzers themselves. Both checks fire only on an explicit `false`\n\n:\n\n```\n// Only an explicit false is evidence. undefined means the mapping was\n// never read, and this finding would be an accusation without evidence.\nif (trigger.reportsBatchItemFailures !== false) continue;\n```\n\nIf a `lambda:ListEventSourceMappings`\n\ncall was denied by IAM, or the page failed halfway, the field is `undefined`\n\n, not `false`\n\n. Treating that as \"the setting is missing\" would produce a high-severity finding about a mapping nothing was ever read from.\n\nThat failure would be worse than staying silent. A tool that occasionally accuses you of a bug you do not have gets muted, and then it is not there on the day it is right. So the extractor raises a partial-extraction error carrying whatever it did page through: the data survives, the source is recorded as `partial`\n\n, and the tools report it as unread rather than reporting a clean bill of health.\n\nTwo things, both small.\n\nBefore writing a batch consumer, `analyze_function`\n\nnow returns the trigger with `batchSize`\n\nand `reportsBatchItemFailures`\n\nalongside the event shape, and `get_lambda_overview`\n\ncarries the same fields for every function. So the assistant knows, before it writes a line, whether the partial-batch response it is about to build will be read or discarded. If it is going to be discarded, the honest answer is either \"set `FunctionResponseTypes`\n\nfirst\" or \"make this handler idempotent,\" and now that answer is available at coding time.\n\nAnd because the mismatch is graded high, `infrawise check`\n\nfails the build on it at the default `--fail-on high`\n\n. The mapping-side gap at medium does not — it is a suggestion, not a broken promise. A handler that reports failures nobody reads is a broken promise.\n\n`batchItemFailures`\n\n; the event source mapping must set `FunctionResponseTypes: [\"ReportBatchItemFailures\"]`\n\n. One without the other is not half a feature, it is zero.`undefined`\n\nand `false`\n\nare different answers. A tool that conflates them will eventually be wrong loudly, and after that nobody reads it.Infrawise is open source and exposes this through MCP, so your assistant reads the mapping's real configuration instead of assuming. [GitHub](https://github.com/Sidd27/infrawise) · [npm](https://www.npmjs.com/package/infrawise)", "url": "https://wpnews.pro/news/your-retry-logic-is-correct-and-does-nothing", "canonical_source": "https://dev.to/siddharth_pandey_27/your-retry-logic-is-correct-and-does-nothing-9oc", "published_at": "2026-08-24 18:12:14+00:00", "updated_at": "2026-08-24 18:43:27.393526+00:00", "lang": "en", "topics": ["developer-tools", "ai-products"], "entities": ["Infrawise", "AWS Lambda", "SQS", "Mads Hansen", "Sidd27"], "alternates": {"html": "https://wpnews.pro/news/your-retry-logic-is-correct-and-does-nothing", "markdown": "https://wpnews.pro/news/your-retry-logic-is-correct-and-does-nothing.md", "text": "https://wpnews.pro/news/your-retry-logic-is-correct-and-does-nothing.txt", "jsonld": "https://wpnews.pro/news/your-retry-logic-is-correct-and-does-nothing.jsonld"}}