RevClamp Enterprise B2B REVENUE ASSURANCE Start Free
REVCLAMP ENGINEERING DISPATCH
← Back to Engineering Blog • DISTRIBUTED SYSTEMS & WEBHOOK FSM • 4 min read

Why out-of-order Stripe & Shopify webhooks resurrect canceled subscriptions — and how to build a 72h monotonic state machine

Verifying Stripe-Signature, X-Shopify-Hmac-Sha256, or X-RevClamp-Signature over the raw UTF-8 request body proves that an incoming webhook was genuinely emitted by the provider within your 300-second transport window. What cryptographic signature verification cannot guarantee is that two valid webhooks emitted 50 seconds apart will arrive at your application servers in chronological order.

In distributed billing pipelines across Stripe (2025-03-31.basil), Shopify Plus B2B, and Lemon Squeezy, webhook delivery queues provide at-least-once delivery, never in-order or exactly-once delivery. If your webhook handler blindly updates your database row using whatever valid event arrives last, a single worker stall or HTTP 502 retry on an earlier customer.subscription.updated (status: "active") event will arrive after customer.subscription.deleted (status: "canceled") and silently resurrect a canceled account into active paid access.

72-Hour Webhook Idempotency Set and Monotonic createdSec State Machine Pipeline Four-stage webhook state machine pipeline: Gate 1 verifies raw UTF-8 HMAC-SHA256, Gate 2 enforces a 72-hour (259,200s) horizon, Gate 3 deduplicates seen eventId entries, and Gate 4 enforces monotonic createdSec >= lastAppliedCreatedSec watermarks. 4-GATE WEBHOOK IDEMPOTENCY & MONOTONIC WATERMARK STATE MACHINE 1. TRANSPORT HMAC Raw UTF-8 HMAC-SHA256 crypto.timingSafeEqual |nowSec - t| <= 300s 2. 72H HORIZON BOUND Window: 259,200 seconds nowSec - createdSec <= 72h Drops ancient replays 3. EVENT ID DEDUP SET seenEventIds.has(eventId) SQLite WAL unique key 200 OK idempotent no-op 4. MONOTONIC WATERMARK createdSec >= watermark Rejects stale regressions Zero zombie subscriptions

1. Anatomy of the “Zombie Subscription” race condition

Consider what happens when a B2B customer updates a billing seat or payment method at t = 1760000950 and then immediately cancels their subscription at t = 1760001000:

2. Five webhook ordering & retry failure modes across B2B billing engines

To prevent both duplicate side-effects and out-of-order state regressions, every stateful webhook consumer must separate HTTP transport freshness (the header signature timestamp) from domain state monotonicity (the event's logical creation watermark):

Webhook Delivery Anomaly Provider Queue Behavior Naive Overwrite-on-Arrival Bug RevClamp Monotonic FSM Guard
1. Out-of-Order Update After Cancel Earlier active update (t=0950) arrives after canceled delete (t=1000) Overwrites canceled back to active (RESURRECTED_CANCELED_SUBSCRIPTION) Drops state regression when createdSec < lastAppliedCreatedSec (STALE_OUT_OF_ORDER_TRANSITION_IGNORED)
2. Duplicate At-Least-Once Retry Identical event_id delivered 2–5 times after transient network timeout Emits duplicate revoke/restore webhooks, emails, or ledger credits Records eventId in 72h SQLite WAL deduplication table (DUPLICATE_EVENT_IDEMPOTENT_NOOP)
3. Stale Tier Downgrade After Upgrade Delayed starter_19 event arrives after customer upgraded to growth_59 Clamps paid Growth customer down to Starter quota (STALE_TIER_OVERWRITE) Preserves growth_59 watermark and logs ignored stale event for audit trail
4. Ancient Dead-Letter Queue Replay Manual or queue replay of an event older than 72 hours (259,200s) Applies historical state snapshot over current billing cycle Rejects events where nowSec - createdSec > idempotencyWindowSec (EXPIRED_OUTSIDE_72H_DEDUP_WINDOW)
5. Cancel With Future Paid Period subscription_cancelled with future ends_at vs. terminal expired Either revokes paid access early or grants free access to expired accounts Checks terminal expired/unpaid first, then holds cancel_at_period_end until currentPeriodEndSec

3. Atomic SQLite WAL / PostgreSQL CAS watermark pattern & live simulation

In production, your database update should enforce the monotonic watermark atomically inside a single compare-and-swap (CAS) statement so concurrent webhook workers cannot race between a SELECT and an UPDATE:

// Atomic monotonic watermark guard (only advances when incoming createdSec >= stored transition_seq)
await db.tenants.updateMany({
  where: {
    stripeCustomerId: payload.customer_id,
    transitionSeq: { lte: payload.created_sec }
  },
  data: {
    billingStatus: payload.subscription_status,
    tier: payload.tier,
    transitionSeq: payload.created_sec
  }
});

You can test any 1–10 event arrival sequence against both our 72-hour deduplication Set and monotonic lastAppliedCreatedSec watermark without an API key via our interactive browser simulator (/tools/webhook-replay-simulator), our 8th Model Context Protocol tool (revclamp_simulate_webhook_replay on POST https://api.revclamp.com/api/mcp), or a single CLI command:

# 1. One-shot CLI simulation of the out-of-order cancel-then-stale-update trap
npx -y github:lhphong09t2/revclamp-mcp --sim-replay out_of_order_stale_update_after_cancel

# 2. JSON-RPC 2.0 MCP tools/call payload (POST https://api.revclamp.com/api/mcp)
{
  "jsonrpc": "2.0",
  "id": "webhook-fsm-sim-1",
  "method": "tools/call",
  "params": {
    "name": "revclamp_simulate_webhook_replay",
    "arguments": {
      "scenarioPreset": "out_of_order_stale_update_after_cancel",
      "idempotencyWindowSec": 259200
    }
  }
}
RevClamp Enterprise -- B2B Revenue & Compliance Guardrails
Open Free Webhook Replay Simulator → Test Raw-Body HMAC-SHA256 Signatures View 12-Line Next.js / Express Guard Test Live in MCP Playground Start Free Workspace (5,000 events/mo)