Python — FastAPI Development
Login Rate Limiting and Brute-Force Protection in FastAPI
Direct answer
Protect a FastAPI login endpoint by counting failed attempts in Redis and limiting by both the client IP and the target account, so neither a single IP hammering many accounts nor many IPs hammering one account gets through. Return a generic 'invalid credentials' on every failure so attackers cannot tell which field was wrong, and clear the counters on a successful login. This stops the two common attacks — brute force against one account and credential stuffing across many.
An unprotected login endpoint is an open invitation to guess passwords at machine speed. The fix is not one lock but two, because attackers come from two directions: one IP trying thousands of passwords on one account, and a botnet trying one leaked password across thousands of accounts. Rate limiting by IP alone misses the second; by account alone misses the first. Here is a small, correct guard that covers both.
Key facts, with sources
- In the JetBrains Python Developers Survey 2024, which collected responses from more than 30,000 Python developers, FastAPI usage jumped from 29% to 38%, overtaking Django (35%) and Flask (34%) as the most-used Python web framework. (JetBrains Python Developers Survey 2024)
- The 2025 Stack Overflow Developer Survey shows FastAPI at 14.8% of respondents doing extensive work with it, edging out Flask at 14.4% and Django at 12.6%. (Stack Overflow Developer Survey 2025)
- FastAPI's official documentation cites independent TechEmpower benchmarks showing FastAPI applications running under Uvicorn as one of the fastest Python frameworks available, ranked only below Starlette and Uvicorn themselves. (FastAPI official documentation)
- FastAPI surpassed Flask in GitHub stars in December 2025, reaching roughly 88,000 stars compared to Flask's 68,400. (DZone)
- Industry analysis of FastAPI's 2025 growth reports about 40% year-over-year growth in job mentions and production adoption at companies including Uber, Netflix, and Microsoft. (byteiota)
Limit by IP and by account, together
Keep two counters per login attempt: one keyed on the client IP, one on the target email. If either crosses the threshold inside the window, reject with 429. This covers brute force (the account counter trips) and credential stuffing from one source (the IP counter trips). A distributed attack still gets slowed per-account, which is the part that actually protects the user.
from fastapi import Request, HTTPException
MAX_ATTEMPTS = 5
WINDOW_SECONDS = 900 # 15 minutes
# ponytail: Redis counters — the simplest correct approach. Reach for a
# gateway (nginx, an API gateway) or slowapi only if you outgrow this.
def check_login_rate(request: Request, email: str, redis):
keys = (f"login:ip:{request.client.host}", f"login:acct:{email.lower()}")
for key in keys:
attempts = redis.incr(key)
if attempts == 1:
redis.expire(key, WINDOW_SECONDS) # start the window on first miss
if attempts > MAX_ATTEMPTS:
raise HTTPException(429, "too many attempts, try again later")
def reset_login_counters(request: Request, email: str, redis):
redis.delete(f"login:ip:{request.client.host}", f"login:acct:{email.lower()}")Wire it into login with generic errors
Check the rate limit before verifying the password, count only failures, and clear the counters on success so a legitimate user who fumbles once then logs in is not penalised. Critically, return the same 401 message whether the email is unknown or the password is wrong — a specific 'no such user' tells an attacker which accounts exist.
@router.post("/auth/login")
def login(body: LoginIn, request: Request,
db: Session = Depends(get_db), redis = Depends(get_redis)):
check_login_rate(request, body.email, redis)
user = authenticate(db, body.email, body.password)
if not user:
# Generic — never reveal whether it was the email or the password.
raise HTTPException(401, "invalid credentials")
reset_login_counters(request, body.email, redis) # clear on success
return issue_tokens(user)Behind a proxy, trust the right IP
If FastAPI sits behind a load balancer or CDN, request.client.host is the proxy's IP, not the user's — so every request shares one counter and you either lock out everyone or no one. Read the real client IP from the X-Forwarded-For header (validated against trusted proxies), or configure your server's proxy headers. Getting this wrong silently defeats the whole guard.
When to hire senior help
Bring in senior help when your API needs to handle real concurrency, when you are designing service boundaries and auth for the first time, or when an existing FastAPI codebase mixes sync and async code and latency is degrading. An experienced engineer can usually diagnose event-loop blocking and connection-pool misconfiguration in days, which is far cheaper than re-architecting after launch. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Python — FastAPI Development projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Calling blocking libraries (classic SQLAlchemy sessions, requests, heavy file I/O) inside async def endpoints, which stalls the event loop and erases FastAPI's concurrency advantage
- ✕Deploying a single Uvicorn process with no process manager or worker scaling, leaving most CPU cores idle under production load
- ✕Treating the auto-generated OpenAPI docs as a versioning strategy, then breaking mobile and partner clients when response schemas change
- ✕Running large payloads through deeply nested Pydantic models on every request and response, adding serialization latency that shows up only at scale
Frequently asked questions
Why limit by account and not just by IP?
Because credential-stuffing attacks come from many IPs trying one leaked password across many accounts — an IP-only limit never trips. Limiting by account slows the attack on each individual user, which is the protection that matters. Use both counters together.
Should I lock the account after too many failures?
A temporary throttle (429 for a cooldown window) is safer than a hard lock, because a hard lock lets an attacker deliberately lock out a victim by failing logins on purpose. Time-boxed backoff plus optional step-up (captcha or email challenge) after repeated failures is the usual balance.
Do I need Redis, or is in-memory enough?
In-memory works for a single process but breaks the moment you run more than one instance — each has its own counters, so the real limit is multiplied by your instance count. Use a shared store like Redis so the limit holds across all workers.
Is FastAPI mature enough for production?
Yes. It was the most-used Python web framework in the JetBrains 2024 survey at 38%, and companies including Uber, Netflix, and Microsoft run it in production. The ecosystem for auth, ORMs, and testing is now well established.
How much faster is FastAPI than Flask or Django really?
Independent TechEmpower benchmarks place FastAPI among the fastest Python frameworks, and published comparisons show several times Flask's throughput on I/O-bound endpoints. For CPU-bound work or database-bottlenecked apps, the framework choice matters far less than query and infrastructure design.
Should we pick FastAPI or Django for a new SaaS backend?
FastAPI suits API-first products, microservices, and ML model serving because of async support and automatic OpenAPI docs. Django ships batteries included (admin, ORM, auth) and is often faster to launch a conventional CRUD product. Many teams run both, per the JetBrains finding that a third of Django developers also use Flask or FastAPI.
Bottom line: Dhairya Senjaliya ships Python — FastAPI Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.