Guides · Free check · $9 fix pack

Guides

Stripe webhook error: "Timestamp outside the tolerance zone"

The signature itself was fine. Your server decided the event was too old. That's either your clock or your code verifying the request late, and the quick fix people share (setting the tolerance to 0) quietly switches off replay protection.

What the error means

Every webhook request carries a Stripe-Signature header like t=1729680000,v1=.... The t= value is a Unix timestamp, and it's part of the signed payload, "so an attacker can't change the timestamp without invalidating the signature" (Stripe docs: Preventing replay attacks). Stripe's libraries "have a default tolerance of 5 minutes between the timestamp and the current time" (source). In stripe-node that's DEFAULT_TOLERANCE: 300 seconds, and when the event is older the library throws Timestamp outside the tolerance zone (stripe-node source).

The check only looks one way. stripe-node computes the age as the current time minus t and rejects it only when the age is greater than the tolerance (source); stripe-python likewise only rejects a timestamp older than the current time minus the tolerance (stripe-python source). So with those libraries, a server clock running fast by more than 5 minutes triggers the error; a clock running slow does not.

What causes it

Not a cause: Stripe's retries. After downtime, retried deliveries don't arrive with the old timestamp. Stripe "generates the timestamp and signature each time we send an event to your endpoint. If Stripe retries an event (for example, your endpoint previously replied with a non-2xx status code), then we generate a new signature and timestamp for the new delivery attempt" (source). If retries fail with this error, look at your clock, not at the retry.

How to fix it

  1. Sync the clock. Run a time sync service (chrony, ntpd or systemd-timesyncd) on the host or VM that runs your server. On most Linux systems, timedatectl shows whether the system clock is synchronized. To see the skew directly, log now - t for each request (code below).
  2. Verify immediately, then queue. Call constructEvent in the request handler, return 2xx, and queue the verified event for the slow work. Stripe asks you to "quickly return a successful status code (2xx) before any complex logic that could cause a timeout" anyway (Stripe docs: Receive events).
  3. Keep the default tolerance. stripe-node's constructEvent(payload, header, secret, tolerance, cryptoProvider, receivedAt) takes tolerance in seconds and falls back to 300 when you don't pass it (source). Leave it out.

Why tolerance 0 is a trap

Stripe lists it as a common mistake: "Don't use a tolerance value of 0. Using a tolerance value of 0 disables the recency check entirely." (source). In stripe-node the check is tolerance > 0 && timestampAge > tolerance, and the source comment says to "Set tolerance to 0 to skip timestamp verification" (source). stripe-python skips it too when tolerance is 0 (source). The signature is still checked, but anyone who captures one real request can replay it forever. A huge tolerance (hours, days) does nearly the same thing. Fix the clock instead.

Log the skew

// Before constructEvent: how old is this request by our clock?
const sig = req.headers['stripe-signature'] || '';
const t = Number((sig.match(/(?:^|,)t=(\d+)/) || [])[1]);
console.log('stripe webhook age (s):', Math.floor(Date.now() / 1000) - t);
// A live Stripe delivery should be a few seconds old. Hundreds of seconds = clock or queue problem.
const event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);

How to test it

// test: a 10-minute-old signed request must be rejected
const payload = JSON.stringify({ id: 'evt_test', object: 'event' });
const header = stripe.webhooks.generateTestHeaderString({
  payload, secret: 'whsec_test', timestamp: Math.floor(Date.now() / 1000) - 600,
});
// stripe.webhooks.constructEvent(payload, header, 'whsec_test') throws "Timestamp outside the tolerance zone"

The other two signature errors are covered in "No signatures found matching the expected signature" (raw body, wrong secret). And if your handler skips verification entirely, see can someone fake a Stripe payment?

Check yours in 10 seconds (free).

Paste your webhook URL. WebhookProof sends 3 harmless test requests and tells you whether Stripe can reach it and whether it rejects unsigned events. If it finds a problem, the $9 fix pack gives you the exact copy-paste fix for your stack (refundable for 14 days).

Run the free check

FAQ

What does "Timestamp outside the tolerance zone" mean in Stripe?

The signature checked out, but the t= timestamp in the Stripe-Signature header is older than the allowed tolerance, 300 seconds (5 minutes) by default in Stripe's libraries, measured against your server's clock.

Should I set the Stripe webhook tolerance to 0?

No. Stripe says a tolerance of 0 disables the recency check entirely, so a captured request could be replayed at any time. Sync your server clock with NTP and keep the default.

Do Stripe webhook retries fail the timestamp check after downtime?

No. Stripe generates a new signature and timestamp for every delivery attempt, including retries.

Can I verify Stripe webhooks later from a queue?

Verify when the request arrives, then queue the verified event. If you verify minutes later, the timestamp may already be outside the tolerance.

Sources

More guides

Stripe webhook not working? Check these 7 things in order · A customer paid but didn't get access. What went wrong? · No signatures found matching the expected signature for payload · Stripe webhook failing with a 307 or 308 redirect

Last reviewed 2026-10-23. WebhookProof is an independent tool, not affiliated with Stripe, Inc.. Quotes are from the linked documentation.