Why Your Webhooks Are Failing: 5 Common Mistakes
You've set up your webhook endpoint, configured it in your third-party service, and deployed to production. Everything seems fine... until webhooks start mysteriously failing. Some arrive, some don't. Error rates climb. Your logs show cryptic failures. Sound familiar?
Webhook failures are frustratingly common, but the good news is that most issues stem from just five recurring mistakes. In this guide, we'll diagnose each one, show you exactly what's going wrong, and provide battle-tested fixes you can implement today.
Understanding Webhook Failure Modes
Before diving into specific mistakes, let's clarify what "webhook failure" actually means. A webhook can fail in several ways:
- HTTP errors: Your endpoint returns 4xx or 5xx status codes
- Timeouts: Your endpoint doesn't respond within the provider's timeout window (typically 5-30 seconds)
- Silent failures: Webhook arrives, you return 200, but processing logic fails
- Rejected webhooks: Provider stops sending after repeated failures
- Duplicate processing: The same event gets processed multiple times
Most webhook providers retry failed deliveries a few times before giving up. Once they disable your endpoint, you'll stop receiving events entirely—often without realizing it until customers complain.
Mistake #1: Slow Response Times
❌ The Problem
Your webhook handler performs slow operations (database queries, API calls, email sending) before returning a response. The webhook provider times out waiting for your 200 status and marks the delivery as failed.
This is the #1 most common webhook mistake. Here's what typically happens:
// ❌ BAD: Slow synchronous processing
app.post('/webhook', async (req, res) => {
const event = req.body;
// This might take 5-30 seconds...
const user = await db.users.findOne({id: event.user_id});
await sendEmail(user.email, 'Event notification');
await updateAnalytics(event);
await notifySlack(event);
// By the time we return, the provider has already timed out
res.json({received: true});
});
âś… The Fix: Acknowledge Immediately, Process Asynchronously
Return a 200 status within milliseconds, then process the event in the background:
// âś… GOOD: Immediate acknowledgment
app.post('/webhook', async (req, res) => {
const event = req.body;
// Return 200 immediately
res.json({received: true});
// Process asynchronously (don't await)
processWebhookEvent(event).catch(err => {
console.error('Background processing failed:', err);
});
});
async function processWebhookEvent(event) {
// Now you can take as long as needed
const user = await db.users.findOne({id: event.user_id});
await sendEmail(user.email, 'Event notification');
await updateAnalytics(event);
await notifySlack(event);
}
Even better: Use a job queue (Redis, RabbitMQ, AWS SQS) for reliable background processing:
const Queue = require('bull');
const webhookQueue = new Queue('webhooks', process.env.REDIS_URL);
app.post('/webhook', async (req, res) => {
// Add to queue and return immediately
await webhookQueue.add(req.body);
res.json({received: true});
});
// Process jobs from the queue
webhookQueue.process(async (job) => {
await processWebhookEvent(job.data);
});
Mistake #2: Missing or Broken Signature Verification
❌ The Problem
Your endpoint either doesn't verify webhook signatures at all, or does it incorrectly. This creates massive security vulnerabilities and can cause mysterious failures when legitimate webhooks are rejected.
Common signature verification mistakes:
- Using
express.json()instead ofexpress.raw()(parsed body can't be verified) - Comparing signatures with
===instead ofcrypto.timingSafeEqual() - Using the wrong secret (test vs. production environments mixed up)
- Not including timestamps in signature verification
- Comparing wrong header names (e.g.,
x-hub-signaturevsx-hub-signature-256)
âś… The Fix: Proper HMAC Verification
const crypto = require('crypto');
// Use raw body parser for webhook routes
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
// Compute expected signature from raw body
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(req.body) // req.body is Buffer with raw()
.digest('hex');
// Constant-time comparison prevents timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(signature || '', 'utf8'),
Buffer.from(expectedSignature, 'utf8')
);
if (!isValid) {
console.error('Invalid signature');
return res.status(401).json({error: 'Invalid signature'});
}
// Now parse the JSON after verification
const event = JSON.parse(req.body);
processWebhook(event);
res.json({received: true});
});
For a deep dive into webhook security, read our guide on webhook security best practices.
Mistake #3: Poor Error Handling
❌ The Problem
Unhandled exceptions cause your webhook handler to crash and return 500 errors. The webhook provider retries, hits the same error, and eventually disables your endpoint.
Common error handling mistakes:
- No try-catch blocks around event processing
- Throwing errors for unknown event types
- Not validating payload structure before accessing fields
- Database connection errors bringing down the entire handler
âś… The Fix: Defensive Error Handling
app.post('/webhook', express.json(), async (req, res) => {
try {
const event = req.body;
// Validate payload structure
if (!event || !event.type || !event.data) {
console.error('Invalid webhook payload:', event);
return res.status(400).json({error: 'Invalid payload'});
}
// Handle known event types
switch (event.type) {
case 'user.created':
await handleUserCreated(event.data);
break;
case 'payment.succeeded':
await handlePaymentSuccess(event.data);
break;
default:
// Unknown events are NOT errors - just log and continue
console.log(`Unhandled event type: ${event.type}`);
}
// Always return 200 for successfully received webhooks
res.json({received: true});
} catch (error) {
// Log the error but still return 200 to prevent retries
console.error('Webhook processing error:', error);
res.status(200).json({
received: true,
error: error.message
});
}
});
Mistake #4: No Idempotency Protection
❌ The Problem
Webhook providers often send the same event multiple times (network issues, retries, etc.). Without idempotency checks, you'll process duplicate events—charging customers twice, sending duplicate emails, or corrupting data.
âś… The Fix: Track Processed Event IDs
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function processWebhook(event) {
const eventId = event.id; // Most providers include unique ID
const key = `webhook:processed:${eventId}`;
// Check if we've already processed this event
const alreadyProcessed = await redis.get(key);
if (alreadyProcessed) {
console.log(`Duplicate webhook ${eventId} - skipping`);
return;
}
// Mark as processing (prevents race conditions)
await redis.set(key, 'processing', 'EX', 86400, 'NX');
try {
// Process the event
await handleEvent(event);
// Mark as successfully processed (keep for 7 days)
await redis.set(key, 'processed', 'EX', 604800);
} catch (error) {
// Remove processing flag to allow retry
await redis.del(key);
throw error;
}
}
Alternative: Use database constraints (unique indexes on event IDs) to prevent duplicate processing at the data layer.
Mistake #5: No Monitoring or Alerting
❌ The Problem
Your webhooks fail silently. You don't discover the issue until customers complain that features aren't working. By then, you've lost hours or days of data.
Critical metrics to monitor:
- Webhook success/failure rate
- Response time (should be <500ms)
- Signature verification failures (indicates attacks or configuration issues)
- Processing queue depth (growing queue = processing is falling behind)
- Unknown event types (provider added new events you should handle)
âś… The Fix: Comprehensive Monitoring
// Basic metrics with Prometheus/StatsD
const metrics = require('prom-client');
const webhookCounter = new metrics.Counter({
name: 'webhooks_received_total',
help: 'Total webhooks received',
labelNames: ['event_type', 'status']
});
const webhookDuration = new metrics.Histogram({
name: 'webhook_processing_duration_seconds',
help: 'Webhook processing duration',
labelNames: ['event_type']
});
app.post('/webhook', async (req, res) => {
const startTime = Date.now();
const event = req.body;
try {
await processWebhook(event);
webhookCounter.inc({
event_type: event.type,
status: 'success'
});
res.json({received: true});
} catch (error) {
webhookCounter.inc({
event_type: event.type,
status: 'error'
});
// Alert on critical errors
if (error.critical) {
await alertTeam('Webhook processing failed', error);
}
res.status(500).json({error: error.message});
} finally {
const duration = (Date.now() - startTime) / 1000;
webhookDuration.observe({event_type: event.type}, duration);
}
});
Even better: Use a webhook monitoring service like HubHook that provides automatic monitoring, alerting, and detailed failure analytics without writing any code.
Stop Debugging Webhook Failures
HubHook automatically monitors all your webhooks, alerts you instantly on failures, and provides detailed debugging info. Catch issues before customers do.
Start Monitoring Free →Bonus: Quick Diagnostic Checklist
When your webhooks are failing, run through this checklist:
- Check provider's webhook dashboard: Look for error messages, delivery attempts, and status codes
-
Verify your endpoint is reachable: Use
curlor Postman to test directly - Check response times: Add logging to measure how long processing takes
- Review recent deployments: Did webhook failures start after a code change?
- Check environment variables: Ensure webhook secrets match across test/production
- Look for rate limiting: Are you hitting provider or infrastructure limits?
- Inspect logs for errors: Look for uncaught exceptions or database errors
- Test signature verification: Use provider's test webhooks to validate
When to Use a Webhook Debugging Tool
If you're experiencing frequent webhook issues, a specialized debugging tool can save hours of troubleshooting:
- Request inspection: See exact headers, payloads, and signatures
- Replay webhooks: Re-send events to test fixes without waiting for new events
- Compare environments: Send same webhook to staging and production to identify differences
- Monitor trends: Identify patterns in failures (time of day, event type, etc.)
Tools like HubHook provide all of this out of the box, plus automatic retries, signature verification testing, and alerting.
Conclusion
The vast majority of webhook failures come down to these five mistakes: slow responses, broken signature verification, poor error handling, missing idempotency, and inadequate monitoring. Fix these, and you'll eliminate 95% of webhook reliability issues.
Remember: webhook integrations are mission-critical infrastructure. They deserve the same level of attention as your database, API, and authentication systems. Invest in proper error handling, monitoring, and testing now to avoid painful debugging sessions later.
For more webhook guides, check out our posts on debugging Stripe webhooks, webhook security best practices, and testing GitHub webhooks locally. And if you're building developer tools, explore Stack Stats Apps for productivity tools or ChainOptics for blockchain analytics.