Python — FastAPI Development
Refresh Token Rotation and Reuse Detection with FastAPI
Direct answer
Refresh token rotation issues a brand-new refresh token on every refresh and immediately invalidates the old one. The security payoff comes from pairing rotation with reuse detection: store each refresh token as a hashed row tied to a token family, and if a token that was already rotated is presented again, revoke the whole family and force re-login. That one rule turns a stolen refresh token from a permanent backdoor into a one-shot that trips an alarm.
A leaked access token expires in fifteen minutes. A leaked refresh token, if you never rotate it, is a login that lasts as long as your refresh window — often two weeks. Rotation with reuse detection closes that gap, and it is the part of a React Native auth flow that most tutorials skip. This is the server-side model I use with FastAPI, and how it interacts with the mobile refresh race covered in the JWT auth guide.
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)
Why rotation alone is not enough
Rotation on its own just moves the target. If every refresh mints a new token but you never notice the old one being replayed, an attacker who copied a refresh token can keep refreshing in parallel with the real user and stay logged in indefinitely. What actually protects the session is detection: the moment a token you already retired shows up again, you know either the client or the attacker is holding a stale copy — and you cannot tell which, so you revoke everything and make both re-authenticate.
The mechanism is a token family. Every login starts a family; every rotation adds a link to the chain but keeps the same family_id. Retired links are marked revoked, not deleted. Presenting a revoked link is the signal that something forked the chain.
Model refresh tokens as a hashed family
Store one row per issued refresh token. Never store the raw token — hash it, so a database leak does not hand out live sessions. Group rows by family_id so reuse detection can revoke the whole chain in a single query.
class RefreshToken(Base):
__tablename__ = "refresh_tokens"
id = Column(String, primary_key=True) # the jti of this link
family_id = Column(String, index=True) # shared across a rotation chain
user_id = Column(String, index=True)
token_hash = Column(String, index=True) # sha256 of the raw token
revoked = Column(Boolean, default=False)
expires_at = Column(DateTime)The rotate-and-detect endpoint
The refresh endpoint does four things in order: look the token up by hash, reject unknown or expired tokens, detect reuse of a retired token, and only then rotate. The reuse branch is the whole point — it revokes the family before issuing anything.
import hashlib, secrets
from datetime import datetime, timedelta, timezone
def _hash(raw: str) -> str:
return hashlib.sha256(raw.encode()).hexdigest()
@router.post("/auth/refresh")
def refresh(body: RefreshIn, db: Session = Depends(get_db)):
row = db.query(RefreshToken).filter_by(token_hash=_hash(body.refresh_token)).first()
# Unknown or expired -> reject outright.
if row is None or row.expires_at < datetime.now(timezone.utc):
raise HTTPException(401, "invalid refresh token")
# Reuse detection: a revoked link was already rotated once. Its reappearance
# means the chain forked -> burn the whole family, force a fresh login.
if row.revoked:
db.query(RefreshToken).filter_by(family_id=row.family_id).update({"revoked": True})
db.commit()
raise HTTPException(401, "token reuse detected - session revoked")
# Rotate: retire the presented link, mint a new one in the same family.
row.revoked = True
new_raw = secrets.token_urlsafe(32)
db.add(RefreshToken(
id=secrets.token_hex(16),
family_id=row.family_id,
user_id=row.user_id,
token_hash=_hash(new_raw),
expires_at=datetime.now(timezone.utc) + timedelta(days=14),
))
db.commit()
return {"access_token": mint_access_token(row.user_id), "refresh_token": new_raw}Why this makes the mobile refresh race non-optional
On the client, the deduplicated single-flight refresh from the React Native JWT guide stops being a nice-to-have and becomes mandatory. Without rotation, two parallel 401s each firing their own refresh is merely wasteful. With rotation, the first refresh retires the token and the second arrives holding a value you just revoked — which trips reuse detection and logs the user out for no reason.
So the two halves are a matched pair: the server rotates and detects reuse, and the client must funnel every concurrent 401 through one shared refresh promise so exactly one rotation happens per cycle. Ship one without the other and you get either an insecure session or a client that logs itself out at random.
Logout and cleanup
Logout revokes the current family so the refresh token cannot be reused after sign-out. Give each device its own family at login, so signing out on a phone does not kill the tablet. Run a periodic job to delete rows past expires_at — reuse detection only needs live and recently-retired links, not months of history.
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
Should refresh tokens be stored in the database?
Yes — store a hash of each one. Access tokens stay stateless and are validated by signature, but refresh tokens need a server-side record so you can rotate them, revoke a family on reuse, and invalidate sessions on logout. Hash them so a database leak does not expose live sessions.
How do I support the same user on two devices?
Start a separate family per login. Each device carries its own chain, so rotating or revoking one device's tokens leaves the other's session untouched, and a reuse event on one device only burns that device's family.
Does rotation break the React Native refresh interceptor?
Only if the client fires more than one refresh per cycle. Rotation requires the deduplicated single-flight refresh — a shared promise every concurrent 401 awaits — so exactly one new token is minted. Without that, a second parallel refresh sends an already-rotated token and reuse detection logs the user out.
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.