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
- Sign up free — 500 requests/month included
- Change your webhook URL from
https://your-app.com/webhooktohttps://zeplo.to/https://your-app.com/webhook?_retry=3&_token=YOUR_TOKEN - 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 →