Webhooks are the backbone of modern integrations. When Stripe processes a payment, GitHub receives a push, or Shopify gets an order, they send webhooks to your application. The challenge? Handling them reliably at scale.
flashQ transforms webhook handling from a fragile synchronous process into a robust, event-driven architecture.
The Webhook Problem
Typical webhook issues:
- Timeouts: Webhook providers expect fast responses (2-30 seconds)
- Retries: Providers retry failed webhooks, causing duplicate processing
- Order: Events can arrive out of order
- Scale: Traffic spikes during peak times
The flashQ Solution
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Webhook β β flashQ β β Worker β
β Provider ββββββΆβ (Enqueue) ββββββΆβ (Process) β
β β β < 100ms β β Async β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β
β Fast response β Reliable processing
ββββββββββββββββββββββ
Handling Stripe Webhooks
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { Queue } from 'flashq';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const queue = new Queue('stripe-webhooks', { useHttp: true });
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
// Verify webhook signature
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Enqueue for async processing (fast response to Stripe)
await queue.add(event.type, event, {
jobId: event.id, // Idempotency - prevents duplicate processing
});
// Respond immediately (< 100ms)
return NextResponse.json({ received: true });
}
Stripe Worker
// workers/stripe.ts
import { Worker } from 'flashq';
new Worker('stripe-webhooks', async (job) => {
const event = job.data;
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
case 'invoice.paid':
await handleInvoicePaid(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
}, {
concurrency: 10,
});
async function handleCheckoutComplete(session) {
// Create user account
await db.users.create({
email: session.customer_email,
stripeCustomerId: session.customer,
});
// Send welcome email
await emailQueue.add('welcome', {
to: session.customer_email,
});
}
GitHub Webhooks
// app/api/webhooks/github/route.ts
import crypto from 'crypto';
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('x-hub-signature-256');
const event = request.headers.get('x-github-event');
const deliveryId = request.headers.get('x-github-delivery');
// Verify signature
const expected = `sha256=${crypto
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!)
.update(body)
.digest('hex')}`;
if (signature !== expected) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Enqueue
await queue.add(event!, JSON.parse(body), {
jobId: deliveryId!, // Idempotency
});
return NextResponse.json({ received: true });
}
Sending Outgoing Webhooks
flashQ can also send webhooks to your users:
// Send webhook when job completes
worker.on('completed', async (job, result) => {
if (job.data.webhookUrl) {
await webhookQueue.add('send', {
url: job.data.webhookUrl,
event: 'job.completed',
payload: { jobId: job.id, result },
}, {
attempts: 5,
backoff: { type: 'exponential', delay: 1000 },
});
}
});
// Webhook sender worker
new Worker('outgoing-webhooks', async (job) => {
const { url, event, payload } = job.data;
const signature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(payload))
.digest('hex');
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': event,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Webhook failed: ${response.status}`);
}
});
Event-Driven Patterns
Fan-Out Pattern
// One event triggers multiple actions
new Worker('events', async (job) => {
if (job.name === 'user.created') {
// Fan out to multiple queues
await Promise.all([
emailQueue.add('welcome', job.data),
analyticsQueue.add('track', { event: 'signup', ...job.data }),
slackQueue.add('notify', { message: `New user: ${job.data.email}` }),
]);
}
});
Saga Pattern
// Multi-step workflow with compensation
async function processOrder(orderId) {
const steps = [];
try {
// Step 1: Reserve inventory
steps.push(await inventoryQueue.add('reserve', { orderId }));
await queue.finished(steps[0].id);
// Step 2: Process payment
steps.push(await paymentQueue.add('charge', { orderId }));
await queue.finished(steps[1].id);
// Step 3: Ship order
steps.push(await shippingQueue.add('ship', { orderId }));
await queue.finished(steps[2].id);
} catch (error) {
// Compensation: rollback in reverse order
for (const step of steps.reverse()) {
await queue.add('compensate', { stepId: step.id });
}
}
}
π‘ Pro Tip
Always use webhook event IDs as job IDs for idempotency. This prevents duplicate processing when providers retry failed deliveries.
Conclusion
flashQ transforms webhook handling from a synchronous bottleneck into a scalable, event-driven architecture. Key benefits:
- Fast responses: Acknowledge webhooks immediately
- Reliability: Automatic retries with backoff
- Idempotency: Prevent duplicate processing
- Scalability: Handle traffic spikes gracefully