Inbox pattern for exactly-once
When a webhook fires twice, your billing logic should not charge twice. The inbox pattern gives you exactly-once processing on top of at-least-once delivery by recording every inbound message id in a dedupe table before any side effect runs.
1.Capture the message id
Pull a stable id from every inbound event — the provider event id, the message id from your queue, or a hash of the payload if neither exists. This becomes the primary key in your inbox table.
CREATE TABLE inbox (
message_id TEXT PRIMARY KEY,
source TEXT NOT NULL,
received_at TIMESTAMPTZ DEFAULT NOW(),
processed_at TIMESTAMPTZ
);2.Insert before you act
Wrap the handler in a transaction. Attempt the insert first — if it raises a unique violation, the message is a duplicate and you return early. Only if the insert succeeds do you run the side effect (charge a card, send an email, write a row).
BEGIN;
INSERT INTO inbox (message_id, source)
VALUES ($1, 'stripe')
ON CONFLICT (message_id) DO NOTHING
RETURNING message_id;
-- if no row returned, this was a duplicate: ROLLBACK and return 200
-- else: run side effect, then UPDATE processed_at, COMMIT3.Keep the inbox bounded
The inbox grows forever if you let it. Pick a retention window longer than your upstream provider's retry horizon — 30 days covers Stripe, SQS, and most webhook senders. Run a nightly job that deletes rows where received_at is older than the window. Index received_at so the sweep stays cheap.