What is a timestamp tolerance?
Timestamp tolerance is the maximum age difference — typically 5 minutes — between a signed webhook and "now" before the receiver rejects it. The signed payload includes a timestamp; the receiver computes `abs(now - t)` and rejects if the delta exceeds the tolerance. Five minutes (300 seconds) is the standard balance — long enough for slow networks, retry delays, and clock drift, short enough that intercepted deliveries become useless quickly. Check the timestamp before verifying the signature so cheap rejection happens first. Two pitfalls: server clock drift (run NTP) and tolerance set too tight (every retry-with-network-spike fails).
Timestamp tolerance is the window your handler accepts. If the signed timestamp is more than tolerance seconds old (or more than tolerance seconds in the future, accounting for clock skew), reject. Stripe's recommendation is 300 seconds (5 minutes); GitHub doesn't include a timestamp at all; Slack uses 300 seconds; Shopify ships a separate X-Shopify-Webhook-Timestamp header.
Why this exists: a replay attack with a captured valid signature succeeds against signature verification. The defense is "even if the signature is valid, this delivery is too old to be real." Five minutes is the standard balance — long enough for slow networks and clock drift, short enough that captured deliveries become useless quickly.
Implementation, for a Stripe-style t=<unix>,v1=<hex> header:
``
const t = parseInt(parts.t, 10)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - t) > 300) {
return res.status(401).send('stale webhook')
}
``
The check has to happen *before* signature verification, or in parallel — never after. If you verify the signature first and only then check the timestamp, you're doing the expensive HMAC work on every replay attempt; cheap timestamp rejection first is better for both correctness and DoS resistance.
Two pitfalls. Server clock drift. If your server's clock is wrong by more than the tolerance, every legitimate webhook fails verification. Run NTP. On managed platforms (AWS, GCP, Heroku) NTP is usually handled; on bare-metal VPSes, verify it. Tolerance too tight. A 30-second tolerance feels secure but breaks under any network latency spike, retry delay, or queue backup. Five minutes is the right default; reduce only if you have specific evidence that shorter delays are required.
What if the provider doesn't include a timestamp? GitHub is the canonical example — X-Hub-Signature-256 covers the body and nothing else. The defenses are weaker: idempotency at the handler is mandatory, and you must trust the TLS layer to prevent passive interception (which it does well in 2026; HTTPS makes silent replay capture genuinely difficult). For maximum defense, a few teams add their own timestamp check using the GitHub event's created_at field — but this only defends against replays of stale events, not freshly-captured ones.
Reject stale webhooks with HTTP 401. Don't silently 200 them. Log the timestamp delta so you can spot clock drift and replay attempts.
See Timestamp Tolerance in real traffic
WebhookWhisper captures every webhook with full headers, body, signature, and timing — so concepts like timestamp tolerance stop being abstract and become something you can inspect.
Start Free