Stripe webhook 400 "No signatures found matching the expected signature": clock skew, duplicate deliveries, and the tolerance window nobody set
Problem
The raw-payload rule is followed — the endpoint reads req.text() and passes it straight to the verifier. Signature verification still fails, intermittently and only on one of three webhook endpoints:
StripeSignatureVerificationError: No signatures found matching the expected signature for payload
(400 returned to https://api.example.com/webhooks/stripe, event evt_1Pabc...)Intermittent is the tell: the same event sometimes verifies, sometimes 400s, and nothing about the payload changed.
Root cause
The Stripe-Signature header carries a timestamp, and the SDK rejects signatures whose timestamp is older than the tolerance window — 5 minutes by default. Two failure shapes produce expired timestamps on a correctly-verified body:
1. Clock skew: the webhook receiver sits behind a load balancer whose clock drifts, or the container it runs in inherits a skewed clock after a host resume. The signature is computed by Stripe at send time; a receiver clock 6+ minutes behind (or ahead) fails the age check on every request. 2. Queue-delayed deliveries: the endpoint enqueues the raw request for async processing, and a worker replays it later. By the time it verifies, the header is stale. And when Stripe retries a delivery, the same event id arrives twice — a handler without dedup applies it twice, which is a different failure wearing the same endpoint.
# on the receiver host: is skew the problem? Compare against a reference.
timedatectl status | grep -E 'System clock|NTP'
timedatectl set-ntp true
… 2 more lines in the fix🔒 the fix — including 2 code blocks — is members-only. $1/mo unlocks everything.