Webhook Patterns & Best Practices

Build reliable event-driven workflows. Learn proven patterns for webhook security, error handling, and scalability.

12 min readIntermediateLast updated: December 2024

6 Essential Webhook Patterns

Implement these patterns to build webhook integrations that are secure, reliable, and maintainable.

1

Signature Verification

Always verify webhook signatures to ensure requests come from legitimate sources. Most services sign payloads with HMAC-SHA256.

// Verify webhook signature
const signature = request.headers['x-signature'];
const payload = JSON.stringify(request.body);
const expected = crypto
  .createHmac('sha256', webhookSecret)
  .update(payload)
  .digest('hex');

if (signature !== expected) {
  throw new Error('Invalid signature');
}

Key Points:

  • Store webhook secrets in environment variables
  • Use constant-time comparison to prevent timing attacks
  • Log failed verification attempts for security monitoring
2

Idempotency Handling

Webhooks may be delivered multiple times. Use idempotency keys to ensure each event is processed only once.

// Check if event already processed
const eventId = request.body.id;
const processed = await db.webhookEvents.findOne({ eventId });

if (processed) {
  return { status: 'already_processed' };
}

// Process event and store ID
await processEvent(request.body);
await db.webhookEvents.create({ eventId, processedAt: new Date() });

Key Points:

  • Store event IDs with TTL for automatic cleanup
  • Return 200 even for duplicate events to prevent retries
  • Include idempotency key in your processing logic
3

Async Processing

Respond quickly to webhook requests (within 30 seconds) by queuing work for background processing.

// Respond immediately, process later
app.post('/webhook', async (req, res) => {
  // Validate quickly
  if (!isValidSignature(req)) {
    return res.status(401).send('Invalid');
  }

  // Queue for background processing
  await queue.add('process-webhook', {
    payload: req.body,
    receivedAt: Date.now()
  });

  // Respond immediately
  res.status(200).send('Accepted');
});

Key Points:

  • Most services timeout after 30 seconds
  • Use message queues (Redis, SQS, etc.) for reliability
  • Implement dead letter queues for failed jobs
4

Payload Validation

Validate webhook payloads against expected schemas to catch malformed data early and prevent downstream errors.

// Validate payload structure
const schema = z.object({
  event: z.enum(['created', 'updated', 'deleted']),
  data: z.object({
    id: z.string(),
    timestamp: z.string().datetime(),
    attributes: z.record(z.unknown())
  })
});

const result = schema.safeParse(request.body);
if (!result.success) {
  console.error('Invalid payload:', result.error);
  return res.status(400).send('Invalid payload');
}

Key Points:

  • Use Zod, Joi, or similar for schema validation
  • Log validation failures for debugging
  • Be lenient with unknown fields for forward compatibility
5

Retry Logic & Backoff

When sending webhooks, implement exponential backoff to handle temporary failures gracefully.

// Exponential backoff for retries
async function sendWithRetry(url, payload, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: { 'Content-Type': 'application/json' }
      });

      if (response.ok) return response;
      if (response.status < 500) throw new Error('Client error');
    } catch (error) {
      const delay = Math.pow(2, attempt) * 1000;
      await sleep(delay);
    }
  }
  throw new Error('Max retries exceeded');
}

Key Points:

  • Start with short delays (1s, 2s, 4s, 8s, 16s)
  • Add jitter to prevent thundering herd
  • Set maximum retry limits and alert on failures
6

Secret Rotation

Rotate webhook secrets periodically. Support multiple active secrets during rotation windows.

// Support multiple secrets during rotation
const secrets = [
  process.env.WEBHOOK_SECRET_CURRENT,
  process.env.WEBHOOK_SECRET_PREVIOUS
].filter(Boolean);

function verifySignature(payload, signature) {
  for (const secret of secrets) {
    const expected = computeSignature(payload, secret);
    if (timingSafeEqual(signature, expected)) {
      return true;
    }
  }
  return false;
}

Key Points:

  • Rotate secrets every 90 days minimum
  • Support 2 active secrets during transition
  • Automate rotation with secret management tools

Common Webhook Use Cases

Real-world scenarios where webhooks power critical automations.

Payment Processing

Handle Stripe/PayPal payment events

payment.succeededpayment.failedrefund.created

CI/CD Pipelines

Trigger builds on code pushes

pushpull_requestdeployment

CRM Sync

Keep systems in sync on contact changes

contact.createddeal.updatedactivity.logged

E-commerce

React to order lifecycle events

order.placedorder.shippedinventory.low

Frequently Asked Questions

Common questions about webhook implementations.

Ready to Build Event-Driven Workflows?

Create webhook-triggered automations with visual debugging and built-in best practices.