Webhook Retry Logic

The payment gateway implements automatic retry logic for failed webhook deliveries with exponential backoff.

Retry Schedule

Webhooks are retried up to 7 times with increasing delays:

AttemptDelayTime After First Attempt
1Immediate0 seconds
21 minute1 minute
310 minutes11 minutes
41 hour1 hour 11 minutes
53 hours4 hours 11 minutes
612 hours16 hours 11 minutes
724 hours40 hours 11 minutes

After 7 failed attempts over approximately 40 hours, the webhook is marked as failed.

Failure Conditions

Webhooks are retried when:

  • HTTP Status: Any non-2xx status code
  • Timeout: Response not received within 10 seconds
  • Connection Error: Unable to establish connection

Success Conditions

A webhook is considered successful when:

  • HTTP status code 200-299 is returned
  • Response received within the 10-second timeout

Handling Retries

Idempotent Processing

Since the same event may be delivered multiple times, always process events idempotently:

async function processWebhook(event) {
  const paymentId = event.payment.id;
  const status = event.payment.status;

  // Check if already processed
  const existing = await db.webhookEvents.findOne({
    paymentId,
    status
  });

  if (existing) {
    console.log(`Event already processed: ${paymentId}:${status}`);
    return { status: 'already_processed' };
  }

  // Process event
  await handlePaymentEvent(event);

  // Mark as processed
  await db.webhookEvents.insert({
    paymentId,
    status,
    processedAt: new Date()
  });

  return { status: 'success' };
}

Quick Response

Return a 200 response as quickly as possible. If your processing takes time, handle it asynchronously:

app.post('/webhooks/payments', (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Process asynchronously
  processWebhookAsync(req.body).catch((error) => {
    console.error('Webhook processing error:', error);
  });
});

Permanent Errors

If the error is permanent (invalid data, business logic rejection), return 200 to stop retries and log the issue:

async function handleWebhook(event) {
  try {
    await processEvent(event);
    return { status: 'success' };
  } catch (error) {
    if (isPermanentError(error)) {
      console.error('Permanent webhook error, stopping retries:', error);
      await logPermanentFailure(event, error);
      return { status: 'permanent_failure' }; // Still return 200
    }
    throw error; // Temporary error — trigger retry
  }
}

Best Practices

  1. Implement idempotency — process events only once
  2. Return 200 quickly — process asynchronously if needed
  3. Log all attempts — for debugging retry issues
  4. Handle permanent failures — return 200 to stop retries for invalid data
  5. Monitor failure rates — set up alerts for repeated failures

Next Steps