What is a replay attack?
A replay attack is when an attacker captures a legitimate webhook request and sends it to the receiver again, unmodified, to trigger its effect a second time. Because the copy is byte-for-byte identical to the original, it carries a perfectly valid webhook signature: signature verification alone proves who sent a message and that it wasn't altered, but not when it was sent.
How a webhook replay attack works
The attacker first needs a copy of one real delivery. That can come from a compromised log aggregator or error tracker that stored full request bodies and headers, a misconfigured proxy, or an old endpoint URL that now points at infrastructure the attacker controls. No cryptographic break is involved; the attacker never learns the signing secret and never modifies the message.
They then POST the captured request to the real webhook endpoint, as many times as they like. The receiver recomputes the HMAC over the raw body, gets a match, and processes the event again. What "again" means depends on the event: a replayed payment.refunded might issue duplicate refunds, a replayed user.created might fire duplicate provisioning, and a replayed password-reset event might reopen a window that was supposed to be closed.
Replays don't have to be malicious to cause the same damage. A provider's retries after a timeout, or a stalled delivery arriving late, produce duplicates with the same shape, which is why the defenses below are worth having even if you never face an attacker.
Defense one: signed timestamps with a tolerance window
The Standard Webhooks specification, which Svix co-created, sends a webhook-timestamp header with every delivery and includes that timestamp in the signed content. Receivers reject messages whose timestamp falls outside an allowable tolerance of the current time; five minutes is the common choice.
This closes the long-tail replay: a request captured today is worthless tomorrow. The signature makes the timestamp trustworthy, since an attacker who rewrites webhook-timestamp to the current time invalidates the HMAC, and one who leaves it alone fails the freshness check. One operational caveat: this check compares your clock against the provider's, so a receiver with significant clock drift will start rejecting legitimate traffic. Keep NTP running.
Defense two: idempotency inside the window
The tolerance window still leaves a few minutes during which a captured message replays successfully. The spec's recommendation is to treat the webhook-id header as an idempotency key: remember the IDs you've processed recently and drop any request whose ID you've already seen. Since IDs only need to be remembered for the length of the tolerance window, a cache with a short TTL is enough; there's no unbounded table to maintain.
Idempotent processing also absorbs benign duplicates from provider retries, which makes it worth implementing even in threat models that exclude attackers.
What replay protection does not cover
Timestamps and idempotency keys defeat resending, not forgery or tampering; those are the signature's job. The three checks form one pipeline, and dropping any of them reopens an attack:
| Check | Stops |
|---|---|
| Signature over ID, timestamp, and raw body | Forged and tampered messages |
| Timestamp within tolerance | Replays after the window |
webhook-id deduplication | Replays inside the window |
Verification libraries from providers that follow the spec, Svix's included, perform the signature and timestamp checks together by default, so the practical work for most receivers is adding the deduplication step and making handlers safe to run twice.
For the broader picture of securing an endpoint, see webhook security and our webhook security best practices.
Frequently asked questions
Can a replay attack succeed against a signed webhook?
Yes, if the receiver checks only the signature. A replayed message is an exact copy of a legitimate one, so its signature verifies. Stopping replays requires the two additional checks: a signed timestamp within a tolerance window, and deduplication on the message ID.
What is the recommended timestamp tolerance for webhooks?
Five minutes is the common default. The Standard Webhooks specification requires the timestamp to be within an allowable tolerance of the current time and leaves the exact value to the implementation; most provider libraries, including Stripe's and Svix's, ship with five minutes.
How long do I need to store webhook IDs for deduplication?
Only as long as the timestamp tolerance window, because anything older is rejected by the freshness check before deduplication runs. With a five-minute window, a cache with a TTL of five to ten minutes covers it.
Are replay attacks the same as duplicate deliveries?
Mechanically yes, which is convenient: a provider retry after a timeout produces the same double-processing as an attacker replay. Idempotent handling keyed on the webhook ID absorbs both, so the same fix covers the accident and the attack.
Ready to send webhooks?
Svix handles signing, retries, rate limiting, and delivery observability for the webhooks you send to your users, so your team can stay focused on your product.
Start sending webhooks with Svix or read the build vs. buy analysis