DEV Community

Cover image for Day 8/30 AWS System Design Patterns
Joud Awad
Joud Awad

Posted on

Day 8/30 AWS System Design Patterns

One malformed record. 61,000 duplicate loyalty credits.

A retail platform streams purchase events into a Kinesis Data Stream (streaming log — consumers track their own position via a checkpoint; the stream never deletes on read). A Lambda function (serverless compute) consumes the stream through an event source mapping (polls the shard and invokes Lambda with batches — advances the checkpoint only when the whole batch succeeds) with a batch size of 100. For each record, the function calls the loyalty service and credits points to the customer's account.

The pipeline has run cleanly for a year at 2 million events per day.

On Tuesday at 9:40 AM, a producer deployment ships a bug: one purchase event is published with a null user_id. It lands at position 73 of a batch. The function processes records 1 through 72 — credits 72 customers — then throws on record 73. The invocation fails.

The event source mapping retries. Not record 73 — the batch. Records 1 through 72 are credited again. Record 73 throws again. Retry. Again. The mapping's retry setting is the default: keep retrying until the record ages out of the stream's 24-hour retention.

Support notices at 1:50 PM: four hours, roughly 850 retry cycles, 61,000 duplicate credits, and every purchase event behind the bad record on that shard is stuck waiting. No Lambda alarm fired — the function's errors look like a routine blip at first glance, because the error count is one per retry, not 61,000.

Why did already-processed records run again, and what is the correct fix?

A) Kinesis (streaming log) delivered the same records multiple times during the incident — but a stream is not a delivery service that can duplicate; the consumer reads from a position it controls, and the stream served exactly what was asked for

B) The checkpoint advances per batch, not per record — a thrown error rewinds the consumer to the last committed checkpoint, so every record after it, including the 72 that succeeded, re-executes on every retry until the batch finally succeeds or the poison record expires

C) Two concurrent Lambda invocations processed the same shard and raced — but the event source mapping invokes one batch at a time per shard precisely to preserve ordering; there is no concurrent second reader on the shard

D) The loyalty service retried the credits internally — but it received separate, fully-formed requests carrying no shared idempotency key; from its side these were 850 distinct instructions to credit points, and it executed them correctly

Answer in the comments.

Top comments (5)

Collapse
 
thejoud1997 profile image
Joud Awad

The answer is B.

A Kinesis consumer's progress is a single number: the sequence checkpoint. The event source mapping (polls the shard, invokes Lambda with batches, commits the checkpoint on success) reads a batch starting at the checkpoint, invokes the function, and only if the invocation returns successfully does it advance the checkpoint past that batch. Success is all-or-nothing at batch granularity.

So when record 73 throws, the mapping does not know that 72 records succeeded — it only knows the batch failed. It rewinds to the checkpoint and reads the same 100 records again. The 72 loyalty credits were side effects fired into the world; the checkpoint has no memory of them. And because the default retry policy for stream sources is to retry until the data expires from retention, a single poison record turns into a four-hour duplication engine — while also blocking the shard, since ordering guarantees forbid skipping ahead.

This is not an AWS quirk. It is the fundamental contract of checkpoint-based consumption: your commit granularity defines your duplicate window. A Kafka consumer committing offsets per batch has the identical failure mode.

Two fixes, both needed:

Fix 1 — Shrink the duplicate window: enable partial batch responses (ReportBatchItemFailures). The function reports the sequence number where processing failed; the mapping commits the checkpoint past the 72 successes and retries only from record 73. Pair it with a bounded retry policy — set maximumRetryAttempts, enable bisectBatchOnFunctionError so the mapping halves the batch to isolate the poison record, and configure an on-failure destination (SQS or SNS target that receives the failed record's shard and sequence metadata) so the bad record is skipped, captured, and the shard unblocks.

Fix 2 — Make the side effect safe to repeat: send an idempotency key with every credit call — event_id is sitting right there in the record. Even with perfect checkpointing, at-least-once processing is the contract of every stream consumer. Fix 1 makes duplicates rare; Fix 2 makes them harmless. Only Fix 2 protects the customer's balance.

Day 3 of this series was the push side of at-least-once: a platform retry re-running a non-idempotent function. This is the pull side: a checkpoint rewind re-running non-idempotent work. Different mechanism, same law — anything that fires a side effect must survive being executed twice.

Collapse
 
thejoud1997 profile image
Joud Awad

A — A Kinesis stream is storage with a cursor, not a delivery pipeline. It cannot "send duplicates" because it does not send — the consumer reads from a position the consumer owns. Every repeated record in this incident was re-requested by the consumer after its own checkpoint rewound.

Collapse
 
thejoud1997 profile image
Joud Awad

C — The event source mapping serializes processing per shard to preserve ordering: one batch in flight at a time. (Even with a parallelization factor, records with the same partition key stay in order on one processor.) The re-processing here is sequential retry, not a race.

Collapse
 
thejoud1997 profile image
Joud Awad

D — The loyalty service behaved correctly. It received hundreds of independent, valid requests with no idempotency key linking them. Deduplicating them without a key would require it to guess — the platform sent the duplicates, and the platform owns the fix.

Collapse
 
thejoud1997 profile image
Joud Awad

Also, it would mean a lot to me if you could support my content and stay in touch 🙏

YouTube: youtube.com/@system-design-lab
LinkedIn: linkedin.com/in/joud-awad/
Medium Blog: joudwawad.medium.com/
Substack: joudawad.substack.com/