Architecting Idempotent Payment Webhooks in High-Latency Networks
When network jitter causes mobile telecommunications carriers in emerging markets to retry billing notifications multiple times in sub-second intervals, standard database locks frequently fail under peak concurrency.
Here is the architectural blueprint we implemented across distributed Node.js/Next.js services using Redis token buckets to guarantee zero duplicate charges, sub-50ms idempotency verification, and seamless transaction recovery.
The Problem: The Duplicate Charge Trap
In West African fintech integrations (Paystack, Flutterwave, Monnify, Interswitch), network timeouts frequently occur after a customer has been debited but before the merchant server receives acknowledgment.
When the aggregator's webhook delivery engine experiences a socket hangup, its automated retry mechanism fires up to five webhook events in rapid succession.
If your webhook handler performs:
- Read balance
- Check if transaction exists in PostgreSQL
- Insert ledger entry
- Credit user balance
A race condition occurs between steps 2 and 3 if two webhook requests are processed concurrently by separate server instances.
// The race condition vulnerability: const existing = await db.transactions.findUnique({ where: { ref } }); if (!existing) { // If request B arrives before request A finishes inserting, both execute! await db.transactions.create({ data: { ref, amount } }); await creditUserWallet(userId, amount); }
Solution: Atomic Distributed Locking with Redis NX
Instead of relying on database-level row locks which can cause thread pool exhaustion under heavy traffic, we place an atomic Redis lease at the edge of the API gateway.
export async function handlePaymentWebhook(req: Request) { const event = await req.json(); const txRef = event.data.reference; const lockKey = `lock:webhook:${txRef}`; // Atomic set if not exists with a 30s TTL const acquired = await redis.set(lockKey, "PROCESSING", "NX", "EX", 30); if (!acquired) { // Return 200 OK immediately so payment aggregator ceases retries return Response.json({ status: "deduped", message: "Event in flight" }, { status: 200 }); } try { await db.$transaction(async (tx) => { const recorded = await tx.ledger.findUnique({ where: { reference: txRef } }); if (recorded) return; await tx.ledger.create({ data: { reference: txRef, amount: event.data.amount, status: "SUCCESSFUL", }, }); await tx.wallet.update({ where: { userId: event.data.customer.id }, data: { balance: { increment: event.data.amount } }, }); }); // Mark permanent processed state await redis.set(`processed:${txRef}`, "TRUE", "EX", 60 * 60 * 24 * 7); // 7-day retention return Response.json({ status: "acknowledged" }); } finally { // Release atomic lock await redis.del(lockKey); } }
Key Metrics & Production Telemetry
After rolling out this idempotent pipeline across 4 production services:
- Zero Phantom Charges: 100% elimination of double-credit incidents.
- P99 Verification Latency: Dropped from 340ms (database lock contention) to 42ms.
- Retry Failure Resilience: Flawlessly handled 4x concurrent webhook bursts during peak payment spikes.

Victor Okeke
DISPATCH_WRITERPRODUCT & FULL-STACK ENGINEER // M.SC AI CANDIDATE
Building human-centered AI products at Nexenda, engineering resilient distributed applications, and actively mentoring early-career African engineering talent across Abuja and Lagos.
Working on something similar? Let's talk.
Available for technical architecture advisory, engineering team workshops, and distributed product systems consulting.
Book a 20-Min Architecture AuditRelated Dev Dispatches
View AllFundamentals First: Why Web Dev Rookies Should Learn to Code, Not Just Prompt
Jumping straight into AI-powered coding might feel fast, but without solid web fundamentals you risk becoming a surface-level developer who vibes without depth.
From Schlumberger Desktop Apps to Modern Web Engineering
How a Schlumberger technology program sparked my journey into C# desktop development, led me into entrepreneurship, and eventually transitioned me into modern web engineering.