惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
量子位
N
Netflix TechBlog - Medium
博客园 - 聂微东
博客园 - Franky
aimingoo的专栏
aimingoo的专栏
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
B
Blog
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
腾讯CDC

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
A practical, “ship-it” guide to idempotency keys, request...
Hossein Esmati · 2026-06-26 · via DEV Community

Hossein Esmati

API layer pattern (HTTP)

1) Contract

  • Require Idempotency-Key on POST that creates resources (optional on GET, safe methods).
  • For PUT/PATCH, prefer natural keys (resource URI) + concurrency (ETag) over custom keys.
  • Return Idempotency-Replayed: true on replays.

2) What to persist

  • (TenantId, IdempotencyKey)unique.
  • Request fingerprint (stable hash of method + path + canonicalized body).
  • First status code, headers (whitelisted), response body (or a pointer), timestamps, and a short TTL.

SQL table (Azure SQL) example

CREATE TABLE Idempotency (
  TenantId        NVARCHAR(64) NOT NULL,
  IdempotencyKey  NVARCHAR(128) NOT NULL,
  RequestHash     VARBINARY(32) NOT NULL, -- SHA-256
  StatusCode      INT           NULL,
  ResponseBody    VARBINARY(MAX) NULL,    -- or NVARCHAR(MAX) if JSON
  CreatedAtUtc    DATETIME2(3)  NOT NULL  DEFAULT SYSUTCDATETIME(),
  CompletedAtUtc  DATETIME2(3)  NULL,
  CONSTRAINT PK_Idem PRIMARY KEY (TenantId, IdempotencyKey)
);
CREATE INDEX IX_Idem_Created ON Idempotency (CreatedAtUtc);

(Cosmos DB works too: define a unique key on /tenantId/idempotencyKey and set TTL on the container.)

3) Minimal middleware/filter in ASP.NET Core

public class IdempotencyMiddleware : IMiddleware
{
    private readonly IIdemStore _store; // wraps Azure SQL or Cosmos
    public IdempotencyMiddleware(IIdemStore store) => _store = store;

    public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
    {
        var tenantId = ctx.User.FindFirst("tenant_id")?.Value ?? "public";
        var key = ctx.Request.Headers["Idempotency-Key"].ToString();
        if (string.IsNullOrWhiteSpace(key)) { await next(ctx); return; }

        var hash = await HashRequestAsync(ctx.Request); // method+path+normalized body
        var existing = await _store.TryGetAsync(tenantId, key);

        if (existing is { Completed: true } && existing.RequestHash.SequenceEqual(hash))
        {
            ctx.Response.Headers["Idempotency-Replayed"] = "true";
            ctx.Response.StatusCode = existing.StatusCode;
            if (existing.ResponseBody is { Length: > 0 })
            {
                ctx.Response.ContentType = "application/json";
                await ctx.Response.Body.WriteAsync(existing.ResponseBody);
            }
            return;
        }

        // Reserve the key up-front (prevents thundering herds)
        var reserved = await _store.ReserveAsync(tenantId, key, hash); // insert if not exists
        if (!reserved) // concurrent duplicate while first is in-flight
        {
            // Poll or short 409 telling client to retry shortly; or wait on a small backoff and re-check
            ctx.Response.StatusCode = StatusCodes.Status409Conflict;
            await ctx.Response.WriteAsync("{\"error\":\"Request in progress\"}");
            return;
        }

        // Capture response
        var originalBody = ctx.Response.Body;
        await using var mem = new MemoryStream();
        ctx.Response.Body = mem;

        try
        {
            await next(ctx);
            await _store.CompleteAsync(tenantId, key, ctx.Response.StatusCode, mem.ToArray());
        }
        finally
        {
            mem.Position = 0;
            await mem.CopyToAsync(originalBody);
            ctx.Response.Body = originalBody;
        }
    }

    static async Task<byte[]> HashRequestAsync(HttpRequest req)
    {
        req.EnableBuffering();
        using var sha = System.Security.Cryptography.SHA256.Create();
        var body = "";
        if (req.ContentLength > 0)
        {
            using var reader = new StreamReader(req.Body, leaveOpen:true);
            body = await reader.ReadToEndAsync();
            req.Body.Position = 0;
        }
        var canonical = $"{req.Method}\n{req.Path}\n{NormalizeJson(body)}";
        return sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(canonical));
    }

    static string NormalizeJson(string json) => string.IsNullOrWhiteSpace(json)
        ? ""
        : System.Text.Json.JsonDocument.Parse(json).RootElement.GetRawText(); // canonical spacing/order for objects mostly stable in client contracts
}

Notes

  • ReserveAsync does an INSERT guarded by the unique key; if it fails, another request holds the key.
  • Set a TTL/cleanup job (e.g., 24–72h) to trim completed entries.

PUT semantics with ETags (Concurrency)

  • PUT should replace the full resource representation (idempotent by nature).
  • Use ETag + If-Match to prevent lost updates.

Cosmos DB / Azure Table / Azure SQL

  • Cosmos & Tables expose ETags natively; for SQL, emulate with a rowversion column.

Controller snippet

[HttpPut("{id}")]
public async Task<IActionResult> Put(string id, [FromBody] Widget dto, [FromHeader(Name="If-Match")] string etag)
{
    // Load current, compare ETag/rowversion; if mismatch => 412 Precondition Failed
    var ok = await _repo.ReplaceAsync(id, dto, etag);
    return ok ? NoContent() : StatusCode(StatusCodes.Status412PreconditionFailed);
}


Messaging/Queue consumers (de-dup & exactly-once illusions)

Azure Service Bus (recommended over Storage Queues for de-dup)

  • Enable Duplicate Detection on the queue/topic (DuplicateDetectionHistoryTimeWindow, e.g., 10 minutes–7 days).
  • Set MessageId to a business idempotency key (e.g., the order ID or the HTTP Idempotency-Key).
  • Still make the handler idempotent (at-least-once delivery can still surface via re-enqueue/abandon).

Producer

var client = new ServiceBusClient(connStr);
var sender = client.CreateSender("orders");
var msg = new ServiceBusMessage(BinaryData.FromObjectAsJson(order))
{
    MessageId = order.Id, // business key
    Subject = "OrderCreated"
};
await sender.SendMessageAsync(msg);

Function/Worker handler (inbox pattern)

public class OrderHandler
{
    private readonly IInboxStore _inbox; // Azure SQL/Cosmos unique on (MessageId)

    public async Task HandleAsync(ServiceBusReceivedMessage m, CancellationToken ct)
    {
        // Fast de-dup check
        if (!await _inbox.ReserveAsync(m.MessageId)) return; // already processed

        try
        {
            var order = m.Body.ToObjectFromJson<OrderCreated>();
            await ProcessAsync(order, ct); // make this idempotent
            await _inbox.CompleteAsync(m.MessageId);
        }
        catch
        {
            await _inbox.ReleaseAsync(m.MessageId); // allow retry
            throw;
        }
    }
}

Azure Storage Queues / Event Hubs

  • No built-in de-dup for Storage Queues; always use an inbox table keyed by a message/business id.
  • Event Hubs is stream-oriented; treat processing as idempotent, or checkpoint and maintain an inbox keyed by event idempotency key.

Outbox (publish exactly-once to the bus)

  • In the same DB transaction as state change, write Outbox rows (events to publish).
  • A background dispatcher reads un-sent rows and publishes to Service Bus; upon success, marks them sent.
  • Libraries: NServiceBus/MassTransit have outbox patterns built-in; or roll your own with EF Core.

EF Core Outbox table

CREATE TABLE Outbox (
  Id UNIQUEIDENTIFIER PRIMARY KEY,
  OccurredUtc DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
  Type NVARCHAR(200) NOT NULL,
  Payload NVARCHAR(MAX) NOT NULL,
  Sent BIT NOT NULL DEFAULT 0
);


Hashing strategy (practical)

  • Canonicalize: METHOD + PATH + canonical(JSON body) + critical headers (e.g., Content-Type, Version) + TenantId.
  • Use SHA-256; store 32-byte hash.
  • Hash matters for replay safety: if key reused with different payload, reject with 422 Unprocessable Entity (or 409 Conflict) to avoid accidental cross-use.

Where to put what (Azure choices)

  • Idempotency store:

    • Cosmos DB: low latency, unique key on (tenantId, idemKey), TTL per item.
    • Azure SQL: great if you already use SQL; unique composite key, rowversion for concurrency.
    • Redis: fast reservation locks via SETNX + TTL—but still persist the final response in durable storage.
  • Queue: Azure Service Bus with duplicate detection + sessions if you need ordered processing per aggregate.


Error handling & UX

  • If a duplicate arrives before the first completes: return 409 (“in progress”) or 202 with a status URL the client can poll.
  • If a duplicate arrives after completion with the same hash: return the original status/body with Idempotency-Replayed: true.
  • If same key, different hash: 422/409 to force client to pick a new key.

Observability

  • Log the idempotency key, request hash, and replay status.
  • Add metrics: “idem_hits”, “idem_inflight”, “idem_conflicts”, and “handler_dedup_hits”.

Summary: Implementation Checklist (.NET + Azure)

  1. HTTP: require Idempotency-Key on POST creates; middleware reserves key → executes → stores response → replays on duplicates.
  2. PUT: design as full replace + ETag (If-Match) to make retries safe and prevent lost updates.
  3. Queues: use Service Bus with DuplicateDetection + MessageId; consumers maintain an inbox table and business-idempotent handlers.
  4. Outbox: publish messages from a DB outbox to get “exactly-once” effect across boundaries.
  5. Persistence: Cosmos (unique key + TTL) or SQL (unique index).
  6. Hashing: canonical SHA-256 over method/path/body/tenant; reject key reuse with different payloads.