All posts
webhooksreliabilityperformancebackend

Why Your Webhook Handler Should Respond in Under 5 Seconds

Webhook providers time out fast, and a slow handler looks identical to a broken one from their side. What causes slow responses and how to fix them.

19 August 20264 min read

A webhook handler that takes 8 seconds to run looks, from the provider's side, exactly like a handler that's completely broken. There's no partial credit for "it would have worked eventually." The request either got a 2xx back inside the timeout window or it didn't.

This is the failure mode that catches teams who tested their integration against a handful of events and never noticed the clock.

Why the timeout is so tight

Providers aren't being stingy. They're holding open an HTTP connection and a worker thread (or the equivalent) for every webhook they send, multiplied across every customer's endpoint. A generous timeout on their side means their own infrastructure is at the mercy of the slowest endpoint on the internet.

Shopify: 5 seconds
GitHub:  10 seconds
Watchtower: 10 seconds
Stripe:  20 seconds

None of these numbers are long enough to do meaningful work synchronously: a database write with a slow query, an API call to a third-party service, a PDF generation step, an email send. Any one of those can eat the entire window on a bad day, even if it's usually fast.

The pattern that fails

The most common way teams get bitten:

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);

  // Looks fine in dev, where the DB is fast and idle
  await db.orders.update({ where: { id: event.data.object.id }, data: { status: 'paid' } });
  await sendConfirmationEmail(event.data.object);
  await syncToAnalytics(event.data.object);

  res.status(200).send('ok');
});

Locally, with a warm connection pool and no load, this might run in 200ms. In production, under real traffic, the database write can queue behind other transactions, the email provider can have a slow day, and the analytics call can hang on a flaky third-party API. Any one of those pushes you over the timeout, and the provider marks the whole delivery as failed.

The fix: acknowledge first, process after

Split the handler into two phases. Phase one does the minimum needed to safely say "received": verify the signature, store the raw payload, respond 200. Phase two, which has no deadline, does the actual work.

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);

  // Phase one: fast, synchronous, bounded
  await db.webhookEvents.insert({ id: event.id, payload: event, status: 'pending' });
  res.status(200).send('ok');

  // Phase two: no deadline, runs after the response is sent
  void processStripeEvent(event).catch((err) => {
    logger.error(`Failed to process ${event.id}`, err);
  });
});

Fire-and-forget after res.send() works, but if your process crashes or restarts between the response and the async work finishing, that event is lost. For anything that matters, push to a real queue (BullMQ, SQS, Cloud Tasks) instead of relying on the request lifecycle to keep running.

A queue is the durable version of the same idea

The pattern above works for low-stakes processing, but it has a gap: nothing survives a process restart. A queue closes that gap by writing the job somewhere durable before your handler returns.

import { Queue } from 'bullmq';

const webhookQueue = new Queue('stripe-events', { connection: redis });

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);

  await webhookQueue.add('process', event, {
    attempts: 5,
    backoff: { type: 'exponential', delay: 60_000 },
  });

  res.status(200).send('ok');
});

Now the handler's only job is verify, enqueue, respond. Everything that can be slow, unreliable, or third-party happens in a worker with its own retry logic, completely decoupled from the provider's timeout.

Watch for hidden synchronous work

Sometimes the slow part isn't obvious from reading the handler. A few places it likes to hide:

  • Middleware that runs before your route. Global request logging that writes to a slow external service, or an auth check that calls out to another API, adds latency to every request including webhooks.
  • Connection pool exhaustion. If your database pool is maxed out from other traffic, a webhook's query queues behind everything else, and a fast query becomes a slow one under load rather than in isolation.
  • Cold starts. On serverless platforms, a webhook that arrives after a period of inactivity can eat several seconds just spinning up the function, before your code runs at all.

Checking if this is actually your problem

If you're seeing intermittent delivery failures with no obvious error in your logs, response time is worth ruling out before anything else. Most providers show you the response time for each delivery attempt in their own dashboard: GitHub's Recent Deliveries tab, Stripe's webhook event log, and Shopify's notifications page all display it directly.


The fix is almost always the same regardless of provider: verify, store, respond, then do the real work in a background job with no deadline. If timeouts turn out not to be the issue, the why webhooks fail guide covers the other common failure modes.

A slow handler stops mattering as much once something else is holding the event: with Webhook Watchtower sitting in front of your endpoint, a timeout only affects delivery to your own infrastructure, not whether the event was captured in the first place, and every attempt's response time is visible per event.

Frequently asked questions

How long do I have to respond to a webhook?

It depends on the provider. Shopify gives you 5 seconds, GitHub and Watchtower both use a 10-second window, and Stripe allows up to 20. Treat anything over 3 seconds as a warning sign rather than relying on the slowest provider's limit as your target.

What happens if my webhook handler is too slow?

The provider treats it as a failed delivery, the same as a 500 error or a dropped connection. Depending on the provider's retry policy, it either tries again later or gives up. Either way, your handler may end up processing the same event twice: once from the slow original attempt finishing, and once from the retry.

Should I use a queue for every webhook handler?

For anything beyond a single fast database write, yes. Acknowledge with a 200 immediately, push the payload onto a queue, and let a worker process it without a deadline. This is the single biggest fix for timeout-related webhook failures.

Share this post

Webhook Watchtower

Stop debugging webhooks in logs.

Capture every event, replay failures, and get alerted when deliveries fail, without changing your existing setup.

Try it free →