{"slug": "fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate", "title": "FastAPI Dependency Injection for Anthropic Claude: Isolating API Keys and Rate Limits Per Tenant", "summary": "CitizenApp engineers used FastAPI's dependency injection to isolate Anthropic Claude API keys and rate limits per tenant, replacing a fragile global key setup that throttled all customers when one hit quota. The system composes per-request dependencies for tenant config, rate-limit buckets, and Claude clients, avoiding middleware spaghetti and context-var leaks.", "body_md": "When CitizenApp hit 15 tenants, I realized our single global Claude API key was a ticking time bomb. One customer's agentic loop burning through their quota would throttle everyone else. Worse, we had no way to enforce per-tenant rate limits without adding middleware spaghetti that would make debugging a nightmare.\n\nThe fix? Lean into FastAPI's dependency injection system to make tenant-specific Claude clients and rate-limit buckets first-class citizens. No globals, no thread locks, no \"who's using the API key right now?\" detective work.\n\nMiddleware runs once per request, which means you'd have to either:\n\nI've been burned by this. We had a `get_current_tenant()`\n\nmiddleware that set `request.state.tenant_id`\n\n, but then handlers had to manually fetch the API key and pass it around. When we added background tasks that queried Claude, the entire pattern fell apart—context vars leaked, rate limits weren't enforced, and debugging which tenant was which took hours.\n\nFastAPI's Depends system solves this cleanly: dependencies are resolved per-request (or per-dependency cache if you use `use_cache=True`\n\n), and they compose naturally. Your handler doesn't care *how* it gets a Claude client—it just declares what it needs.\n\nLet's start with the data layer. You need a way to fetch tenant configuration and manage rate limits:\n\n``` python\n# models.py\nfrom sqlalchemy import Column, Integer, String, Float\nfrom sqlalchemy.orm import Session\nfrom datetime import datetime, timedelta\n\nclass Tenant(Base):\n    __tablename__ = \"tenants\"\n\n    id = Column(Integer, primary_key=True)\n    name = Column(String, unique=True)\n    anthropic_api_key = Column(String)  # Encrypted in production\n    max_requests_per_minute = Column(Integer, default=60)\n    preferred_model = Column(String, default=\"claude-3-5-sonnet-20241022\")\n\nclass RateLimitBucket:\n    \"\"\"In-memory rate limit tracker. Use Redis for distributed deployments.\"\"\"\n    def __init__(self, max_requests: int, window_seconds: int = 60):\n        self.max_requests = max_requests\n        self.window_seconds = window_seconds\n        self.requests: list[datetime] = []\n\n    def is_allowed(self) -> bool:\n        now = datetime.utcnow()\n        cutoff = now - timedelta(seconds=self.window_seconds)\n        self.requests = [req for req in self.requests if req > cutoff]\n\n        if len(self.requests) < self.max_requests:\n            self.requests.append(now)\n            return True\n        return False\n```\n\nNow the dependency providers:\n\n``` python\n# dependencies.py\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import HTTPBearer, HTTPAuthCredential\nfrom sqlalchemy.orm import Session\nimport anthropic\nfrom functools import lru_cache\n\nsecurity = HTTPBearer()\n\ndef get_db() -> Session:\n    # Standard FastAPI DB dependency\n    db = SessionLocal()\n    try:\n        yield db\n    finally:\n        db.close()\n\ndef get_tenant_id(credentials: HTTPAuthCredential = Depends(security)) -> int:\n    \"\"\"Extract and validate the tenant from JWT or API key header.\"\"\"\n    # In reality, decode your JWT here\n    try:\n        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[\"HS256\"])\n        tenant_id = payload.get(\"tenant_id\")\n        if not tenant_id:\n            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)\n        return tenant_id\n    except jwt.InvalidTokenError:\n        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)\n\ndef get_tenant(\n    tenant_id: int = Depends(get_tenant_id),\n    db: Session = Depends(get_db)\n) -> Tenant:\n    \"\"\"Fetch the tenant record. This runs once per request.\"\"\"\n    tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()\n    if not tenant:\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)\n    return tenant\n\n# Global cache for rate-limit buckets and Claude clients\n# Keys are tenant IDs. In production, use Redis.\n_rate_limit_buckets: dict[int, RateLimitBucket] = {}\n_claude_clients: dict[int, anthropic.Anthropic] = {}\n\ndef get_claude_client(tenant: Tenant = Depends(get_tenant)) -> anthropic.Anthropic:\n    \"\"\"\n    Get or create a Claude client for this tenant.\n    Reused within the request if other dependencies need it.\n    \"\"\"\n    if tenant.id not in _claude_clients:\n        _claude_clients[tenant.id] = anthropic.Anthropic(\n            api_key=tenant.anthropic_api_key\n        )\n    return _claude_clients[tenant.id]\n\ndef get_rate_limit_bucket(tenant: Tenant = Depends(get_tenant)) -> RateLimitBucket:\n    \"\"\"\n    Get or create the rate-limit bucket for this tenant.\n    Separate from Claude client so you can inject one without the other if needed.\n    \"\"\"\n    if tenant.id not in _rate_limit_buckets:\n        _rate_limit_buckets[tenant.id] = RateLimitBucket(\n            max_requests=tenant.max_requests_per_minute\n        )\n    return _rate_limit_buckets[tenant.id]\n\ndef check_rate_limit(bucket: RateLimitBucket = Depends(get_rate_limit_bucket)) -> None:\n    \"\"\"Dependency that enforces the rate limit. Use in handlers that call Claude.\"\"\"\n    if not bucket.is_allowed():\n        raise HTTPException(\n            status_code=status.HTTP_429_TOO_MANY_REQUESTS,\n            detail=\"Rate limit exceeded for this tenant\"\n        )\n```\n\nNow your handlers are clean and testable:\n\n``` python\n# routes.py\nfrom fastapi import FastAPI, Depends\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass ChatRequest(BaseModel):\n    message: str\n\n@app.post(\"/chat\")\nasync def chat(\n    req: ChatRequest,\n    client: anthropic.Anthropic = Depends(get_claude_client),\n    tenant: Tenant = Depends(get_tenant),\n    _: None = Depends(check_rate_limit),  # Rate limit is checked first\n):\n    \"\"\"\n    The handler only declares what it needs.\n    FastAPI wires up the tenant, validates their rate limit, and gives us a pre-configured Claude client.\n    \"\"\"\n    response = client.messages.create(\n        model=tenant.preferred_model,\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": req.message}]\n    )\n    return {\"response\": response.content[0].text}\n\n@app.post(\"/batch-analyze\")\nasync def batch_analyze(\n    files: list[UploadFile],\n    client: anthropic.Anthropic = Depends(get_claude_client),\n    bucket: RateLimitBucket = Depends(get_rate_limit_bucket),\n    tenant: Tenant = Depends(get_tenant),\n):\n    \"\"\"\n    You can also use the bucket directly if you need fine-grained control.\n    E.g., consume multiple tokens per request.\n    \"\"\"\n    results = []\n    for file in files:\n        if not bucket.is_allowed():\n            return {\"error\": \"Rate limit exceeded mid-batch\", \"processed\": len(results)}\n\n        content = await file.read()\n        response = client.messages.create(\n            model=tenant.preferred_model,\n            max_tokens=512,\n            messages=[{\"role\": \"user\", \"content\": f\"Analyze: {content.decode()}\"}]\n        )\n        results.append(response.content[0].text)\n\n    return {\"results\": results}\n```\n\n`get_claude_client`\n\ndepends on `get_tenant`\n\n, which depends on `get_tenant_id`\n\n. You can test each layer independently.I initially used `lru_cache`\n\non `get_tenant()`\n\nto avoid DB hits. **Don't.** If a tenant's API key rotates mid-day, cached tenants still have the old key. Instead:\n\n``` python\n# Bad\n@lru_cache(maxsize=128)\ndef get_tenant(tenant_id: int, db: Session):\n    return db.query(Tenant).filter(Tenant.id == tenant_id).first()\n\n# Good\ndef get_tenant(tenant_id: int = Depends(get_tenant_id), db: Session = Depends(get_db)):\n    return db.query(Tenant).filter(Tenant.id == tenant_id).first()\n```\n\nThe DB query is cheap. Stale credentials are expensive.\n\nFor distributed deployments with multiple FastAPI instances, replace in-memory buckets with Redis:\n\n``` python\npython\nimport redis\nfrom datetime import datetime, timedelta\n\ndef get_rate_limit_bucket(tenant: Tenant = Depends(get_tenant)) -> int:\n    \"\"\"Return the tenant ID; use Redis in the handler.\"\"\"\n    return tenant.id\n\n@app.post(\"/chat\")\nasync def chat(\n    req: ChatRequest,\n    client: anthropic.Anthropic = Depends\n```\n\n", "url": "https://wpnews.pro/news/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate", "canonical_source": "https://dev.to/uaslimcreate/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate-limits-per-tenant-4n7j", "published_at": "2026-08-15 06:02:17+00:00", "updated_at": "2026-08-15 06:11:09.245878+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["CitizenApp", "FastAPI", "Anthropic", "Claude"], "alternates": {"html": "https://wpnews.pro/news/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate", "markdown": "https://wpnews.pro/news/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate.md", "text": "https://wpnews.pro/news/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate.txt", "jsonld": "https://wpnews.pro/news/fastapi-dependency-injection-for-anthropic-claude-isolating-api-keys-and-rate.jsonld"}}