What This Means
Webhook delivery failures occur when external services cannot successfully send event notifications to your application's webhook endpoints. These failures prevent your application from receiving real-time updates about important events, breaking automation, causing data gaps, and degrading user experience.
Impact on Your Business
Missed Events:
- Payment confirmations not received
- Order updates delayed
- User actions not tracked
- Inventory changes missed
- Critical notifications lost
Data Inconsistency:
- Out-of-sync records
- Stale information displayed
- Duplicate data processing
- Missing transaction records
- Incomplete audit trails
Broken Automation:
- Workflows don't trigger
- Email notifications not sent
- Integrations fail silently
- Business processes stall
- Customer experience degraded
Common Causes
Endpoint Issues:
- URL not publicly accessible
- SSL certificate invalid or expired
- Server not responding (timeouts)
- Wrong HTTP response codes
- Endpoint moved or deleted
Configuration Errors:
- Webhook URL misconfigured
- Wrong HTTP method (GET vs POST)
- Missing or invalid signature verification
- Payload format mismatch
- Content-Type header issues
Network/Infrastructure:
- Firewall blocking requests
- Load balancer misconfiguration
- Rate limiting on your server
- DDoS protection blocking legitimate webhooks
- DNS resolution failures
How to Diagnose
Method 1: Check Webhook Dashboard
Most services provide webhook monitoring dashboards:
- Login to service provider (Stripe, Shopify, etc.)
- Navigate to Webhooks or Developers section
- Check webhook delivery status
- Review failed deliveries
- Check error messages and response codes
What to Look For:
Status: Failed
Response Code: 502 Bad Gateway
Error: Connection timeout after 10s
Attempts: 3 of 3 (retries exhausted)
Last Attempt: 2024-01-15 14:32:01 UTC
Method 2: Test Webhook Endpoint Manually
Test your endpoint using curl or Postman:
# Test basic connectivity
curl -X POST https://your-domain.com/webhooks/stripe \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
# Test with signature (example for Stripe)
curl -X POST https://your-domain.com/webhooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=timestamp,v1=signature" \
-d '{"id": "evt_test", "type": "charge.succeeded"}'
# Check response
# Should return 200 OK
# Response body should acknowledge receipt
What to Look For:
- HTTP 200 OK response (success)
- HTTP 401/403 (authentication issues)
- HTTP 404 (endpoint not found)
- HTTP 500 (server error)
- Timeout (no response)
- SSL/TLS errors
Method 3: Review Server Logs
Check your application and web server logs:
# Application logs
tail -f /var/log/app/webhooks.log
# Nginx logs
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
# Apache logs
tail -f /var/log/apache2/access.log
tail -f /var/log/apache2/error.log
Common log patterns:
[ERROR] Webhook signature verification failed
[WARN] Webhook received but processing timed out
[ERROR] Database connection failed during webhook processing
[INFO] Webhook received: order.created (id: 12345)
[ERROR] Invalid JSON payload in webhook
Method 4: Use Webhook Testing Tools
Test webhooks using dedicated tools:
Tools:
- webhook.site - Inspect webhook payloads
- RequestBin - Capture and inspect requests
- ngrok - Test localhost endpoints publicly
- Service-specific testing (Stripe CLI, Shopify CLI)
Process:
- Create test endpoint URL
- Configure webhook to point to test URL
- Trigger test event
- Inspect received payload
- Verify headers, body, signature
Method 5: Check SSL Certificate
Verify your SSL certificate is valid:
# Check SSL certificate
openssl s_client -connect your-domain.com:443 -servername your-domain.com
# Check certificate expiration
echo | openssl s_client -servername your-domain.com \
-connect your-domain.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Verify certificate chain
curl -vI https://your-domain.com/webhooks/endpoint
What to Look For:
- Certificate expired
- Self-signed certificate
- Certificate mismatch (wrong domain)
- Incomplete certificate chain
- Weak cipher suites
General Fixes
Fix 1: Configure Webhook Endpoint Correctly
Ensure your webhook endpoint is properly set up:
Node.js/Express example:
const express = require('express');
const app = express();
// Raw body parser for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
// Webhook endpoint
app.post('/webhooks/stripe', async (req, res) => {
// Get raw body for signature verification
const payload = req.body;
const signature = req.headers['stripe-signature'];
try {
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
// Process event
await processWebhookEvent(event);
// Respond quickly (within 10 seconds)
res.status(200).json({ received: true });
// Process event asynchronously if needed
// Don't make webhook wait for slow operations
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
});
async function processWebhookEvent(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
}
app.listen(3000);
Important considerations:
- Return 200 OK quickly (< 10 seconds)
- Process heavy work asynchronously (queues)
- Use raw body parser for signature verification
- Log all webhook receipts
- Handle errors gracefully
Fix 2: Implement Webhook Signature Verification
Verify webhooks are from legitimate sources:
Stripe example:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
function verifyStripeWebhook(payload, signature, secret) {
try {
const event = stripe.webhooks.constructEvent(
payload,
signature,
secret
);
return event;
} catch (err) {
console.error('Signature verification failed:', err.message);
throw new Error('Invalid signature');
}
}
// In webhook handler
app.post('/webhooks/stripe', (req, res) => {
const signature = req.headers['stripe-signature'];
try {
const event = verifyStripeWebhook(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
// Signature valid, process event
processEvent(event);
res.status(200).json({ received: true });
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
}
});
Shopify example:
const crypto = require('crypto');
function verifyShopifyWebhook(payload, hmacHeader, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('base64');
if (hash !== hmacHeader) {
throw new Error('Invalid webhook signature');
}
}
app.post('/webhooks/shopify', (req, res) => {
const hmac = req.headers['x-shopify-hmac-sha256'];
try {
verifyShopifyWebhook(
req.body,
hmac,
process.env.SHOPIFY_WEBHOOK_SECRET
);
// Process webhook
res.status(200).send('OK');
} catch (err) {
res.status(401).send('Unauthorized');
}
});
Fix 3: Implement Retry Logic and Idempotency
Handle duplicate deliveries and implement retries:
// Track processed webhooks to handle duplicates
const processedWebhooks = new Set();
app.post('/webhooks/endpoint', async (req, res) => {
const webhookId = req.headers['x-webhook-id'];
// Check if already processed (idempotency)
if (processedWebhooks.has(webhookId)) {
console.log(`Webhook ${webhookId} already processed, skipping`);
return res.status(200).json({ received: true });
}
try {
// Process webhook
await processWebhook(req.body);
// Mark as processed
processedWebhooks.add(webhookId);
// Respond success
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook processing failed:', err);
// Return 500 to trigger retry
res.status(500).json({ error: 'Processing failed' });
}
});
// Clean up old webhook IDs periodically
setInterval(() => {
processedWebhooks.clear();
}, 24 * 60 * 60 * 1000); // Clear daily
Better approach with database:
app.post('/webhooks/endpoint', async (req, res) => {
const webhookId = req.headers['x-webhook-id'];
try {
// Check if webhook already processed
const existing = await db.webhooks.findOne({ webhookId });
if (existing) {
console.log(`Duplicate webhook ${webhookId}, already processed`);
return res.status(200).json({ received: true });
}
// Store webhook receipt
await db.webhooks.insert({
webhookId,
payload: req.body,
receivedAt: new Date(),
status: 'processing'
});
// Respond quickly
res.status(200).json({ received: true });
// Process asynchronously
await queue.add('process-webhook', {
webhookId,
payload: req.body
});
} catch (err) {
console.error('Error handling webhook:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
Fix 4: Handle Webhook Retries
Understand and configure retry behavior:
Common retry patterns:
- Immediate retry
- Exponential backoff (1min, 5min, 15min, 1hr, etc.)
- Maximum retry attempts (usually 3-10)
- Manual retry option
Best practices:
// Respond with appropriate status codes
// Success - no retry needed
res.status(200).send('OK');
// Temporary error - will retry
res.status(500).send('Temporary error');
res.status(502).send('Bad gateway');
res.status(503).send('Service unavailable');
res.status(504).send('Gateway timeout');
// Permanent error - will NOT retry
res.status(400).send('Bad request');
res.status(401).send('Unauthorized');
res.status(403).send('Forbidden');
res.status(404).send('Not found');
Configure retry settings in webhook provider:
// Example: Configure Stripe webhook with retry settings
// (Done in Stripe dashboard, not code)
// Retry schedule:
// - Immediately
// - 5 minutes
// - 30 minutes
// - 2 hours
// - 12 hours
// - After 12 hours, retries end
// Maximum attempts: 5-10 (varies by provider)
Fix 5: Implement Webhook Queue Processing
Process webhooks asynchronously for reliability:
const Queue = require('bull');
const webhookQueue = new Queue('webhooks', {
redis: {
host: 'localhost',
port: 6379
}
});
// Webhook endpoint - receives and queues
app.post('/webhooks/endpoint', async (req, res) => {
const webhookId = req.headers['x-webhook-id'];
try {
// Add to queue immediately
await webhookQueue.add({
id: webhookId,
type: req.body.type,
payload: req.body,
receivedAt: Date.now()
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
// Respond quickly
res.status(200).json({ received: true });
} catch (err) {
console.error('Failed to queue webhook:', err);
res.status(500).json({ error: 'Failed to queue' });
}
});
// Queue processor - handles actual processing
webhookQueue.process(async (job) => {
const { id, type, payload } = job.data;
console.log(`Processing webhook ${id} (${type})`);
try {
// Process webhook based on type
switch (type) {
case 'order.created':
await handleOrderCreated(payload);
break;
case 'payment.succeeded':
await handlePaymentSucceeded(payload);
break;
default:
console.log(`Unknown webhook type: ${type}`);
}
console.log(`Webhook ${id} processed successfully`);
} catch (err) {
console.error(`Error processing webhook ${id}:`, err);
throw err; // Will retry
}
});
// Monitor queue
webhookQueue.on('failed', (job, err) => {
console.error(`Webhook job ${job.id} failed:`, err.message);
// Alert on repeated failures
});
webhookQueue.on('completed', (job) => {
console.log(`Webhook job ${job.id} completed`);
});
Fix 6: Validate Webhook Payload
Validate payload structure before processing:
const Joi = require('joi');
// Define expected payload schema
const orderWebhookSchema = Joi.object({
type: Joi.string().valid('order.created').required(),
data: Joi.object({
id: Joi.string().required(),
amount: Joi.number().positive().required(),
currency: Joi.string().length(3).required(),
customer: Joi.object({
email: Joi.string().email().required(),
name: Joi.string().required()
}).required()
}).required(),
created_at: Joi.date().iso().required()
});
app.post('/webhooks/orders', async (req, res) => {
try {
// Validate payload
const { error, value } = orderWebhookSchema.validate(req.body);
if (error) {
console.error('Invalid webhook payload:', error.details);
return res.status(400).json({
error: 'Invalid payload',
details: error.details
});
}
// Payload valid, process it
await processOrderWebhook(value);
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook processing error:', err);
res.status(500).json({ error: 'Processing failed' });
}
});
Platform-Specific Guides
Detailed webhook implementation for your specific platform:
Verification
After implementing fixes:
Test webhook delivery:
- Trigger test event in provider dashboard
- Verify webhook received
- Check logs for receipt
- Confirm processing completed
- Verify data updated correctly
Test signature verification:
- Valid signature accepted
- Invalid signature rejected (400)
- Missing signature rejected
- Check logs for verification attempts
Test retry behavior:
- Return 500 to trigger retry
- Verify retries happen
- Check retry timing
- Confirm eventual success
- Test retry exhaustion
Test idempotency:
- Send duplicate webhooks
- Verify only processed once
- No duplicate data created
- Check deduplication logs
-
- Send multiple webhooks rapidly
- Verify all processed
- Check for dropped webhooks
- Monitor queue depth
- Test under high load
Common Mistakes
- Slow response time - Respond within 10 seconds
- No signature verification - Always verify authenticity
- Synchronous processing - Use queues for heavy work
- No idempotency - Handle duplicate deliveries
- Wrong status codes - Return appropriate codes
- Invalid SSL certificate - Keep certificates current
- No error logging - Log all webhook failures
- Firewall blocking - Whitelist webhook IPs
- No retry handling - Expect and handle retries
- Missing payload validation - Validate structure
Troubleshooting Checklist
- Endpoint URL publicly accessible
- SSL certificate valid and current
- Webhook signature verification working
- Responding within 10 seconds
- Appropriate HTTP status codes
- Idempotency implemented
- Async processing for heavy work
- Error logging enabled
- Retry logic tested
- Payload validation implemented
- Queue monitoring set up
- Firewall rules configured
- Test webhooks working
Further Reading
- Webhook Best Practices
- Stripe Webhooks Guide
- Shopify Webhooks Documentation
- Building Reliable Webhooks
- Integration Issues Overview
- API Authentication