Webhook Engineering · Chapter 3 · 11 min
Verify, timestamp and replay windows
Constant-time comparison, tolerance windows and deduplication by event id.
Verification, step by step
The verification process must be meticulous to be effective against determined attackers on the web. First, read the raw body as bytes before any parser touches it to ensure complete integrity. Next, parse the t and every v1 from the signature header provided by the ub3r platform. You should then reject the request if the difference between now and t exceeds your window.
Why constant time
A naive === comparison returns as soon as the first byte differs, which is a risk. Measured over thousands of requests, that timing difference leaks how many leading bytes were correct. This is known as a timing attack, and it's a real threat in high-security environments. Use crypto.timingSafeEqual in Node to ensure your verification takes the same amount of time regardless of input.
−const expected = hmac(secret, body);−if (header.v1 === expected) {− handle(JSON.parse(body));−}
The replay window is not enough
A five-minute window still lets an attacker replay a captured request multiple times within that period. To prevent this, you must record processed event ids for at least the length of the window. This doubles as protection against legitimate duplicate deliveries, which every producer will eventually send due to noise. Event deduplication is a core requirement for any idempotent webhook consumer in a production system.
Implementing the window check
When checking the timestamp, be mindful of clock drift between your server and the ub3r platform. A small amount of drift is normal, but a large discrepancy can cause valid requests to fail. Allow for a small buffer on both sides of the current time to account for this drift. This makes your system more resilient to minor timing variations while providing strong protection against old requests.
const FIVE_MINUTES = 5 * 60 * 1000;
const diff = Math.abs(Date.now() - timestamp);
if (diff > FIVE_MINUTES) {
throw new Error("Request outside of tolerance window");
}Scaling your Deduplication Store
As your event volume grows, your store of processed event IDs will also grow significantly. Use a high-performance, TTL-enabled store like Redis to manage these IDs efficiently without manual cleanup. Set the TTL to be slightly longer than your replay window to ensure complete coverage. This keeps your memory usage predictable while providing robust protection against duplicate event processing at scale.
Discussion (3)
Ran the playground with three concurrent sends — receipts came back out of order, exactly as the text says. Great way to make it stick.
Good point — we're adding a short section on this in the next revision.
We learnt the idempotency lesson the expensive way: duplicate OTPs during an incident. This chapter is now required reading on my team.