Try it → Submit an order and it runs the real pipeline. Orders are deleted automatically after 24 hours.
The pipeline
Ingest (top): browser → API Gateway → CreateOrder → DynamoDB as PENDING. DynamoDB Streams → ProcessOrder (PROCESSING) → SNS fan-out, raw delivery, one queue per consumer. Left: worker queue → OrderWorker (COMPLETED or FAILED); three failed receives → dead-letter queue → CloudWatch alarm → email. Right: Pipe queue → EventBridge Pipe, amount over 10,000 → high-value-orders queue.
Three things to try on the demo: a normal order completes; an amount over 10,000 is rejected as a business failure and routed as high-value; and the word FAIL in the notes triggers a technical failure that retries three times and lands in the dead-letter queue.
The architecture in the course does not work
The exercise this came from puts the EventBridge Pipe’s source and the worker Lambda’s event-source mapping on the same SQS queue. Both are pollers. A standard queue delivers each message to exactly one consumer — and a Pipe deletes the messages its filter rejects rather than returning them.
So the Pipe doesn’t occasionally win a race. It eats normal orders and drops them on the floor.
First run, three test orders:
| Test | Amount | Expected | Actual |
|---|---|---|---|
| Normal | 1,200 | COMPLETED | stuck PROCESSING — worker never saw it |
| High-value | 20,000 | FAILED + routed | routed, but DynamoDB never reached FAILED |
| Forced failure | 1,500 | retry ×3 → DLQ | stuck PROCESSING, DLQ empty, alarm never fired |
All three queues read zero. The messages were gone, and nothing reported an error.
Isolating it
I stopped the Pipe and changed nothing else, then resubmitted the same orders:
normal order -> COMPLETED completedAt 01:59:08.879Z
forced failure -> PROCESSING, retried 3x, DLQ=1 at t≈70s
alarm OK -> ALARM at t≈125s, email delivered
One variable changed, both failures resolved. That’s the difference between a diagnosis and a guess.
The fix
The Pipe and the worker are two independent consumers of the same event. They shouldn’t compete for one queue — they should each get their own copy. ProcessOrderFunction now publishes to SNS, and both queues subscribe.
Both subscriptions use RawMessageDelivery=true. Without it, SNS wraps the payload in an envelope and both the worker’s JSON.parse(record.body) and the Pipe’s $.body.amount filter break.
Afterwards, all three cases behave correctly in the same run — the high-value order is marked FAILED by the worker and routed by the Pipe, from one submission. Under the original topology only one of those could ever happen.
What the DLQ is for
The dead-letter queue is the cheapest failure signal in an event-driven system. One alarm on ApproximateNumberOfMessagesVisible >= 1 covers every failure mode downstream of the queue.
It also forces a distinction that’s easy to get backwards:
| Business failure | Technical failure | |
|---|---|---|
| Example | amount over the limit | DynamoDB timeout, malformed JSON |
| Handling | consume the message, mark FAILED | throw — let SQS retry |
| Retried? | No — retrying can’t change the answer | Yes, 3× |
| Reaches the DLQ? | No | Yes |
Get it backwards and you either retry forever or lose errors silently.
Running it in public
A public, unauthenticated endpoint that writes to DynamoDB is a cost-amplification target, so it’s guarded:
- API Gateway throttling at 5 req/s, burst 10. Verified: 60 concurrent requests produced 15 successes and 45 rejections.
- DynamoDB TTL — every order self-deletes after 24 hours, so the table can’t grow without bound. The attribute is epoch seconds; milliseconds would mean the year 56000 and nothing would ever be removed.
- A budget alarm scoped by tag, because throttling caps rate but not total — 5 req/s sustained is still ~13M requests a month.
It costs about $0.10/month because everything in it is pay-per-request and idle genuinely costs nothing.
Other defects in the source material
- Every template literal in the published Lambda code lost its backticks in export — none of it compiles as printed.
- CommonJS in a file the console creates as an ES module.
- The Pipe’s transformer omits a field the consumer logs, so it prints
Routed At: undefined. - Three
*FullAccessIAM policies, replaced here with one scoped to the single table and the four actions actually used.
Links
- Live demo
- Source and full write-up
- aws-labs — all nine projects