🛑 The Engineering Bottleneck & Failure Mode
SaaS billing architectures processing 50,000+ monthly active subscriptions frequently suffer from database split-brain scenarios caused by un-synchronized webhook consumption. When a user changes their subscription tier mid-cycle while an automatic renewal triggers simultaneously, standard read-committed database transactions permit race conditions. This leads to duplicate entitlement grants, missing invoices, orphaned stripe_customer_id references, and manual finance reconciliation overhead. Standard implementations fail to address concurrent execution paths and lack deterministic state ordering.
⚡ BuildDigital Reference Solution Architecture
Step 1: Cryptographic Signature Verification & Stream Ingestion
In Next.js 16 App Router, route handlers consume streams by default. To accurately verify the Stripe signature (`Stripe-Signature` header), we must buffer the raw HTTP request body as a text literal or Buffer before invocation of `stripe.webhooks.constructEvent`.
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { processWebhookEvent } from '@/lib/billing/dispatcher';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2025-01-27.acacia' });
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing Stripe signature header' }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
console.error(`[Webhook Auth Error] Invalid signature: ${err.message}`);
return NextResponse.json({ error: 'Invalid webhook signature' }, { status: 400 });
}
// Fast acknowledge: Enqueue for deterministic background processing
await processWebhookEvent(event);
return NextResponse.json({ received: true }, { status: 200 });
}Step 2: Distributed Redis SETNX Idempotency Locking
To eliminate race conditions from redundant Stripe retries, we extract `event.id` and acquire a 24-hour expiration lock on a Redis Cluster using atomic SETNX primitives. If the lock exists, ingestion terminates immediately as a successful no-op.
import { redis } from '@/lib/infra/redis';
export async function processWebhookEvent(event: Stripe.Event): Promise<boolean> {
const lockKey = `lock:stripe:webhook:${event.id}`;
const acquired = await redis.set(lockKey, 'LOCKED', 'EX', 86400, 'NX');
if (!acquired) {
console.warn(`[Idempotency Skip] Event ${event.id} is currently processing or completed.`);
return false;
}
try {
await dispatchSubscriptionUpdate(event);
await redis.set(`processed:stripe:webhook:${event.id}`, 'DONE', 'EX', 604800);
return true;
} catch (error) {
// Release lock on execution failure to permit Stripe exponential backoff retries
await redis.del(lockKey);
throw error;
}
}Step 3: Serializable PostgreSQL State Machine Mutability
To handle out-of-order events (such as `invoice.payment_succeeded` preceding subscription creation), database mutations are wrapped in a `SERIALIZABLE` transaction that validates the monotonically increasing event sequence numbers or epoch timestamps.
import { pool } from '@/lib/infra/db';
async function dispatchSubscriptionUpdate(event: Stripe.Event): Promise<void> {
const client = await pool.connect();
try {
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
const data = event.data.object as any;
const customerId = data.customer as string;
const eventTimestamp = event.created;
// Reject updates if a newer event timestamp has already reconciled this subscription
const existing = await client.query(
'SELECT last_event_timestamp, status FROM subscriptions WHERE stripe_customer_id = $1 FOR UPDATE',
[customerId]
);
if (existing.rows.length > 0 && existing.rows[0].last_event_timestamp >= eventTimestamp) {
console.info(`[Out-Of-Order Skip] Event ${event.id} timestamp is older than reconciled state.`);
await client.query('ROLLBACK');
return;
}
if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.created') {
await client.query(
`INSERT INTO subscriptions (stripe_customer_id, stripe_subscription_id, status, current_period_end, last_event_timestamp)
VALUES ($1, $2, $3, to_timestamp($4), $5)
ON CONFLICT (stripe_customer_id) DO UPDATE SET
stripe_subscription_id = EXCLUDED.stripe_subscription_id,
status = EXCLUDED.status,
current_period_end = EXCLUDED.current_period_end,
last_event_timestamp = EXCLUDED.last_event_timestamp,
updated_at = NOW()`,
[customerId, data.id, data.status, data.current_period_end, eventTimestamp]
);
}
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}