Tous les articlesAn engraved hand coming out of a cloud, holding a compass over the world

Making webhooks idempotent on Cloudflare Workers

Providers retry. Your handler runs twice. Here is how to make the second run do nothing, with one KV key per event.

Dans cet article
  1. Why the same event arrives twice
  2. What counts as the same event
  3. One key per event
  4. What to answer

A payment provider calls your webhook. Your handler times out after it has written to the database, the provider gets no answer, and it sends the same event again a minute later. Now the customer has two orders.

Why the same event arrives twice

Every serious provider retries until it gets a 2xx. That is a feature: it means you never lose an event. It also means your handler must accept the same event more than once.

What counts as the same event

Providers send an id with each event. Two deliveries with the same id are the same event, even if their timestamps differ.

One key per event

Before doing any work, record the event id. If it is already there, answer 200 and stop.

import { Hono } from "hono";

type Env = { EVENTS: KVNamespace };
const app = new Hono<{ Bindings: Env }>();

app.post("/webhooks/payments", async (c) => {
  const event = await c.req.json<{ id: string; type: string }>();
  const key = `event:${event.id}`;

  if (await c.env.EVENTS.get(key)) return c.body(null, 200);
  await c.env.EVENTS.put(key, "1", { expirationTtl: 60 * 60 * 24 * 7 });

  // ...handle the event once
  return c.body(null, 200);
});

export default app;
pnpm wrangler kv namespace create EVENTS
pnpm wrangler deploy

KV is eventually consistent: two deliveries a few milliseconds apart can both pass the check. When that matters, use a Durable Object or a unique constraint in your database.

Two engraved hands shaking

What to answer

Situation Answer Why
New event, handled 200 The provider stops retrying
Event already seen 200 Nothing left to do
Bad signature 401 Not from the provider
Your database is down 503 Let the provider retry later

Record first, work second, and a retry becomes harmless.