MiddlewareGENERALSCALE-SPECIFICRUNTIME-SPECIFICCLOUD-SPECIFIC

Authenticate First, or Rate-Limit First?

Authenticating first means an unauthenticated flood still costs you crypto and a user lookup; rate-limiting first means your key is an IP, which is coarse and evadable. Both are true, so you do both, with different keys.

What actually happensHow to build it

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.

The question

Should rate limiting run before or after authentication?

The requirement

A public API. Paying customers have per-plan quotas that must be enforced accurately. The service must also survive a flood of requests from someone who has no account at all.

The obvious build

Authenticate first — obviously. You cannot apply a customer's quota until you know which customer it is, so identity has to be established before any limit can be enforced.

Why it breaks

An unauthenticated flood with garbage bearer tokens still runs your full authentication path: parse the token, verify the signature, look up the session or user. Every request costs asymmetric crypto and usually a datastore round trip before you decide to refuse it.

How it breaks in production
  • An unauthenticated flood with garbage bearer tokens still runs your full authentication path: parse the token, verify the signature, look up the session or user. Every request costs asymmetric crypto and usually a datastore round trip before you decide to refuse it.
  • Under that flood the connection pool is the resource that fails first: each auth lookup takes a pooled connection, legitimate traffic queues behind them, and the symptom is "the whole API is slow" rather than "someone is attacking the login endpoint" (Connection Pool Saturation: Waiting in Front of an Idle Database).
  • On a single-threaded runtime, signature verification is synchronous CPU work on the loop thread; enough of it and every other in-flight request is delayed, including the health check that decides whether this instance stays in rotation (Blocking the Event Loop).
  • Password endpoints are the extreme case: a deliberately slow hash such as bcrypt or Argon2 is a CPU-cost amplifier by design, so an unlimited login route lets an attacker convert their bandwidth into your CPU at a very favourable exchange rate (Credentials and Password Handling).
  • Flip it around and rate-limit first, and now a corporate office or a mobile carrier behind one NAT address shares a single bucket — so the limit is either too generous to protect anything or it locks out a hundred legitimate users at once.
RequirementAPI ContractApplication LogicData AccessExternal DepsConcurrencyFailureSecurityObservabilityDeploymentScale

What is actually happening

  • A rate limiter needs a key. The whole question is which keys are available at which point in the pipeline, and how much each one is worth.
  • Before authentication, the only keys available are transport-level: source IP, an IP prefix, a TLS fingerprint, an API key presented in plaintext, or the route itself. These are cheap and unreliable — IPs are shared by many users through NAT and CGNAT, and IPv6 gives a single client an enormous address range to rotate through.
  • After authentication, the key is the principal: a user, an API key, a tenant, a plan. This is accurate, matches your billing, survives IP changes, and is exactly the key you cannot compute yet when the expensive work happens.
  • Authentication is not one cost, it is a gradient. Reading a header is free. Parsing a JWT and checking exp is cheap. Verifying an RS256 signature is real CPU. Fetching the session or user record is a network round trip and a pooled connection. Fetching and validating a JWKS document over the network is worse. You can reject at each of these steps and pay only what you have spent so far.
  • A hard constraint sits on top of this: never key a rate limit on an unverified identity. If you read sub from an unverified JWT to pick a bucket, an attacker sets sub to a victim's id and exhausts their quota. Unverified claims may be used to *reject* a request, never to attribute one (JWT Failure Modes).
  • So the honest answer is not an ordering at all. It is a budget per key, enforced wherever that key first becomes available: a coarse pre-auth limit on the transport key, and an accurate post-auth quota on the principal.

The trade, stated honestly in both directions

Both orderings are defended by people who have operated real systems, and both defences are correct about what they claim. Setting them side by side makes it clear that they are not answering the same question: one is protecting the service, the other is enforcing a contract.

Note the last row. It is the one that decides the argument — the two orderings do not have the same key available, and the key is the entire substance of a rate limit.

Authenticate, then limitLimit, then authenticate
Cost of a rejected requestFull auth path: parse, verify signature, look up user — then refuseA counter increment, then refuse
Cost under an unauthenticated floodCPU on signature verification plus a pooled connection per requestBounded by the limiter itself
Accuracy of the limitExact: per user, per API key, per plan, survives IP changesCoarse: an IP or prefix shared by many real users
EvasionHard — needs valid credentials to spend anyone's quotaEasy at scale — rotate IPs, especially in IPv6
False positivesNone; each principal has their own bucketWhole offices and carrier NATs share one bucket
Billing and documentationThis is the quota customers were sold (Quotas vs Rate Limits)Not expressible in customer terms
Key available at this pointPrincipal, tenant, planIP, IP prefix, route class, raw API key

The answer is a budget per key, not an ordering

Once you stop treating rate limiting as one box that must be placed once, the design writes itself: enforce a limit at each point where a new, more meaningful key becomes available, and make each one cheap relative to the work it protects.

The middle steps matter as much as the ends. Authentication is a gradient of increasing cost, and each step can reject. A structurally invalid token should never reach signature verification; a token with a valid signature but an unknown subject should be answered from a negative cache rather than the database. That is what turns an authenticated flood from an outage into a metric.

Cheapest rejection first
  1. 1
    Edge: connections and per-IP requests

    Sheds volumetric traffic before it reaches your process at all.

    fails by Being the only layer — it can be bypassed, misconfigured, or absent for internal traffic (Rate Limiting).

  2. 2
    Pre-auth limit: IP prefix + route class

    Bounds what an unauthenticated caller can cost you, with a strict budget for login and token routes.

    fails by A single global IP bucket: too loose to protect login, too tight for a shared office address.

  3. 3
    Structural token check

    Rejects malformed tokens and expired exp values with no crypto and no I/O.

    fails by Trusting anything read here for attribution — unverified claims may reject, never attribute (JWT Failure Modes).

  4. 4
    Signature verification

    Establishes that the token is genuine. Real CPU, no network if keys are cached.

    fails by Fetching JWKS per request, turning every verification into an outbound HTTP call (Timeouts).

  5. 5
    Identity lookup (cached, incl. negatives)

    Resolves principal, tenant and plan.

    fails by No negative caching, so well-formed invalid tokens still hit the datastore every time (Connection Pools).

  6. 6
    Post-auth quota: principal / API key / tenant

    Enforces the customer-visible limit and emits the RateLimit headers clients depend on.

    fails by Per-instance counters, so the effective limit is your instance count times the configured number (Making an Existing Service Stateless).

  7. 7
    Authorisation, then handler

    Decides what this principal may do, then does it (Authorization in Backends).

    fails by Treating the quota check as if it were an authorisation check (Authentication vs Authorization).

Steps 2 and 6 are both "rate limiting". They have different keys, different budgets, different owners and different dashboards, and collapsing them into one decision is what makes this question feel unanswerable.

The limiter is a dependency too

A rate limiter is a piece of shared state consulted on every request, which makes it a dependency with its own latency, its own failure modes and its own concurrency bugs. Most of the ways rate limiting goes wrong in production are not about ordering at all — they are about the limiter itself.

The first row is the one to decide before an incident rather than during one. Fail-open keeps you serving and removes your protection at exactly the moment something is stressing your infrastructure; fail-closed protects you and converts a limiter outage into a total outage. There is no correct default, only a decision you have made and instrumented (Fail Open vs Fail Closed).

TriggerSymptomCauseResponse
Limiter datastore unreachableEither all requests pass, or all requests 429No explicit policy for limiter unavailabilityChoose fail-open with a loud counter and an alert; consider a local fallback bucket so you degrade rather than disable
Read-modify-write counting across instancesEffective limit far above the configured one under loadThe check and the increment are separate operationsUse an atomic increment or a single server-side script (Atomic Operations)
Fixed window boundaryTwice the limit in a short span around each boundaryTwo adjacent windows each allow a full burstSliding window or token bucket (Rate Limit Algorithms)
X-Forwarded-For trusted unconditionallyPre-auth limit has no effectThe client sets the header the limiter keys onTrust the header only from known proxies and take the correct hop; otherwise use the socket address
Limit keyed on an unverified sub claimA specific customer is locked outAttribution from an unverified claimNever key on unverified identity; use it only to reject (JWT Failure Modes)
Per-instance counters behind a load balancerDocumented quota is off by the instance countLocal state used for a global limitShared counter, or divide the budget deliberately and account for uneven balancing (Making an Existing Service Stateless)
Auth cache with no negative entriesInvalid-token flood saturates the poolOnly successful lookups are cachedCache negative results with a short TTL and a bounded key space (TTL and Expiry)

How to build it

Most important first.

  • Put a coarse, generous limit before authentication, keyed on client IP and route class, so the login route, the token route and the signature-verifying routes have their own small budgets while general reads have a large one.
  • Put the real, customer-visible quota after authentication, keyed on the principal or API key. This is the one your documentation describes and your billing depends on (Quotas vs Rate Limits).
  • Order the authentication work cheapest-first: structural checks and expiry before signature verification, signature verification before any datastore lookup. Reject at the earliest step that can decide (Token Authentication and the Revocation Problem).
  • Cache the identity lookup — and cache negative results too, or a flood of invalid-but-well-formed tokens still reaches the datastore on every request (Caching in Backends).
  • Push the coarsest shedding to the edge, where it costs you nothing: connection limits and per-IP request caps at the CDN, load balancer or WAF. In-process limits remain, because the edge can be bypassed or misconfigured (Rate Limiting).
  • Derive the client IP correctly: trust X-Forwarded-For only from proxies you control, count hops from the right end, and treat the header as attacker-controlled otherwise. A misconfigured trust setting makes the pre-auth limiter trivially bypassable by spoofing.
  • Decide the failure policy for the limiter before you need it: if the limiter's datastore is unreachable, do you allow or refuse (Fail Open vs Fail Closed)? Usually allow, with an alert — but that is a decision, not a default to inherit.

What can go wrong

Failure modes
  • A shared-IP false positive: an office, a school or a mobile carrier hits the pre-auth limit and every user behind it is refused simultaneously. This is the cost of the design, and it needs a support path.
  • IPv6 rotation: limiting on a full /128 address is nearly useless when a client is allocated a /64 or larger. Limit on a prefix.
  • The limiter datastore becoming the bottleneck: a per-request round trip to Redis added to every request, including the ones you were trying to make cheap.
  • Non-atomic counting: read, increment, write from many instances admits far more than the limit under exactly the load the limit exists for (Atomic Operations).
  • Fixed windows allowing a double burst across the boundary — the full limit at the end of one window and again at the start of the next (Rate Limit Algorithms).
  • Cached authentication with no negative caching, so invalid tokens still cost a lookup each.
  • Two limiters with independent configuration drifting apart, so the documented quota and the enforced one differ.
What can race
  • Read-modify-write counting from multiple instances loses increments under concurrency and admits well over the limit exactly when the limit matters. Use an atomic increment or a server-side script that reads and writes in one operation (Atomic Operations).
  • Fixed-window boundaries let a caller spend a full window's budget at the end of one window and again immediately at the start of the next — a 2x burst that the configured number does not describe (Rate Limit Algorithms).
  • A cached identity and a concurrent revocation disagree for the length of the TTL, so a revoked credential is briefly still accepted (Where Sessions Live).
  • Two requests arriving simultaneously for a key with one unit of budget left can both observe "1 remaining" if the check and the decrement are separate steps.
Security
  • The unauthenticated surface is the attacker's surface. A design that only limits authenticated callers has no limit where it matters most (Rate Limiting as a Security Control).
  • Never derive a limit key from an unverified claim. Doing so converts your rate limiter into a tool for denying service to a chosen victim.
  • Login, token issuance, password reset, signup and any endpoint doing a deliberately slow hash need their own strict pre-auth limits keyed on both IP and the submitted identifier — and limiting on the identifier alone lets an attacker lock out a user, so pair it with an unlock path (How Passwords Are Actually Attacked).
  • Return 429 with Retry-After and no information about why a specific key was chosen. A limiter that says "you have 3 of 100 requests left for user X" confirms account existence.
  • Failing open on limiter outage means an attacker who can degrade your limiter has removed your limits. Make that trade knowingly and alert on it (Fail Open vs Fail Closed).
  • Log rejections with enough context to investigate — key type, route class, count — without logging the credentials that were presented (Secrets in Logs).
Misreads
  • "You must authenticate first, because the limit is per user." The customer-visible quota is per user. The protective limit is not, and it is a different control with a different key.
  • "Rate-limit first, then you never waste work." Then your only key is the IP, which is shared by legitimate users and cheap for an attacker to rotate. It is a blunt instrument, useful precisely because it is cheap.
  • "IP-based limits are useless." They are coarse, evadable at scale, and still the only thing standing between an unauthenticated flood and your CPU. Coarse is not the same as useless.
  • "The gateway rate-limits, so we are covered." Gateway limits are usually per-IP and global, not per-plan; and internal traffic, retries and anything that bypasses the gateway are not covered (The Gateway as Policy Boundary).
  • "429 and 503 are interchangeable." 429 says this caller is over their limit; 503 says the service cannot serve anyone right now. Clients back off differently and your dashboards mean different things (Status Codes From the Server's Side).
  • "Rate limiting is authorisation." It bounds volume. It says nothing about whether the caller may perform the action (Authentication vs Authorization).

Operating it

How you see it in production
  • Split the 429 counter by stage and key type: stage="pre_auth", key="ip" versus stage="post_auth", key="principal". A shift between them during an incident tells you immediately whether you are seeing a flood or a heavy customer.
  • Authentication failures per source IP and per route. A high rate of well-formed but invalid tokens is a probe, and it is exactly the traffic the pre-auth limit exists to make cheap.
  • CPU time attributable to signature verification, and the p99 of the authentication middleware. If either moves during a traffic spike, the ordering is doing work it should not (Self Time, Total Time, and Where the CPU Went).
  • Pool wait time during the spike. Auth lookups queueing on the pool is the mechanism by which an unauthenticated flood becomes a full outage (Connection Pool Saturation: Waiting in Front of an Idle Database).
  • Limiter datastore latency and error rate, plus a counter of "allowed because the limiter was unavailable". That counter must be visible; it is your fail-open budget.
  • Distinct-key cardinality on the pre-auth limiter — an explosion of distinct IPs is a distributed source and means the IP key has stopped working (Cardinality: The Label That Took Down Monitoring).
What changes at 10x and 100x
  • At low traffic none of this matters and a single post-auth limiter is a reasonable, honest choice. The pre-auth layer earns its complexity when you are exposed to traffic you did not solicit.
  • At high traffic the in-process limiter itself becomes a cost: a network round trip per request to a shared counter. Local token buckets with periodic reconciliation trade exactness for cost, and the trade is usually right for the coarse layer and wrong for the billing layer (Local vs Distributed Cache).
  • With many instances, per-instance limits multiply: ten instances with a limit of 100 each enforce 1000. Either use a shared counter or divide deliberately and account for uneven load balancing.
  • Distributed floods defeat per-IP limiting by construction. Above a certain scale the answer moves to the edge — connection limits, ASN reputation, challenge responses — and the in-process limiter becomes a backstop rather than the defence (Cascading Failure).
What this costs
  • Two limiters means two configurations, two dashboards and two places for the rules to drift. It is genuinely more to operate than one.
  • The pre-auth limit produces false positives for shared-IP users and there is no way to avoid that: before authentication you cannot tell a busy office from an attacker.
  • Caching identity lookups makes authenticated floods cheap and adds a staleness window on revocation — a revoked token keeps working for the TTL unless you check a revocation list (Where Sessions Live).
  • Rejecting before parsing means your 429 responses carry less useful diagnostic information, because you deliberately did not do the work needed to produce it.
  • Edge shedding is the cheapest possible rejection and moves a security control into infrastructure that application engineers often cannot see or test.

Where this applies

Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.

  • GENERALThe trade — identity gives you an accurate key but costs work to obtain, transport gives you a cheap key but a coarse and evadable one — is a property of the problem, not of any stack.
  • SCALE-SPECIFICOn an internal service behind a VPN with known callers, one post-auth limiter is sufficient and the pre-auth layer is unjustified complexity. On an internet-facing API the pre-auth layer is the one that keeps you up during an incident. Do not port the answer between them.
  • RUNTIME-SPECIFICSignature verification on Node runs on the single loop thread unless you dispatch to a worker, so an auth flood delays every concurrent request in the process; in a thread-per-request server the same work saturates a bounded thread pool instead, and in Go it consumes scheduler capacity across cores. The failure is saturation in all three, but the first symptom differs — event-loop lag, thread-pool exhaustion, or CPU throttling (Backend Runtime Models).
  • CLOUD-SPECIFICManaged edge rate limiting exists at every major provider and none of them are equivalent: the granularity, the key options, whether limits are per-edge-location or global, and what happens on a rules-engine failure all differ. Verify behaviour against the specific product rather than assuming a mapping (Mapping Services Across Cloud Providers).

Where the depth lives

This domain teaches the application-side mechanism and hands the rest off.