hooksentinel
Errors

timestamp_out_of_tolerance

hooksentinel error timestamp_out_of_tolerance — the request's signed timestamp is outside the allowed window, usually clock skew or a replayed request.

Summary

FieldValue
Codetimestamp_out_of_tolerance
HTTP status400
RetryableNo

What caused it

Providers that include a timestamp in their signature scheme (Stripe, Slack, Discord, Standard Webhooks) do so specifically to prevent replay attacks: even a valid, correctly-signed payload should be rejected if it's being replayed long after it was originally sent, because a captured request-and-signature pair would otherwise be valid forever. hooksentinel checks the timestamp against the current server time and rejects anything outside the configured tolerance — 300 seconds (5 minutes) by default.

Two very different situations produce this error:

  1. Your server's clock is wrong. If your server's clock has drifted (common on containers/VMs without NTP sync, or after a suspended VM resumes), every request looks either too old or from the future, even though the provider sent it seconds ago.
  2. A genuine replay — someone captured a valid request and signature and is resending it later. This is exactly the scenario tolerance checking exists to catch.
  3. Legitimately delayed delivery. A provider's own retry queue redelivering an event after an outage can, in rare cases, exceed the default 5-minute window even though it's not malicious — this is what duplicate_event and idempotency handle in the common case, but a very late redelivery can hit the timestamp check first if the payload is old enough.

The fix

Check your server's clock first — this is the most common cause by far:

timedatectl status   # Linux — confirm "NTP synchronized: yes"
date -u               # compare against an external time source

If you're running in a container or VM, confirm the host has NTP enabled; a guest OS can drift significantly after being paused and resumed, which is a common cause on some hosting platforms.

If your clock is correct and you're seeing this from a provider's legitimately delayed retry, you can widen the tolerance — but do this deliberately, since it weakens replay protection:

import { stripe } from '@hooksentinel/core';

stripe({
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
  tolerance: 600, // 10 minutes instead of the 5-minute default
});

If you suspect an actual replay attack, don't widen the tolerance — investigate instead. Check ctx.headers and request metadata (source IP, user agent) logged via onError for the request that triggered this, and confirm your webhook secret hasn't leaked (rotate it if there's any doubt, since a leaked secret combined with a captured request is what makes replay attacks possible in the first place).

Last updated on

On this page