Skip to content
Kumar Chandrachooda
.NET

Throttling, Idempotency, and an HTTP 402 Surprise

The cross-cutting drawer of FastEndpoints holds a rate limiter that documents its own limitations, idempotency built on output caching, server-sent events - and a full implementation of the x402 micro-payments protocol. Part 16 of FastEndpoints in Depth.

By Kumar Chandrachooda 20 May 2026 5 min read
The utility drawer: hit counters, replay keys, event streams, and a 402

Every framework accumulates a drawer of features that are nobody's headline and everybody's requirement. This part opens FastEndpoints' drawer — rate limiting, idempotency, response caching, streaming responses — and ends with the strangest thing I found in the entire repository: a working implementation of HTTP 402.

Throttling: honest about what it is

Per-endpoint rate limiting is one line — Throttle(hitLimit: 120, durationSeconds: 60) — and Part 3 showed where it enforces: in FeRequestHandler, before the endpoint instance is even created. A 429 costs almost nothing.

The implementation (HitCounter.cs) is a ConcurrentDictionary of per-client counters keyed by a header value — default X-Forwarded-For, falling back to the remote IP, refusing the request outright (403) if neither exists. Each counter is a fixed window: first hit sets an expiry, hits increment until the limit, expiry resets the count. A 60-second timer per counter removes expired entries.

What I want to praise is not the algorithm — it is deliberately basic — but the honesty around it. Fixed windows allow the classic 2x burst at window edges. Header-keyed identity means clients choose their identity unless you strip the header at the edge (the docs say exactly this: it's suited for scenarios like per-device app keys, not adversarial abuse prevention). State is per-instance memory, so three replicas multiply every limit by three, and a restart amnesties everyone. None of this is hidden; the design signals it. Use it for politeness enforcement — a partner integration that retries too enthusiastically, a mobile client with a bug — and use infrastructure (or ASP.NET Core's RateLimiter middleware, which composes fine with FastEndpoints routes) for actual abuse. A rate limiter that documents its own inadequacies is worth ten that promise fairness they can't distribute.

Idempotency: composition over invention

Payment-shaped endpoints need “retrying the same request must not repeat the side effect”. FastEndpoints ships idempotency support, and the implementation choice is the story: it did not build a replay store. AddIdempotency() configures ASP.NET Core's output caching with a custom policy; Idempotency() on an endpoint requires an Idempotency-Key header and makes the cached response vary by it:

public override void Configure()
{
    Post("/payments");
    Idempotency(o =>
    {
        o.CacheDuration = TimeSpan.FromHours(1);
    });
}

First request with key K executes and caches the response; every replay of K within the window gets the cached response without touching your handler. Because it rides IOutputCacheStore, pointing it at Redis makes idempotency distributed with zero additional code — the composition dividend, the same philosophy as Part 7's policies riding standard authorization.

The fine print you must read: this is response replay, not request fingerprinting by default — two different payloads sent with the same key return the same cached response unless you enable the additional request-matching options; and the caveats of caching apply (the swagger integration marks the header requirement, but your clients must actually generate unique keys per logical operation). Idempotency keys are a protocol between client and server; the library gives you the server half.

Plain response caching — ResponseCache(60) — also exists, mapping to the standard ASP.NET Core response caching attributes and middleware, executed via ResponseCacheExecutor at dispatch time. Same standard-plumbing story.

Streams: SSE and large bodies

Two response shapes in the Send API deserve a mention beyond the Ok/Created family.

Server-sent events are first-class: Send.EventStreamAsync("progress", myAsyncEnumerable, ct) writes a standards-compliant text/event-stream, and a StreamItem variant lets one stream carry heterogeneous event types. Pair it with a streaming command handler from Part 11 and “job progress as live events” (Part 13's tracker feeding an SSE endpoint) is an afternoon's work. For dashboards and progress UX, SSE over websockets is the underrated choice — no connection protocol, plain HTTP, works through proxies — and having it in the response vocabulary keeps teams from reaching for SignalR by reflex.

Files and byte streams: Send.FileAsync/BytesAsync/StreamAsync handle content types, content disposition and — notably — range request processing for resumable downloads. Combined with the buffering-free multipart upload streaming from Part 4, the large-binary story is complete in both directions without ever holding a payload in memory.

The drawer also contains: antiforgery token middleware for form endpoints, endpoint feature flags (the IFeatureFlag hook from Part 3 that 404s dark endpoints), plain-text request handling, health-checks and OData packages, and a RedirectAsync that refuses remote redirects unless you explicitly allow them — a small default that closes an open-redirect class of bug.

And then there is X402

Browsing Src/Library, between Validation and the DTOs, sits a folder called X402. I assumed an issue number. It is an implementation of x402 — the payments protocol (spearheaded by Coinbase) that resurrects HTTP's reserved 402 Payment Required status code for machine-payable endpoints: a client (increasingly, an AI agent) calls an endpoint, receives a 402 carrying structured payment requirements, pays (stablecoin settlement via a “facilitator” service), retries with a payment-signature header, and gets its response.

The FastEndpoints middleware implements the server side end to end: metadata on an endpoint declares price and accepted network; the middleware answers unauthenticated calls with the 402 + requirements JSON; on retry it deserializes the base64 payment payload from the header, verifies and settles through a pluggable facilitator client, and only then lets the request through to your handler — with the payment details available via context extensions, and the 402 response advertised in swagger (Part 10's produces-metadata machinery again).

I am not going to pretend I have shipped x402 to production — I have not, and the protocol's ecosystem is young. But its presence in a mainstream .NET framework is a signal worth reading: pay-per-call APIs priced in fractions of a cent, consumed by autonomous agents without accounts or API-key onboarding, are being built now, and the server-side plumbing has already commoditized to a Configure() call. File under “things I checked the commit history to believe”.

What the drawer says about the library

Surveyed together, the cross-cutting features repeat one design sentence: implement the semantics, delegate the infrastructure, disclose the limits. Throttling implements counting and tells you it isn't distributed. Idempotency implements key semantics and delegates storage to output caching. SSE implements the wire format and leaves fan-out to you. It is the opposite of platform ambition, and it is why the pieces compose instead of competing with your architecture.

That completes the tour — sixteen parts through the pipeline, the security model, the documentation machinery, three messaging systems, the test framework, and the generators. What remains is the question every deep dive owes an answer: knowing all of this, would I choose it again — and when would I tell you not to? Part 17: When Not to Use FastEndpoints.