Python — FastAPI Development

FastAPI Rate Limiting and API Keys

Direct answer

In FastAPI I implement API keys as hashed database records validated in a dependency, and rate limiting as Redis counters keyed on the API key rather than the IP address. Store only a SHA-256 hash of each key, keep a short plaintext prefix for lookup and support, and enforce two layers: a per-minute burst limit and a daily quota, both communicated through rate-limit headers and 429 responses with Retry-After.

The moment an API serves anyone outside your own team — partners, paying customers, a public tier — keys and limits stop being optional hardening and become the product's pricing and abuse boundary. This is the implementation I ship and the design decisions behind it.

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)

Layer your limits: edge first, application second

Rate limiting lives at two altitudes and they solve different problems. The edge — your CDN, load balancer, or reverse proxy — should absorb blunt abuse: floods from single IPs, obvious bot storms. That traffic should never reach Python at all, because rejecting a request in your application still costs you a worker slot and often a database lookup.

The application layer is where business logic lives: this key gets sixty requests a minute, that enterprise customer gets six hundred, the free tier gets a daily cap. Only your application knows who the caller is and what they paid for, so identity-based limits belong in FastAPI code. Teams that pick one layer end up either DDoS-fragile or unable to enforce plans.

API key design: hash at rest, prefix for lookup

Treat API keys like passwords: generate them with a CSPRNG, show the full key exactly once at creation, and store only a SHA-256 hash. A database leak then exposes no usable credentials. Keep the first several characters as a plaintext prefix column — it lets your dashboard display which key is which, lets support identify a key a customer pastes in half-redacted, and gives your logs something safe to record.

Every key row carries its scopes, its plan limits, created and last-used timestamps, and a revoked_at column. Last-used matters more than it looks: when a customer asks whether an old key is safe to rotate away, that column answers instead of guesswork.

API key validation as a dependency
import hashlib

from fastapi import Depends, HTTPException, Security
from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)


async def require_api_key(
    raw_key: str | None = Security(api_key_header),
    db: AsyncSession = Depends(get_db),
) -> ApiKey:
    if not raw_key:
        raise HTTPException(status_code=401, detail="Missing API key")
    key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
    record = await api_keys.get_by_hash(db, key_hash)
    if record is None or record.revoked_at is not None:
        raise HTTPException(status_code=403, detail="Invalid or revoked API key")
    return record

Rate limiting with Redis counters

For per-key limits I usually skip middleware libraries and write a small Redis counter dependency — it is a dozen lines, keys off the authenticated identity instead of the IP, and composes naturally with the key-validation dependency. A fixed one-minute window with INCR and EXPIRE is simple and atomic; its known weakness is bursts straddling window boundaries, which a sliding-window or token-bucket script fixes if your product actually needs that precision. Most APIs I audit do not.

What matters more than the algorithm: fail open or closed deliberately. If Redis is down, I typically let traffic through and alert loudly — for most products, a few minutes of unmetered requests beats a total outage. For an expensive AI endpoint, I might choose the opposite.

Per-key fixed-window limiter
import time

from fastapi import Depends, HTTPException


async def enforce_rate_limit(key: ApiKey = Depends(require_api_key)) -> ApiKey:
    window = int(time.time() // 60)
    bucket = f"rl:{key.prefix}:{window}"
    count = await redis.incr(bucket)
    if count == 1:
        await redis.expire(bucket, 90)
    if count > key.requests_per_minute:
        raise HTTPException(
            status_code=429,
            detail="Rate limit exceeded",
            headers={"Retry-After": "60"},
        )
    return key

Quotas and communicating limits like a good citizen

Burst limits protect your infrastructure; quotas implement your pricing. I track a second, longer-window counter — daily or monthly requests per key — and return a distinct error code when a quota is exhausted, because clients should handle "slow down" and "upgrade your plan" differently.

Well-behaved APIs tell callers where they stand on every response: headers for the limit, remaining count, and reset time, plus Retry-After on 429s. Document the exact header names and the 429 body shape in your OpenAPI schema. Partners build their retry logic against whatever you expose, and vague limit behavior turns directly into integration support tickets and angry retry storms.

Rotation, revocation, and watching for abuse

Revocation must be immediate — a revoked_at timestamp checked on every request, not a cached flag with a day of lag, because the moment a customer revokes a key is precisely when they believe it leaked. If you cache validated keys for performance, keep the TTL short and flush the entry on revocation. Rotation should be graceful: let two keys stay active per customer so they cut over with zero downtime, then retire the old one.

Beyond enforcement, per-key metrics are your abuse radar. A key that suddenly shifts endpoints, triples volume, or starts failing validation on strange payloads is either compromised or misbehaving. Alerting on per-key anomalies has typically warned me before infrastructure dashboards noticed anything.

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

How should I store API keys for a FastAPI application?

Never in plaintext. Generate keys with a cryptographically secure random source, show the full key once at creation, and store only a SHA-256 hash plus a short plaintext prefix for identification in dashboards and support conversations. Include scopes, per-plan limits, last-used timestamps, and a revoked_at column so revocation takes effect on the very next request.

Should rate limits be per IP address or per API key?

Per API key for authenticated APIs. IP-based limits punish legitimate customers behind shared NATs and cloud egress points while barely slowing attackers who rotate addresses. Keep coarse IP-based protection at the edge for unauthenticated abuse, and enforce business limits — per-minute bursts and daily quotas tied to the customer's plan — on the authenticated key inside the application.

What should a 429 rate limit response include?

A Retry-After header with seconds until the caller may retry, headers reporting the limit, remaining requests, and reset time, and a body with a machine-readable error code that distinguishes a temporary burst limit from an exhausted plan quota. Documenting this exact shape in your OpenAPI schema lets partners implement correct backoff instead of hammering you with immediate retries.

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.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership