Best Practices

Responding to webhooks

Quick acknowledgment

Respond with a 2xx status quickly, preferably within a few seconds. Extend times out after 30 seconds.

1app.post('/webhook', async (req, res) => {
2 if (!verifyWebhookSignature(req)) {
3 return res.status(401).send('Invalid signature');
4 }
5 await messageQueue.send({
6 type: 'webhook-event',
7 payload: req.body
8 });
9 res.status(200).send('OK');
10});

Asynchronous processing

Queue tasks that are slow, depend on external services, or may need retries.

1// ❌ Bad: Synchronous work can cause timeouts and duplicates
2app.post('/webhook', async (req, res) => {
3 const event = req.body;
4
5 await sendToMultipleAPIs(event.data);
6 await generatePDFReport(event.data);
7 await enrichDataFromThirdParty(event.data);
8 await sendEmailNotifications(event.data);
9
10 res.status(200).send('OK');
11});
12
13// ✅ Good: Enqueue then respond
14app.post('/webhook', async (req, res) => {
15 if (!verifyWebhookSignature(req)) {
16 return res.status(401).send('Invalid signature');
17 }
18 const event = req.body;
19 if (!isValidEvent(event)) {
20 return res.status(400).send('Invalid event');
21 }
22 await jobQueue.add('process-webhook', event);
23 res.status(200).send('OK');
24});

Handling duplicates and retries

Idempotency with event IDs

Extend tries to minimize duplicate requests, but occasionally they are unavoidable. If your side effects are not idempotent, you can use the eventId (e.g., event_abc123) to avoid processing the same event multiple times.

1async function processWebhook(event) {
2 const eventId = event.id;
3
4 if (await redis.get(`processed:${eventId}`)) return;
5
6 await handleEvent(event);
7
8 await redis.setex(`processed:${eventId}`, 86400 * 7, 'true');
9}

Error handling and reliability

Retry strategy

Extend retries failed or timed-out (30 s) requests with exponential backoff:

1app.post('/webhook', async (req, res) => {
2 try {
3 if (!verifyWebhookSignature(req)) {
4 return res.status(401).send('Invalid signature');
5 }
6
7 const queued = await messageQueue.send(req.body);
8 if (!queued.success) {
9 return res.status(503).send('Service temporarily unavailable');
10 }
11
12 res.status(200).send('OK');
13 } catch {
14 res.status(500).send('Internal server error');
15 }
16});

Security considerations

Always verify signatures

Always verify the webhook signature. See the signature verification guide.