HTTP 429 Too Many Requests is the status code a server returns to tell a client it has sent too many requests in a given time — the standard signal of rate limiting. On a login page it is the response that stops automated brute-force and credential-stuffing tools, and it usually carries a Retry-After header saying when to try again. The SecScan checker looks for exactly this signal. Here is what it means and how to return it correctly.
For the site owner (plain English)
Every response a website sends has a numeric code: 200 means “OK, here it is,” 404 means “not found.” 429 means “you're asking too often — slow down.” It is the polite way a server pushes back when something is hammering it, and on a login page it is the sign that automated password attacks are being blocked. When our checker reports your login page is protected, it means the server answered with a 429 (or a related Retry-After instruction) during a short burst of test requests. If it never does, there is nothing telling attackers' bots to stop — that is the gap to close, and the developer section below shows exactly how.
What the 429 status means
The 429 status is defined in RFC 6585 (see also the MDN reference). It sits in the 4xx family — client errors — and means the client has exceeded a rate limit the server enforces. The response should explain the condition and may include a Retry-After header telling the client how long to wait, as either a number of seconds or an HTTP date.
Crucially, 429 responses are not meant to be cached by shared caches by default, and the server can apply the limit however it likes — per IP, per account, per API key. It is the server's statement that this particular caller is going too fast, not that the request was malformed. The same request, sent more slowly, would succeed.
It is worth separating 429 from the status codes it is often confused with. A 401 Unauthorized or 403 Forbidden means the caller lacks valid credentials or permission — a statement about who they are. A 503 Service Unavailable means the whole server is temporarily overloaded or down — a statement about the server. A 429 is narrower and more precise: the caller is authenticated-or-not as usual, the server is healthy, but this client has simply made too many requests too quickly. That precision is why it is the right code for a login rate limit: it does not leak whether the credentials were valid (which would help an attacker), it just says “too fast.”
The Retry-After header
Retry-After turns a bare rejection into a useful instruction. Well-behaved clients (your own front-end, mobile app, or a partner integration) read it and back off politely instead of retrying immediately and making things worse. It takes two forms:
Retry-After: 120 # wait 120 seconds
Retry-After: Wed, 01 Jul 2026 12:00:00 GMT # wait until this dateFor attackers running automated tools, the 429 itself is the deterrent: it means their high-throughput guessing has been throttled to a crawl. For honest clients, Retry-After is a courtesy that keeps your API pleasant to integrate with. When the header is absent, a naive client often retries immediately in a tight loop, which only piles more load onto an already-limited endpoint — so returning it is both polite and practical. The seconds form is simplest for a fixed cool-down; the HTTP-date form suits a limit that resets at a known wall-clock time.
How to return a 429 correctly
Whatever the framework, the shape of the response is the same: status 429, a Retry-After header, and a short body explaining the condition. In practice you rarely write this by hand — the rate-limiting middleware for your stack emits it for you once you configure a window and a limit — but it helps to know the exact bytes it should produce, because that is what a checker and an attacker both observe.
Raw HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{"error": "too_many_requests", "retry_after": 60}Node / Express — express-rate-limit
import rateLimit from "express-rate-limit";
// standardHeaders: true makes the middleware emit Retry-After on a 429
const limiter = rateLimit({ windowMs: 60_000, limit: 10, standardHeaders: true });
app.post("/api/auth/login", limiter, loginHandler);
// Or set it by hand:
res.status(429).set("Retry-After", "60").json({ error: "too_many_requests" });Django
from django.http import JsonResponse
resp = JsonResponse({"error": "too_many_requests"}, status=429)
resp["Retry-After"] = "60"
return respJava / Spring
return ResponseEntity.status(429)
.header("Retry-After", "60")
.body(Map.of("error", "too_many_requests"));Ruby / Rack::Attack
# Rack::Attack returns 429 automatically; customise the body/headers:
Rack::Attack.throttled_responder = lambda do |req|
[429, { "Content-Type" => "application/json", "Retry-After" => "60" },
[{ error: "too_many_requests" }.to_json]]
endRetry-After header, or putting the rate limit only on the HTML login page while the JSON API route that checks the password answers 200 forever. The checker (and attackers) hit the API endpoint — make sure that is the one returning 429.What the checker reports
The free SecScan Login Rate Limit Checker sends a small burst of requests with provably fake credentials and reports your login page as protected if it sees a 429 or Retry-After at any point in the burst. If it never does, the endpoint has no visible rate limit — the fix is to return the 429 shown above.
A 429 is a passive, observable configuration fact: any client that sends a short burst of requests can see whether one comes back, the same way a browser sees any status code. The checker never logs in and uses provably fake credentials, so a “protected” result reflects your server's real rate-limiting behaviour rather than any judgement about a specific account. If you add a limit, re-run the check to confirm the 429 fires on the exact endpoint that verifies passwords — the common miss is a limit on the form page but not the API.
Check whether your login page returns a 429 under repeated requests — free, no signup.
Run the login rate limit checker →The 429 is the mechanism behind the whole cluster: see the pillar, how to rate-limit your login page, and the two attacks it stops — brute force and credential stuffing.