python 110 lines · 3 tabs

Idempotent Webhook Ingestion With a Postgres Dedupe Store in FastAPI

Shared by codesnips Aug 2026
3 tabs
import datetime as dt

from sqlalchemy import Column, DateTime, Integer, String, UniqueConstraint
from sqlalchemy.orm import declarative_base

Base = declarative_base()


class WebhookDelivery(Base):
    __tablename__ = "webhook_deliveries"
    __table_args__ = (
        UniqueConstraint("dedupe_key", name="uq_webhook_dedupe_key"),
    )

    id = Column(Integer, primary_key=True)
    dedupe_key = Column(String(255), nullable=False)
    provider = Column(String(64), nullable=False)
    event_id = Column(String(255), nullable=False)
    status = Column(String(32), nullable=False, default="processing")
    received_at = Column(DateTime, default=dt.datetime.utcnow, nullable=False)
    completed_at = Column(DateTime, nullable=True)
3 files · python Explain with highlit

Webhook senders like Stripe or GitHub guarantee at-least-once delivery, which means the same event can arrive twice — after a network blip, a timeout, or a manual retry. Processing a duplicate can double-charge a customer or send two emails, so the receiver has to make handling idempotent. This snippet shows the classic pattern: a durable dedupe store keyed on the provider's event id, checked and inserted atomically before any side effects run.

In models.py, WebhookDelivery is the store — one row per unique delivery, with a dedupe_key under a UniqueConstraint. The uniqueness is enforced by the database, not application logic, which is what makes the check race-safe: two concurrent requests carrying the same key cannot both win the insert. The status column lets the system distinguish a delivery that is still processing from one that succeeded, so a retry that arrives while the first is mid-flight can be told to back off rather than re-run.

idempotency.py holds the core claim_delivery helper. It attempts an INSERT and relies on Postgres' ON CONFLICT (dedupe_key) DO NOTHING to detect an existing row without raising. If INSERT ... RETURNING yields no id, the key was already claimed, so the function returns the existing delivery instead. This turns a potential exception-driven flow into a single round trip. Marking the row succeeded via mark_succeeded happens only after the business logic commits.

webhooks.py wires it into a FastAPI route. verify_signature runs first — an unverified body must never touch the store, since an attacker could otherwise poison dedupe keys. The dedupe_key combines provider and event id so keys never collide across sources. When claim_delivery reports the event was already succeeded, the endpoint returns 200 immediately, which is exactly what the sender wants: a fast, successful ack that stops further retries. New events are handed to a background task so the HTTP response stays quick.

The main trade-off is storage growth — old rows should be pruned on a TTL — and the store must live in a shared, transactional database, not in-process memory, for the guarantee to hold across replicas.


Related snips

Share this code

Here's the card — post it anywhere.

Idempotent Webhook Ingestion With a Postgres Dedupe Store in FastAPI — share card
Link copied