An event consumer should assume every message can arrive twice, late, or out of order, and that an attacker can resend a captured one. Verify the signature, reject stale timestamps, record the event IDs you processed, and never depend on arrival order. Return a success status quickly and do the work from a queue.
Three different problems
Replay is an attacker re-sending a valid payload and signature. Duplication is the sender delivering the same event more than once, usually because it retried. Disorder is events arriving in a different order than they happened. They look alike in logs and need different fixes.
Replay: sign the time
Stripe's webhook documentation describes the pattern well. The signature header carries a timestamp, and the timestamp is part of the signed payload, so an attacker cannot change it without breaking the signature. If the signature is valid but the timestamp is too old, your code can reject it. Stripe's libraries default to a tolerance of five minutes, and the page warns that a tolerance of zero disables the recency check entirely.
Two supporting details from the same page: use a constant-time comparison when checking signatures, and keep your server clock synced with NTP. It also notes that signature verification needs the raw request body, so a framework that rewrites the body breaks verification. For a worked example on another platform see Shopify webhook HMAC verification.
Duplicates: remember what you did
The documentation says endpoints might occasionally receive the same event more than once, and suggests logging the event IDs you have processed and skipping ones already seen. In some cases two separate event objects are sent for the same thing, so it also suggests matching on the object ID in the data plus the event type.
Make the handler idempotent where you can. Setting a status to paid twice is harmless. Sending an email twice is not, so guard it with the stored event ID.
Disorder: do not trust sequence
Stripe states it does not guarantee delivery in the order events are generated, and that the created timestamp is recorded in seconds, so distinct events can share a timestamp. The page advises against using it to decide order or whether an event was processed. If an event arrives before the thing it refers to, fetch the current state from the source API instead of assuming.
Respond fast, work later
The page says to return a 2xx before complex logic that could time out, and recommends processing events through an asynchronous queue so a spike does not overwhelm your hosts. This also helps security: a slow handler invites retries, which creates duplicates.
Other hygiene from the same guidance
- Verify the signature before any action. The docs explain that without it an attacker could send fake events to trigger fulfilment or change records.
- Roll signing secrets periodically and when you suspect exposure. The page describes keeping the old secret active for up to 24 hours during a rollover.
- Subscribe only to the event types you need.
- Exempt the webhook route from CSRF protection only for that route, since the signature is its protection.
The same rules for internal queues
Internal brokers also redeliver messages after a consumer crash. Treat at-least-once delivery as normal and design for it.
Safeguard can help here in a small way. Its SBOM and dependency scanning covers the broker client libraries and service frameworks your code pulls in, and reachability analysis shows which advisories your services actually call. You can try a free scan at gold.safeguard.sh/scan.
Quick check
Is a valid signature enough?
No. A replayed message also has a valid signature. Check the timestamp and track event IDs too.
Should I process events in the order received?
Do not rely on it. Delivery order is not guaranteed, so fetch current state when order matters.
Sources
Last reviewed: 19 October 2026.