Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To prevent duplicate webhook work, use the provider’s stable event ID as an idempotency key, claim it atomically, and make PostgreSQL’s unique constraint the durable safeguard. Put the claim and database-backed business changes in one transaction. Redis can speed up repeat checks, but it should not be the only record. Acknowledge confirmed duplicates successfully, and protect external effects with their own idempotency or state-aware operations.

Why webhook handlers need idempotency

Webhook senders may deliver an event more than once. A handler can finish its work while its response is lost or delayed, leaving the sender unsure whether to retry. Network failures and concurrent deliveries create the same practical problem: the same event can reach your application again.

Idempotency means repeated deliveries of one event do not repeat its business effect. It does not mean ignoring every request that looks similar. The handler needs a reliable identity for the event and an atomic way to record that identity.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a stable key and scope it correctly

After authenticating the request according to the provider’s requirements, extract the provider’s stable event or delivery ID. Do not use a received timestamp or a locally generated request ID: those can change between delivery attempts and therefore fail to identify the same event.

Scope the key by provider and, if event namespaces overlap, by tenant or endpoint. For example, a key can combine a provider name and its event ID, with additional scope where needed. The important property is that two deliveries of the same event map to the same key, while unrelated providers or tenants do not collide.

Use PostgreSQL as the durable correctness gate

For database-backed business work, a uniquely constrained processed-events table provides a durable, concurrency-safe gate. Begin a transaction, try to insert the event key, and apply the business changes only if that insert created a row. Commit the claim and those changes together. If processing fails, roll back the transaction so a later retry can attempt the work again.

CREATE TABLE processed_webhook_events (
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  processed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (provider, event_id)
);

A Node.js handler using a PostgreSQL client with transaction support can follow this shape. Adapt the table scope and business update to your provider and application schema.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function handleWebhook(pool, provider, event) {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');

    const claim = await client.query(
      `INSERT INTO processed_webhook_events (provider, event_id)
       VALUES ($1, $2)
       ON CONFLICT DO NOTHING
       RETURNING event_id`,
      [provider, event.id]
    );

    if (claim.rowCount === 0) {
      await client.query('COMMIT');
      return { duplicate: true };
    }

    // Apply database-backed business changes using this same client.
    await applyBusinessChanges(client, event);

    await client.query('COMMIT');
    return { duplicate: false };
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

The key behavior is the database constraint plus INSERT ... ON CONFLICT DO NOTHING. Do not replace it with a separate SELECT followed by INSERT: two concurrent requests can both observe no row before either writes. The unique constraint arbitrates the race. Use the same transaction-bound client for the event claim and the business changes; a separate connection or transaction would break the all-or-nothing guarantee.

Add Redis only when its fast path is useful

Redis can provide a quick exact claim using SET with NX and an expiry. Redis describes the atomicity of NX this way: “The NX flag makes this atomic—no check-then-insert race, even under concurrent requests.” Its tutorial demonstrates the command pattern, but uses a Bun and Express demo, so it is not by itself a production architecture prescription. See Redis: How to do data deduplication with Redis.

For a Redis-assisted design, use the key as a fast filter and retain PostgreSQL’s unique constraint as the final authority. Redis state may expire, be evicted, disappear in a restart, or otherwise be lost; if it is your only record, a later delivery can be processed again. A Redis “already seen” result can short-circuit work, but a Redis miss should still be checked against the durable database gate.

Redis-only can reduce database lookups, but it makes correctness depend on Redis retention and availability. PostgreSQL-only is simpler to reason about when its latency and capacity suit the workload. The right split depends on traffic, failure tolerance, and provider replay behavior; maintaining Redis adds infrastructure and operational work, so use it when the fast path justifies that cost.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle duplicates and failures without trapping retries

When the event is already committed

If the atomic claim shows that the event is already in the processed table, return a successful acknowledgement appropriate to the provider. This prevents the sender from retrying an event whose database work has already completed. Verify the provider’s expected success status and response deadline in its documentation; those details are provider-specific.

When processing fails before commit

Roll back the transaction if a database operation fails. Because the processed-event row and business updates share the transaction, rollback removes the claim along with incomplete database work. A subsequent delivery can try again rather than being rejected as a completed duplicate.

When an external effect is involved

A PostgreSQL transaction cannot atomically include a remote API call, email, or other external effect. If the database commits but the remote call’s result is ambiguous, blindly retrying can produce a duplicate external action. Pass a downstream idempotency key when the receiving service supports one; otherwise use state-aware operations such as upserts or guarded state transitions, and define how ambiguous outcomes are reconciled.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Set retention around retries and replay

Choose Redis expiration to cover the provider’s retry horizon plus realistic processing delay. Keep durable event IDs for the application’s manual replay and recovery window. These windows vary by provider and operational policy, so there is no single duration that is correct for every webhook integration. Confirm the provider’s retry and replay rules before setting a TTL or deleting durable records.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep the idempotency check exact for important events

Redis SET NX and sets perform exact key checks. Bitsets and Bloom-style filters can produce false positives: an event that has not been processed may appear to have been seen. Do not use a probabilistic filter by itself to discard correctness-critical work such as payment events. It can serve as an optimization only when a false positive leads to an exact check rather than a dropped event.

Implementation checklist

  • Authenticate the request before trusting its event ID.
  • Use the provider’s stable event identifier and scope it by provider, tenant, or endpoint when needed.
  • Enforce uniqueness atomically in PostgreSQL; avoid a read-then-write deduplication check.
  • Commit the event claim and database business changes in the same transaction.
  • Use Redis as an optional fast layer, not the sole durable record.
  • Acknowledge confirmed duplicates successfully and follow the provider’s response requirements.
  • Give external effects their own idempotency or state protection.
  • Set Redis TTL and durable retention according to actual retry and replay policies.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.