· via dev.to (home feed)
Stripe webhook signature checks break delayed queue replays; a re-signing proxy fixes it
A dev.to post explains why Stripe webhook verification rejects any event held longer than five minutes, why the obvious workarounds are insecure, and how a verify-and-re-sign proxy solves it.

The failure mode
Anyone building a resilient pipeline for Stripe billing webhooks eventually hits the same wall, as a recent post on dev.to lays out. Your handler crashes while processing a customer.subscription.updated event, the payload lands in a dead-letter queue, you fix the underlying bug, trigger a replay — and Stripe's SDK immediately throws an error saying the event's timestamp falls outside the allowed tolerance zone.
The reason, according to the post, lies in how Stripe signs deliveries. Every webhook carries a Stripe-Signature header containing a Unix timestamp and an HMAC-SHA256 digest computed over the string combining that timestamp with the raw, unparsed JSON body, using the endpoint's signing secret. When your code calls stripe.webhooks.constructEvent(), the SDK performs two independent checks: the digest must match the secret, and the timestamp must be within 300 seconds of the current time. That second check exists purely as replay protection, and it cannot tell a malicious replay apart from a legitimate redelivery after an outage. Once an event has sat in a queue for more than five minutes, verification fails every single time.
Workarounds that weaken security
The post walks through the two fixes teams typically reach for and explains why both are dangerous.
Skipping verification for retried events — for instance, marking replayed requests with a custom header and bypassing constructEvent() when it is present — opens the endpoint to anyone who discovers the URL. A forged subscription event carrying that header would sail through without ever touching Stripe.
Widening the SDK's tolerance window — for example, passing a value of three days — does let queued events pass, but it effectively disables replay protection across the entire billing surface, allowing stale or intercepted requests to be accepted as valid.
Verify at the edge, re-sign inside
The architecture the author proposes instead decouples ingress verification from internal delivery. A small proxy sits in front of the application and does four things:
- It receives each webhook from Stripe and verifies the original signature within the 300-second window, rejecting forged requests up front.
- It acknowledges Stripe with an immediate 200 response, which stops Stripe's own retry cycle from timing out.
- It persists the untouched raw payload and metadata to durable local storage — the post names SQLite in WAL mode as an example.
- When forwarding or replaying, whether milliseconds or days later, it computes a fresh timestamp and a fresh HMAC using the same secret before handing the event downstream.
Because the re-signed request is indistinguishable from a fresh delivery, the application handler stays completely stock: the same constructEvent() call succeeds both on initial delivery and on a replay days later, with zero code changes in the app.
Handling out-of-order events
One remaining hazard is stale state overwriting newer state — a two-day-old subscription deletion replayed after today's fresh subscription creation, for example. The post recommends attaching provenance metadata to every re-signed delivery: a replay flag, the original Stripe timestamp, the delivery ID, and the original signature header. Downstream code can then compare the original timestamp against a record's last-updated time before applying any changes.
An open-source implementation
The author has packaged the pattern as HookArmor, an MIT-licensed reverse proxy distributed as a single binary or Docker image. Per the post, it buffers events to local SQLite, retries with exponential backoff, includes an embedded dead-letter queue with a web interface for one-click manual replays, and adds the provenance headers automatically, with no external database required.
Why it matters
Buffering failed webhook deliveries for later retry is standard practice — it is the entire point of a dead-letter queue — yet Stripe's five-minute signature window makes naive replays impossible, and the tempting shortcuts either disable verification entirely or gut replay protection. Decoupling the question of whether Stripe really sent an event from the question of whether your handler can process it right now preserves both security properties at once. The pattern also generalises beyond Stripe to any webhook scheme built on an HMAC plus a timestamp, and it keeps application code free of queue-specific special cases.
- #stripe
- #webhooks
- #security
- #dead-letter-queue
- #api