Delivery & Retries#

HookRelay ensures reliable webhook delivery through automatic retries and intelligent retry policies. This guide explains how webhooks are delivered to your Next.js application and how failures are handled.

How Delivery Works#

When a webhook provider (like Stripe) sends an event to HookRelay, here’s what happens:

1. Receipt & Storage#

The webhook is received by HookRelay and immediately stored in the database. This ensures the event is never lost, even if your application is temporarily unavailable.

2. Validation#

If you’ve configured a Provider Secret, HookRelay verifies the webhook signature to ensure it actually came from your provider (e.g., Stripe). Invalid signatures are rejected and not forwarded.

3. Queueing#

The event is queued for delivery to your Next.js application. HookRelay maintains a queue of events waiting to be delivered.

4. Forwarding#

HookRelay sends an HTTP POST request to your Forward URL (your Next.js API route) with:

  • The webhook payload in the request body (as JSON)
  • Special headers for identification and verification
  • A 30-second timeout

5. Response Evaluation#

Your Next.js handler responds with an HTTP status code. HookRelay evaluates this response:

  • 2xx status codes → Success! Event is marked as succeeded
  • 4xx status codes → Client error, typically not retried
  • 5xx status codes → Server error, will be retried
  • Timeout → No response within 30 seconds, will be retried
  • Connection error → Network/DNS issues, will be retried

6. Retry or Complete#

Based on the response:

  • Success → Event is marked complete, no further action
  • Retryable failure → Event is scheduled for retry with exponential backoff
  • Non-retryable failure → Event is marked as failed immediately

Success Criteria#

A delivery is considered successful when your Next.js handler:

  1. Returns a 2xx status code (200, 201, 202, 204, etc.)
  2. Responds within 30 seconds
  3. Doesn’t throw an unhandled exception

Example Successful Handler#

// app/api/webhooks/stripe/route.ts
import { withHookRelay } from '@hookrelay/next';

export const POST = withHookRelay(
  async (event) => {
    // Process the webhook
    await processStripeEvent(event.payload);
    
    // Return 202 Accepted (or any 2xx)
    // The wrapper handles this automatically
  },
  { provider: 'stripe' }
);

The withHookRelay wrapper automatically returns 202 Accepted on success, so you don’t need to return a response manually.

Retry Behavior#

When a delivery fails, HookRelay automatically retries with exponential backoff. This means the wait time between retries increases exponentially.

Default Retry Schedule#

AttemptWait TimeTotal Time Since First Attempt
1Immediate0 seconds
21 minute1 minute
35 minutes6 minutes
415 minutes21 minutes
51 hour1 hour 21 minutes

Note: After 5 failed attempts, the event is marked as failed with reason retry_limit_exceeded. You can manually replay it from the dashboard.

Why Exponential Backoff?#

Exponential backoff helps in several ways:

  • Temporary issues - Gives your application time to recover from brief outages
  • Rate limiting - Avoids overwhelming your server with rapid retries
  • Resource conservation - Reduces unnecessary load during extended outages

Retry Triggers#

HookRelay automatically retries when these conditions occur:

1. Response Timeout#

Your handler didn’t respond within 30 seconds.

Common causes:

  • Long-running database queries
  • Slow external API calls
  • Heavy processing in the handler

Solution: Return quickly and process asynchronously:

export const POST = withHookRelay(
  async (event) => {
    // Queue for background processing
    await queueJob(event.payload);
    // Handler returns immediately
  },
  { provider: 'stripe' }
);

2. Server Errors (5xx)#

Your handler returned a 500, 502, 503, or other 5xx status code.

Common causes:

  • Unhandled exceptions
  • Database connection errors
  • Internal server errors

Solution: Fix the underlying issue. Check your application logs.**

3. Connection Errors#

Network-level failures prevented delivery.

Common causes:

  • DNS resolution failures
  • Connection refused (server down)
  • Network timeouts
  • SSL/TLS errors

Solution: Ensure your application is running and accessible via HTTPS.

Non-Retryable Failures#

Some failures are not automatically retried because they indicate a permanent issue:

4xx Client Errors#

  • 400 Bad Request - Invalid request format
  • 401 Unauthorized - Authentication failed
  • 403 Forbidden - Access denied
  • 404 Not Found - Endpoint doesn’t exist

These typically indicate a configuration problem that won’t be fixed by retrying.

Exception: 429 Too Many Requests is retried (after a delay) because it’s a temporary rate limit.

Invalid Configuration#

  • Endpoint not found
  • Invalid Forward URL
  • Signature verification failures

These require manual intervention to fix.

Delivery Attempts#

Every delivery attempt is tracked in detail. You can view the complete history in the event detail page:

Attempt Information#

  • Attempt Number - Sequential number (1, 2, 3, etc.)
  • Status - succeeded, failed, or pending
  • Response Status - HTTP status code from your handler
  • Duration - How long the request took (milliseconds)
  • Started At - Timestamp of when the attempt began
  • Failure Reason - Classification if the attempt failed (see Failure Classification)

Example Attempt History#

Attempt 1: Failed - handler_timeout (30,000ms)
  → Your handler took too long to respond

Attempt 2: Failed - handler_non_2xx (500 Internal Server Error)
  → Your handler returned a 500 error

Attempt 3: Succeeded - 202 Accepted (150ms)
  → Success! Event processed correctly

Headers Sent to Your Endpoint#

When HookRelay forwards a webhook to your Next.js handler, it includes these headers:

Content-Type: application/json
X-HookRelay-Event-Id: hr_abc123def456...
X-HookRelay-Provider: stripe
X-HookRelay-Attempt: 1
X-HookRelay-Replayed: false
X-HookRelay-Timestamp: 1699123456
X-HookRelay-Signature: sha256=abc123...

Header Reference#

  • X-HookRelay-Event-Id - Unique HookRelay event ID
  • X-HookRelay-Provider - Provider name (stripe, github, etc.)
  • X-HookRelay-Attempt - Delivery attempt number (1-based)
  • X-HookRelay-Replayed - Whether this is a replayed event (true or false)
  • X-HookRelay-Timestamp - Unix timestamp when the request was sent
  • X-HookRelay-Signature - HMAC SHA-256 signature for verification

Accessing Headers in Your Handler#

The withHookRelay wrapper automatically extracts and validates these headers. You can access event metadata through the event object:

export const POST = withHookRelay(
  async (event) => {
    console.log(`Event ID: ${event.id}`);
    console.log(`Attempt: ${event.attempt}`);
    console.log(`Replayed: ${event.replayed}`);
    // ... your handler logic
  },
  { provider: 'stripe' }
);

Verifying HookRelay Signatures#

The X-HookRelay-Signature header contains an HMAC SHA-256 signature that proves the webhook came from HookRelay. The withHookRelay wrapper verifies this automatically, but here’s how it works:

Signature Format#

The signature is in the format: sha256=<hex-encoded-hmac>

Verification Process#

  1. Extract the timestamp from X-HookRelay-Timestamp
  2. Get the raw request body
  3. Create a signed payload: ${timestamp}.${body}
  4. Compute HMAC SHA-256 using your HookRelay Secret
  5. Compare with the signature in the header

If you’re not using withHookRelay, you can verify manually:

import crypto from 'crypto';

function verifySignature(
  payload: string,
  timestamp: string,
  signature: string,
  secret: string
): boolean {
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  return signature === `sha256=${expectedSignature}`;
}

However, we strongly recommend using withHookRelay which handles this automatically.


Best Practices for Reliable Delivery#

1. Respond Quickly#

Your handler should return a 2xx status code as quickly as possible. For long-running operations, process asynchronously:

export const POST = withHookRelay(
  async (event) => {
    // Queue for background processing
    await queueOrderFulfillment(event.payload);
    // Returns immediately (handler completes in < 1 second)
  },
  { provider: 'stripe' }
);

2. Use Idempotency#

Handle duplicate deliveries gracefully using the event ID:

export const POST = withHookRelay(
  async (event) => {
    // Check if already processed
    const processed = await db.events.findUnique({
      where: { hookRelayEventId: event.id }
    });
    
    if (processed) {
      console.log(`Event ${event.id} already processed`);
      return; // Idempotent - safe to skip
    }
    
    // Process the event
    await processEvent(event.payload);
    
    // Mark as processed
    await db.events.create({
      data: { hookRelayEventId: event.id, ... }
    });
  },
  { provider: 'stripe' }
);

3. Handle Errors Gracefully#

Catch and handle errors appropriately:

export const POST = withHookRelay(
  async (event) => {
    try {
      await processEvent(event.payload);
    } catch (error) {
      // Log the error
      console.error('Failed to process event:', error);
      
      // Re-throw to trigger retry, or handle gracefully
      if (isRetryableError(error)) {
        throw error; // Will trigger retry
      } else {
        // Non-retryable - log and continue
        await logError(event.id, error);
      }
    }
  },
  { provider: 'stripe' }
);

4. Monitor Delivery Status#

Regularly check the HookRelay dashboard for:

  • Failed events that need attention
  • High retry rates (indicates issues)
  • Slow response times

5. Test with Replay#

Use the replay feature to test your handler:

  • Fix a bug in your handler
  • Replay a failed event to verify the fix
  • Ensure idempotency works correctly

6. Set Up Alerts#

Configure alerts for:

  • High failure rates
  • Events stuck in retrying state
  • Repeated failures for the same endpoint

Retry Flow Visualization#

Delivery & Retry Flow

The diagram above illustrates the exponential backoff retry schedule. Failed deliveries are automatically retried with increasing wait times between attempts. If an attempt succeeds at any point, the event is marked as succeeded and no further retries occur.


Understanding Delivery vs Execution#

It’s important to understand the difference:

  • Delivery Success - HookRelay successfully sent the webhook to your handler
  • Execution Success - Your handler successfully processed the webhook

HookRelay tracks both:

  • If delivery fails → HookRelay retries automatically
  • If execution fails → Your handler throws an error, HookRelay retries delivery

The withHookRelay wrapper reports execution outcomes back to HookRelay, so you can see both delivery and execution status in the dashboard.

Timeout Considerations#

HookRelay has a 30-second timeout for delivery. This means:

  • Your handler must respond within 30 seconds
  • If it takes longer, the delivery is considered failed and will be retried

For Next.js on Vercel:

  • Serverless functions have their own timeouts (10s for Hobby, 60s for Pro)
  • Plan your handler execution time accordingly
  • Use background jobs for long-running operations

Common Questions#

Why did my event retry even though my handler succeeded?#

This shouldn’t happen. If you see this, check:

  • Your handler is actually returning a 2xx status code
  • The withHookRelay wrapper is being used correctly
  • There are no network issues between HookRelay and your app

Can I change the retry schedule?#

The retry schedule is fixed for now. If you need different behavior, contact support.

What happens if my app is down for hours?#

HookRelay will continue retrying according to the schedule. After 5 attempts (about 1.5 hours), the event will be marked as failed. You can replay it once your app is back online.

How do I know if a retry will succeed?#

Check the failure reason in the event details. If it’s a temporary issue (timeout, 5xx), retries will likely succeed. If it’s a permanent issue (4xx, configuration error), you need to fix the underlying problem first.