Skip to main content
  1. Code/

Idempotent Order Intake at the Edge

A retried POST should never place a second order. The client timed out, a proxy replayed the request, the phone flipped from Wi-Fi to cellular mid-flight — none of that is the trading desk’s problem. The fix is an idempotency key: the caller stamps each logical order with a unique value and repeats it on every retry, and the server promises to act on that key at most once.

Here it runs at the edge as a single Cloudflare Worker over one Workers KV namespace — no origin round-trip, no database to operate. This page is the working code; drop it into a Worker project and it runs.

The Worker #

The request lifecycle is three KV touches: check for a finished result, claim the key, then write the result and release the claim. A replay short-circuits on the first check; a concurrent retry that arrives mid-flight hits the pending marker and is told to back off instead of racing a duplicate through.

idempotent-orders.ts Raw Download
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
/**
 * Idempotent order intake at the edge.
 *
 * A Cloudflare Worker that accepts order submissions over HTTP and guarantees
 * at-most-once processing per client-supplied Idempotency-Key. A retried POST
 * — the client timed out, the network blipped, a proxy replayed it — returns
 * the original response instead of placing a second order.
 *
 * Storage is a single Workers KV namespace. The first request for a key writes
 * a short-lived "pending" marker, processes the order, then overwrites it with
 * the final response. Concurrent retries that arrive mid-flight see the marker
 * and are told to back off rather than racing a duplicate through.
 */

interface Env {
  ORDERS: KVNamespace;
}

interface OrderRequest {
  symbol: string;
  side: "buy" | "sell";
  quantity: number;
}

interface OrderResult {
  orderId: string;
  status: "accepted";
  symbol: string;
  side: "buy" | "sell";
  quantity: number;
  acceptedAt: string;
}

// Keep a completed response addressable by its key for 24h of retries.
const RESULT_TTL_SECONDS = 60 * 60 * 24;
// A pending marker should outlive a single request but expire quickly if the
// Worker is evicted mid-flight, so a stuck key self-heals.
const PENDING_TTL_SECONDS = 30;

const json = (body: unknown, status = 200): Response =>
  new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json" },
  });

function validate(body: unknown): OrderRequest | null {
  if (typeof body !== "object" || body === null) return null;
  const { symbol, side, quantity } = body as Record<string, unknown>;
  if (typeof symbol !== "string" || symbol.length === 0) return null;
  if (side !== "buy" && side !== "sell") return null;
  if (typeof quantity !== "number" || !Number.isInteger(quantity) || quantity <= 0) return null;
  return { symbol, side, quantity };
}

async function placeOrder(order: OrderRequest): Promise<OrderResult> {
  // Stand-in for the real order-management call. In production this is where the
  // FIX session or OMS gateway lives; here we just mint a deterministic receipt.
  return {
    orderId: crypto.randomUUID(),
    status: "accepted",
    symbol: order.symbol,
    side: order.side,
    quantity: order.quantity,
    acceptedAt: new Date().toISOString(),
  };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") {
      return json({ error: "method_not_allowed" }, 405);
    }

    const key = request.headers.get("Idempotency-Key");
    if (!key) {
      return json({ error: "missing_idempotency_key" }, 400);
    }

    // Replay path: a finished result already exists for this key.
    const cached = await env.ORDERS.get(`result:${key}`);
    if (cached) {
      return new Response(cached, {
        status: 200,
        headers: { "content-type": "application/json", "Idempotent-Replay": "true" },
      });
    }

    // Claim the key. If another in-flight request already claimed it, ask the
    // caller to retry once the first one settles.
    const pending = await env.ORDERS.get(`pending:${key}`);
    if (pending) {
      return json({ error: "in_progress", retryAfter: PENDING_TTL_SECONDS }, 409);
    }
    await env.ORDERS.put(`pending:${key}`, "1", { expirationTtl: PENDING_TTL_SECONDS });

    const order = validate(await request.json().catch(() => null));
    if (!order) {
      await env.ORDERS.delete(`pending:${key}`);
      return json({ error: "invalid_order" }, 422);
    }

    const result = await placeOrder(order);
    const body = JSON.stringify(result);

    await env.ORDERS.put(`result:${key}`, body, { expirationTtl: RESULT_TTL_SECONDS });
    await env.ORDERS.delete(`pending:${key}`);

    return new Response(body, { status: 201, headers: { "content-type": "application/json" } });
  },
};

The two TTLs do the real work. result: lives for 24 hours so retries stay idempotent across a realistic client window, while pending: expires in 30 seconds so a Worker evicted mid-request can’t wedge a key forever — the marker self-heals and the next retry succeeds. Validation runs after the claim so a malformed body releases the key rather than poisoning it.

Binding the namespace #

One KV namespace holds both the pending markers and the response cache. The binding name ORDERS is what the Worker reads as env.ORDERS.

wrangler.toml Raw Download
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
name = "idempotent-orders"
main = "idempotent-orders.ts"
compatibility_date = "2026-01-01"

# A single KV namespace holds both the short-lived "pending" markers and the
# 24h response cache. Create it once, then paste the id below:
#   wrangler kv namespace create ORDERS
[[kv_namespaces]]
binding = "ORDERS"
id = "<your-kv-namespace-id>"

[observability]
enabled = true

Create the namespace with wrangler kv namespace create ORDERS, paste the returned id, and wrangler deploy.

Proving it #

The contract is visible from the command line: submit once, retry under the same key, and compare. The first call returns 201 with a fresh orderId; the replay returns 200 with the identical body and an Idempotent-Replay: true header.

submit-order.sh Raw Download
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env bash
# Submit the same order twice under one Idempotency-Key.
# The first call returns 201 with a fresh orderId; the replay returns 200 with
# the identical body and an "Idempotent-Replay: true" response header.
set -euo pipefail

ENDPOINT="${1:-https://idempotent-orders.example.workers.dev}"
KEY="$(uuidgen)"

submit() {
  curl -sS -D - -o /tmp/order-body.json \
    -X POST "$ENDPOINT" \
    -H "Idempotency-Key: $KEY" \
    -H "content-type: application/json" \
    -d '{"symbol":"MSFT","side":"buy","quantity":100}'
  echo
  cat /tmp/order-body.json
  echo
}

echo "# First submission"
submit

echo "# Retry with the same key"
submit

Same key in, same receipt out — the second request never reaches the order-management gateway. That is the whole point: the edge absorbs the duplicate so the system of record never sees it.