Skip to content
Back to field notes
AuthJune 6, 20266 min read

Login endpoint rate limiting for vibe-coded apps

An AI-generated login route usually accepts unlimited attempts, so one script can run thousands of password guesses against it. This note shows how to add a server-side rate limit and confirm it before you share the URL.

AuthRate limitingAbuse

An unthrottled login route accepts unlimited guesses

The concrete launch risk is narrow and easy to state: your login endpoint accepts an unlimited number of attempts per second, from one caller, with no slowdown and no lockout. AI tools generate the happy path. They check the password, return a session on success and an error on failure, and stop there. The missing piece is the rate limit login endpoint guard that stops the same IP or account from trying ten thousand passwords in a minute.

That gap turns three classic attacks from theoretical into routine. Credential stuffing replays username and password pairs leaked from other breaches against your form. Brute forcing tries many passwords against one known account. Both depend on volume, and volume is exactly what an unthrottled endpoint allows. A scripted client does not use your UI; it posts directly to the route, so the form's own behavior is irrelevant.

Separate what is observable from what is only possible. What you can observe directly is whether the endpoint slows or rejects rapid repeated requests. Whether an attacker actually has a working credential list is not something you can see, and not something you need to assume. The control you are checking is volume handling, and it should hold regardless of who is calling.

  • This is an abuse and availability problem, not a broken login. A correct password still works; the gap is that wrong passwords cost the attacker nothing.
  • Rate limiting is about request volume per caller, not about whether a given credential is valid.
  • It applies to any auth-adjacent route that an attacker can hammer: password login, magic-link issue, password reset, and OTP verification.

Why AI-built apps ship without it

A rate limit is not part of the minimal code that makes login work, so a generator that optimizes for a working feature leaves it out. The prompt "add email and password login" produces a handler that authenticates correctly on the first try. It looks finished because it is finished as a feature. The throttling layer is a separate, cross-cutting concern that nothing in the happy path forces you to add.

There is also a hosting assumption baked into a lot of generated code: that some upstream layer handles abuse. Sometimes a platform does apply a coarse network-level limit, but it is rarely tuned to a login route, and on a self-hosted deploy there may be nothing in front of the app at all. Generated code that comments "rate limiting handled by the proxy" is making a guess about your infrastructure that may be wrong.

  • The login handler works without a limiter, so the missing limiter is invisible from inside the app.
  • Fast deploy means the public URL exists before anyone load-tests the auth routes.
  • Per-route tuning is the part that gets skipped: a global limiter that allows hundreds of requests a second is fine for a static page and useless on a login form.

Verify your own endpoint before launch

You can confirm this on your own app safely, against your own account, without attacking anyone. The check is to send repeated requests to your login route and watch how it responds.

From the command line, post the same login request in a tight loop with curl, using a deliberately wrong password and a test account you control. Watch the HTTP status of each response. A protected endpoint will start returning 429 Too Many Requests after a small number of attempts, ideally with a Retry-After header telling the client how long to wait. An unprotected endpoint returns 401 or 200 for every request, no matter how fast you send them. Keep the volume modest; you are checking for the presence of a limit, not stress-testing your own server.

From the browser, open DevTools, the Network tab, attempt a few failed logins, and inspect the response headers and status codes. Look for the same signal: does the status change to 429 after several tries, or does it stay at a plain auth error indefinitely.

  • Safer result: after a handful of rapid failed attempts, the endpoint returns 429, often with Retry-After, and stops processing guesses for a window.
  • Suspicious result: every request returns the same auth error with no slowdown, even at high speed. That endpoint has no effective per-caller limit.
  • Test each auth route separately. A limit on the login route does not mean the password-reset or OTP-verify route has one; attackers will probe whichever is open.

Add a server-side limit, and know what it does not cover

The fix must run on the server, before the password is checked. A client-side counter or a disabled submit button does nothing, because the attacker bypasses your UI and posts straight to the route. The limiter has to live where the request is handled.

A real configuration tracks attempts per identifier, usually a combination of client IP and the submitted account, in a shared store such as Redis so the count survives across server instances and restarts. A common shape is a small number of attempts allowed per account per short window, with a longer cooldown after repeated failures, returning 429 with Retry-After when the limit trips. Pair this with an account-level lockout or step-up challenge so an attacker cannot dodge an IP limit by rotating addresses. Apply the same limiter to every sensitive auth route, not just the main login form.

  • Enforce on the server, in the request path, before authentication runs.
  • Key on both IP and account so neither a single IP nor a single targeted account can absorb unlimited tries.
  • Use a shared store, not in-memory state, so the limit holds across instances and after a restart.

Rate limiting reduces the speed and volume of guessing. It does not make a weak password strong, and it does not stop a slow, distributed attack that stays under your threshold from many IPs. It is a brute-force and credential-stuffing brake, not a substitute for strong password rules, multi-factor authentication, or breached-password checks. Treat it as one layer.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check probes your auth endpoint from the public surface and reads how it responds to repeated requests. If the endpoint returns rate-limit signals such as a 429 status and a Retry-After header, the scan can see that a limit is present; if it keeps answering with no throttle, that absence shows up as a finding with severity and fix direction. It checks the response an outside caller actually gets.

What it cannot do is verify that your limit is correct. Whether the window, the threshold, the per-account keying, and the lockout logic actually hold under a real distributed attack is enforcement behavior that lives in your code and your shared store. Confirming that needs manual testing or load testing against the running route, not a single public-surface read.

Next step: pick your login route, run the curl loop against a test account, and confirm you see 429 with Retry-After after a few tries. Then check your reset and OTP routes the same way, and run a focused Launch Check against the public URL before sharing it.

A clean Launch Check here is a weak positive signal, not proof. The scan can tell that the endpoint responded with a limit on the requests it sent; it cannot tell you the threshold is low enough, that the key includes the account and not just the IP, or that reset and OTP routes share the same protection. Treat a missing limit as a launch blocker, and treat a present one as a prompt to verify the config, not a guarantee.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.