Building a Reliable Webhook Queue for Serverless Applications

Serverless functions (AWS Lambda, Vercel, Cloudflare Workers) are great for handling webhooks — until they aren’t. Cold starts, timeout limits, concurrent execution caps, and the lack of persistent state make reliable webhook processing surprisingly hard.

Here’s how to add a queue layer in front of your serverless functions without managing any infrastructure.

The Serverless Webhook Problem

Consider a typical Stripe webhook handler on Vercel:

// /api/webhooks/stripe.js
export default async function handler(req, res) {
  const event = req.body;
  
  switch (event.type) {
    case 'invoice.paid':
      await updateSubscription(event.data);
      await sendReceiptEmail(event.data);
      await syncToAccounting(event.data);
      break;
  }
  
  res.status(200).json({ received: true });
}

This works until: - Vercel times out (10s on Hobby, 60s on Pro) and Stripe marks the webhook as failed - Your email provider is down and the entire handler fails — Stripe retries the whole thing - You hit Lambda concurrency limits during a traffic spike and webhooks get dropped - Cold starts push response times past the webhook sender’s timeout threshold

The Queue Pattern

Instead of processing webhooks inline, queue them for reliable async processing:

Stripe → Zeplo Queue → Your Serverless Function

Point Stripe’s webhook URL at Zeplo, which queues the request and forwards it to your function with retry:

Stripe webhook URL:

https://zeplo.to/https://your-app.vercel.app/api/webhooks/stripe?_retry=5&_token=YOUR_TOKEN

Now: - Stripe gets an immediate 202 Accepted — no timeout risk - If your function fails, Zeplo retries with exponential backoff - If your function is cold-starting, Zeplo waits for the response - Every delivery attempt is logged with full request/response details

Separating Concerns With Multiple Queues

The inline handler above does three things: update subscription, send email, sync accounting. If any one fails, they all fail together.

Queue them separately:

// /api/webhooks/stripe.js — thin dispatcher
export default async function handler(req, res) {
  const event = req.body;
  const token = process.env.ZEPLO_TOKEN;
  
  if (event.type === 'invoice.paid') {
    // Fan out to separate handlers via Zeplo
    await Promise.all([
      fetch(`https://zeplo.to/https://your-app.vercel.app/api/jobs/update-subscription?_retry=3&_token=${token}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event.data),
      }),
      fetch(`https://zeplo.to/https://your-app.vercel.app/api/jobs/send-receipt?_retry=5&_token=${token}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event.data),
      }),
      fetch(`https://zeplo.to/https://your-app.vercel.app/api/jobs/sync-accounting?_retry=3&_token=${token}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event.data),
      }),
    ]);
  }
  
  res.status(200).json({ received: true });
}

Now each job retries independently. If SendGrid is down, your subscription update and accounting sync still succeed.

Delayed Processing

Some webhook workflows need delays. Send a follow-up email 24 hours after a purchase:

// Queue a delayed request
await fetch(
  `https://zeplo.to/https://your-app.vercel.app/api/jobs/follow-up-email?_delay=86400&_retry=3&_token=${token}`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ customer_id: event.data.customer }),
  }
);

Zeplo holds the request for 24 hours, then delivers it with retry. No database timers, no cron jobs, no scheduler infrastructure.

Monitoring and Debugging

Every queued request shows up in the Zeplo dashboard with:

  • Full request log — headers, body, URL for each delivery attempt
  • Response details — status code, response body, timing
  • Retry history — which attempts failed and why
  • Manual retry — re-deliver any failed webhook with one click

When a webhook fails at 3 AM, you don’t need to reconstruct what happened from logs. Open the dashboard, see the exact request and response, fix the issue, and click retry.

When This Pattern Makes Sense

Good fit: - Webhook handlers that call external services (email, payments, APIs) - Functions that exceed serverless timeout limits - Workloads with traffic spikes that hit concurrency limits - Any webhook where you need retry without building a queue

Probably overkill: - Simple webhook handlers that just write to a database - Internal service-to-service calls with stable latency - High-throughput stream processing (use Kafka/Kinesis instead)

Getting Started

  1. Sign up free — 500 requests/month included
  2. Change your webhook URL from https://your-app.com/webhook to https://zeplo.to/https://your-app.com/webhook?_retry=3&_token=YOUR_TOKEN
  3. Your webhook provider gets instant acknowledgment, your function gets reliable delivery

No queues to provision, no workers to scale, no dead letter queues to monitor.


Zeplo is a managed HTTP request queue for developers. Add retry, delay, and scheduling to any webhook or API call. Try it free →

Return to Blog