Webhook Reliability: A Guide to Never Missing an Event
Webhooks from services like Stripe or GitHub are critical. Learn how to build resilient webhook handlers, monitor their uptime, and implement strategies like idempotency and retry queues to ensure you never miss a critical event.

Webhooks are the connective tissue of the modern web, delivering real-time events from services like Stripe, GitHub, and Twilio directly to your application. A successful payment, a new code commit, or an incoming SMS-these events are often business-critical. But what happens when your application is down during a deployment, or a bug causes your webhook handler to crash? You could lose revenue, break a CI/CD pipeline, or miss customer messages. Silent failures are the most dangerous.
This guide provides a practical framework for building a robust webhook ingestion system that can withstand failures, retries, and unexpected load. We'll cover architecture, code patterns, and monitoring strategies to ensure you never miss a critical event.
Why Webhooks Fail
Before we build a solution, we must understand the failure modes. A webhook delivery fails for a handful of reasons:
- Your Endpoint is Down: The most obvious cause. Your server is offline due to a crash, deployment, or network outage. The sender gets a connection error.
- Your Endpoint is Slow: Your handler performs a complex, time-consuming task (e.g., image processing, generating a PDF). The sending service gives up, timing out before you can send a
200 OKresponse. - Your Endpoint Returns an Error: Your code encounters a bug, a validation error, or a database connection issue and returns a
4xxor5xxstatus code. Most senders will interpret this as a failure and may retry. - Network Problems: Transient issues on the internet between the sender's servers and yours can cause packet loss or timeouts.
- Post-Acknowledgement Failures: The most insidious problem. Your handler immediately returns
200 OKbut then fails during a subsequent asynchronous task. The sender thinks the event was delivered successfully and will not retry.
The Core Principle: Acknowledge, Then Process
The single most important architectural choice for webhook reliability is to separate the acknowledgment of an event from the processing of it. The job of your public webhook endpoint should be minimal: validate the request, place it onto a queue, and immediately return a 200 OK.
This pattern solves two major problems: it makes your endpoint extremely fast, preventing timeouts, and it decouples ingestion from processing, allowing you to handle processing failures independently.
Here’s a conceptual example in a Node.js/Express app using a job queue like BullMQ (backed by Redis):
// In your Express app
const { Queue } = require('bullmq');
const express = require('express');
const app = express();
// Connect to your Redis-backed queue
const webhookQueue = new Queue('webhook-jobs');
// Middleware to parse JSON bodies
app.use(express.json());
app.post('/webhooks/stripe', async (req, res) => {
// 1. (Optional but Recommended) Validate the signature first
// ... signature validation logic ...
// 2. Add the job to the queue
// The job includes the payload and any relevant headers
await webhookQueue.add('process-stripe-event', {
headers: req.headers,
body: req.body
});
// 3. Immediately acknowledge success
console.log('Event queued:', req.body.id);
res.status(200).send({ received: true });
});
// In a separate worker process...
const { Worker } = require('bullmq');
const worker = new Worker('webhook-jobs', async job => {
console.log('Processing event:', job.data.body.id);
// ... your actual business logic goes here ...
// e.g., update a database, send an email, etc.
// This can fail and be retried without affecting ingestion.
});
This architecture ensures your endpoint responds in milliseconds, well within the timeout limits of any webhook provider.
A Resilient Webhook System: Checklist
Use this checklist to build and audit your webhook handling infrastructure.
- [ ] Use a Job Queue: As shown above, a proper job queue (like AWS SQS, Google Pub/Sub, RabbitMQ, or Redis-based systems) is the foundation. It provides persistence, ensuring that even if your worker processes crash, the events are safe.
- [ ] Implement Idempotency: Webhook senders often retry, and your own workers might retry a failed job. This can lead to the same event being processed multiple times. Design your worker logic to be idempotent-meaning running it multiple times with the same input has the same effect as running it once. A common pattern is to save the unique event ID (
idfrom Stripe,X-GitHub-Deliveryfrom GitHub) and check if it has been processed before starting. - [ ] Handle Retries & Dead-Letter Queues (DLQs): Configure your job queue workers with a retry strategy (e.g., exponential backoff). After a set number of failures, the job should be moved to a Dead-Letter Queue (DLQ) for manual inspection. This prevents a single poison-pill message from blocking your entire queue.
- [ ] Validate Signatures: Never trust a webhook payload without verifying its source. Most services sign their webhooks (e.g., using HMAC-SHA256). This prevents attackers from sending forged events to your endpoint. Always perform this check before queuing the job.
- [ ] Monitor Endpoint Uptime: Your endpoint must be available 24/7. A simple uptime check is the first line of defense. Services like ServerInspector can monitor your webhook URL from multiple geographic regions, ensuring it's globally accessible and responding with a
200 OKstatus code. If the check fails, you get an immediate alert. - [ ] Monitor Background Job Health: Uptime monitoring for the endpoint is not enough. You also need to know if your background workers are running and processing jobs. A heartbeat check (or 'dead man's switch') is perfect for this. Configure your worker to send a request to a special URL after successfully processing a batch of jobs. If ServerInspector doesn't receive a ping within a configured interval (e.g., 5 minutes), it will alert you that your processing has stalled.
Comparison: Naive vs. Resilient Webhook Handling
| Feature | Naive Handler | Resilient Handler |
|---|---|---|
| Acknowledgment Speed | Slow (depends on business logic) | Fast (sub-50ms) |
| Failure Handling | Sender must retry; processing errors can cause 500s. | Ingestion and processing failures are isolated. |
| Retries | Relies entirely on the sender. | Sender retries + internal worker retries with a DLQ. |
| Idempotency | Not handled; can lead to duplicate processing. | Handled in the worker to prevent side effects. |
| Monitoring | Simple endpoint uptime check. | Uptime check for endpoint + heartbeat check for workers. |
Debugging Common Webhook Issues
-
Problem: Receiving
4xx/5xxErrors in the Sender's Dashboard- Solution: Check your application logs for the corresponding timestamp. For local development, use a tool like
ngrokto expose your local server to the internet.ngrok http 3000gives you a public URL you can provide to the webhook sender and an interface to inspect raw requests and responses.
- Solution: Check your application logs for the corresponding timestamp. For local development, use a tool like
-
Problem: Timeouts
- Solution: Your handler is too slow. Immediately implement the Acknowledge-Then-Process pattern. Move all non-essential logic (database writes, API calls, email sending) to a background worker.
-
Problem: Not Receiving Events at All
- Solution: First, check the sender's dashboard for delivery logs (Stripe and GitHub have excellent ones). Second, confirm your endpoint URL is correct and publicly accessible. Use
curlfrom an external server (not localhost) to verify connectivity:
curl -X POST -H "Content-Type: application/json" \ -d '{"test_event": true}' https://your-app.com/webhooks/my-handlerIf this fails, you have a firewall, DNS, or server issue.
- Solution: First, check the sender's dashboard for delivery logs (Stripe and GitHub have excellent ones). Second, confirm your endpoint URL is correct and publicly accessible. Use
FAQ about Webhook Reliability
Q1: Isn't the sender's built-in retry logic enough?
No, for two reasons. First, if your processing logic has a bug that takes longer to fix than their retry window (which can be hours or days), you will eventually lose the event. Second, it doesn't protect you from post-acknowledgement failures, where you've told the sender everything was fine but failed internally.
Q2: How can I test webhooks on my local machine?
ngrok is the industry standard. It creates a secure tunnel to your local machine with a public URL. You can give this URL to services like Stripe or GitHub for testing. The ngrok web interface lets you inspect and replay requests, which is invaluable for debugging.
Q3: What's the difference between monitoring my webhook URL and monitoring my background workers?
Monitoring the URL checks for ingestion availability-can your app receive the event? Monitoring the background workers with a heartbeat check confirms processing availability-is your app actually doing the work? You need both for a complete picture of your system's health.
Q4: Should my webhook endpoint require authentication?
Absolutely. But not with a traditional API key in a header. The standard and most secure method is signature validation. The sender uses a shared secret to create a signature for the payload, which you then verify. This proves the request is authentic and hasn't been tampered with. Never process a webhook without validating its signature.