[{"data":1,"prerenderedAt":1180},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F":310,"surround-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F":1178},[4,96,212],{"title":5,"path":6,"stem":7,"children":8},"Advanced Pydantic Validation Serialization","\u002Fadvanced-pydantic-validation-serialization","advanced-pydantic-validation-serialization",[9,12,30,42,54,66,90],{"title":10,"path":6,"stem":11},"Advanced Pydantic Validation and Serialization","advanced-pydantic-validation-serialization\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Custom Validators and Field Constraints in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Findex",[17,18,24],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Creating Reusable Custom Validators in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcreating-reusable-custom-validators-in-pydantic\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Pydantic v2 Async Custom Validator: What to Do Instead","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fpydantic-v2-async-custom-validator\u002Findex",[29],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"JSON Schema Customization in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Findex",[35,36],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Customizing OpenAPI Schema Generation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fcustomizing-openapi-schema-generation-in-fastapi\u002Findex",[41],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"Nested Model Serialization in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Findex",[47,48],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Handling Deeply Nested JSON Models Efficiently","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fhandling-deeply-nested-json-models-efficiently\u002Findex",[53],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Performance Optimization for Pydantic Models in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Findex",[59,60],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Pydantic Model Serialization Performance in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fpydantic-model-serialization-performance\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Pydantic V2 Migration Guide for FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Findex",[71,72,78,84],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Migrate @validator to @field_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrate-validator-to-field-validator\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Migrating from Pydantic v1 to v2 Without Breaking APIs","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmigrating-from-pydantic-v1-to-v2-without-breaking-apis\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"model_config vs class Config in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Fmodel-config-vs-class-config\u002Findex",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Type Hinting and IDE Integration in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Findex",[95],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"Async Background Tasks Observability","\u002Fasync-background-tasks-observability","async-background-tasks-observability",[101,104,122,140,158,176,194],{"title":102,"path":98,"stem":103},"Async, Background Tasks, and Observability in FastAPI","async-background-tasks-observability\u002Findex",{"title":105,"path":106,"stem":107,"children":108},"Async Correctness and Concurrency in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Findex",[109,110,116],{"title":105,"path":106,"stem":107},{"title":111,"path":112,"stem":113,"children":114},"FastAPI async def vs def: Performance and When to Use Each","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance\u002Findex",[115],{"title":111,"path":112,"stem":113},{"title":117,"path":118,"stem":119,"children":120},"Fixing Blocking Calls in Async FastAPI Routes","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002Findex",[121],{"title":117,"path":118,"stem":119},{"title":123,"path":124,"stem":125,"children":126},"Async Database Sessions in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions","async-background-tasks-observability\u002Fasync-database-sessions\u002Findex",[127,128,134],{"title":123,"path":124,"stem":125},{"title":129,"path":130,"stem":131,"children":132},"Async SQLAlchemy Session per Request in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request","async-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request\u002Findex",[133],{"title":129,"path":130,"stem":131},{"title":135,"path":136,"stem":137,"children":138},"Fixing asyncpg Connection Pool Exhaustion in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion","async-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion\u002Findex",[139],{"title":135,"path":136,"stem":137},{"title":141,"path":142,"stem":143,"children":144},"Background Task Processing in FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing","async-background-tasks-observability\u002Fbackground-task-processing\u002Findex",[145,146,152],{"title":141,"path":142,"stem":143},{"title":147,"path":148,"stem":149,"children":150},"FastAPI BackgroundTasks vs Celery vs ARQ","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq","async-background-tasks-observability\u002Fbackground-task-processing\u002Ffastapi-backgroundtasks-vs-celery-vs-arq\u002Findex",[151],{"title":147,"path":148,"stem":149},{"title":153,"path":154,"stem":155,"children":156},"Running ARQ Workers with FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi","async-background-tasks-observability\u002Fbackground-task-processing\u002Frunning-arq-workers-with-fastapi\u002Findex",[157],{"title":153,"path":154,"stem":155},{"title":159,"path":160,"stem":161,"children":162},"Caching Strategies in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies","async-background-tasks-observability\u002Fcaching-strategies\u002Findex",[163,164,170],{"title":159,"path":160,"stem":161},{"title":165,"path":166,"stem":167,"children":168},"Cache Invalidation Patterns in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fcache-invalidation-patterns-in-fastapi\u002Findex",[169],{"title":165,"path":166,"stem":167},{"title":171,"path":172,"stem":173,"children":174},"Redis Response Caching in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi","async-background-tasks-observability\u002Fcaching-strategies\u002Fredis-response-caching-in-fastapi\u002Findex",[175],{"title":171,"path":172,"stem":173},{"title":177,"path":178,"stem":179,"children":180},"Observability and Tracing in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing","async-background-tasks-observability\u002Fobservability-and-tracing\u002Findex",[181,182,188],{"title":177,"path":178,"stem":179},{"title":183,"path":184,"stem":185,"children":186},"Instrumenting FastAPI with OpenTelemetry","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry","async-background-tasks-observability\u002Fobservability-and-tracing\u002Finstrumenting-fastapi-with-opentelemetry\u002Findex",[187],{"title":183,"path":184,"stem":185},{"title":189,"path":190,"stem":191,"children":192},"Structured JSON Logging with Request IDs in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids\u002Findex",[193],{"title":189,"path":190,"stem":191},{"title":195,"path":196,"stem":197,"children":198},"Rate Limiting and Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Findex",[199,200,206],{"title":195,"path":196,"stem":197},{"title":201,"path":202,"stem":203,"children":204},"FastAPI Rate Limiting with Redis and SlowAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi","async-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002Findex",[205],{"title":201,"path":202,"stem":203},{"title":207,"path":208,"stem":209,"children":210},"Per-User Token Bucket Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002Findex",[211],{"title":207,"path":208,"stem":209},{"title":213,"path":214,"stem":215,"children":216},"Core Architecture Routing Patterns","\u002Fcore-architecture-routing-patterns","core-architecture-routing-patterns",[217,220,232,250,268,280,292],{"title":218,"path":214,"stem":219},"FastAPI Core Architecture and Routing Patterns","core-architecture-routing-patterns\u002Findex",{"title":221,"path":222,"stem":223,"children":224},"Application Factory Patterns in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Findex",[225,226],{"title":221,"path":222,"stem":223},{"title":227,"path":228,"stem":229,"children":230},"FastAPI App Factory Pattern for Testing and Deployment","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Ffastapi-app-factory-pattern-for-testing-and-deployment\u002Findex",[231],{"title":227,"path":228,"stem":229},{"title":233,"path":234,"stem":235,"children":236},"Configuration Management in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management","core-architecture-routing-patterns\u002Fconfiguration-management\u002Findex",[237,238,244],{"title":233,"path":234,"stem":235},{"title":239,"path":240,"stem":241,"children":242},"Managing Environment Variables with Pydantic Settings","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fmanaging-environment-variables-with-pydantic-settings\u002Findex",[243],{"title":239,"path":240,"stem":241},{"title":245,"path":246,"stem":247,"children":248},"Pydantic Settings vs Dynaconf vs python-decouple","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fpydantic-settings-vs-dynaconf-vs-python-decouple\u002Findex",[249],{"title":245,"path":246,"stem":247},{"title":251,"path":252,"stem":253,"children":254},"Dependency Injection Strategies in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Findex",[255,256,262],{"title":251,"path":252,"stem":253},{"title":257,"path":258,"stem":259,"children":260},"Best Practices for FastAPI Dependency Injection","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fbest-practices-for-fastapi-dependency-injection\u002Findex",[261],{"title":257,"path":258,"stem":259},{"title":263,"path":264,"stem":265,"children":266},"Fixing FastAPI Dependency Injection Circular Imports","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Ffastapi-dependency-injection-circular-import-fix\u002Findex",[267],{"title":263,"path":264,"stem":265},{"title":269,"path":270,"stem":271,"children":272},"Error Handling and Global Exceptions in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Findex",[273,274],{"title":269,"path":270,"stem":271},{"title":275,"path":276,"stem":277,"children":278},"Global Exception Handlers for Consistent API Responses","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002Findex",[279],{"title":275,"path":276,"stem":277},{"title":281,"path":282,"stem":283,"children":284},"Middleware Implementation in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Findex",[285,286],{"title":281,"path":282,"stem":283},{"title":287,"path":288,"stem":289,"children":290},"Implementing Custom Middleware for Request Tracing","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fimplementing-custom-middleware-for-request-tracing\u002Findex",[291],{"title":287,"path":288,"stem":289},{"title":293,"path":294,"stem":295,"children":296},"Modular Router Organization in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Findex",[297,298,304],{"title":293,"path":294,"stem":295},{"title":299,"path":300,"stem":301,"children":302},"APIRouter Prefix vs Sub-Application Mounting in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fapirouter-prefix-vs-sub-application-mounting\u002Findex",[303],{"title":299,"path":300,"stem":301},{"title":305,"path":306,"stem":307,"children":308},"How to Structure Large FastAPI Projects for Scale","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fhow-to-structure-large-fastapi-projects-for-scale\u002Findex",[309],{"title":305,"path":306,"stem":307},{"id":311,"title":195,"body":312,"description":1147,"extension":1148,"meta":1149,"navigation":536,"path":196,"seo":1176,"stem":197,"__hash__":1177},"content\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Findex.md",{"type":313,"value":314,"toc":1139},"minimark",[315,319,323,347,482,487,490,739,743,746,953,959,963,966,970,973,1069,1073,1102,1106,1135],[316,317,195],"h1",{"id":318},"rate-limiting-and-throttling-in-fastapi",[320,321,322],"p",{},"Rate limiting is how an API protects itself: bounding how many requests any one client can make in a window so a single heavy or misbehaving caller cannot exhaust shared resources or degrade the service for everyone else.",[320,324,325,326,331,332,336,337,341,342,346],{},"This topic is part of ",[327,328,330],"a",{"href":329},"\u002Fasync-background-tasks-observability\u002F","Async, Background Tasks and Observability",". It is the edge defense in front of the ",[327,333,335],{"href":334},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002F","async database pool"," and works naturally as ",[327,338,340],{"href":339},"\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002F","middleware"," or a ",[327,343,345],{"href":344},"\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002F","dependency",".",[348,349,350,478],"figure",{},[351,352,360,361,360,365,360,369,360,378,360,385,360,393,360,399,360,404,360,409,360,414,360,421,360,427,360,430,360,435,360,440,360,447,360,452,360,457,360,461,360,468,360,471,360,474],"svg",{"viewBox":353,"role":354,"ariaLabelledBy":355,"xmlns":358,"style":359},"0 0 760 220","img",[356,357],"rl-title","rl-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;font-family:Inter,system-ui,sans-serif","\n  ",[362,363,364],"title",{"id":356},"A rate limiter backed by a shared store",[366,367,368],"desc",{"id":357},"A client request reaches the rate limiter, which checks a shared counter in Redis. If the client is under its limit the request proceeds to the handler; if over, the limiter returns a 429 with a Retry-After header.",[370,371],"rect",{"x":372,"y":373,"width":374,"height":375,"rx":376,"style":377},"24","88","120","46","8","fill:#F9FAFB;stroke:#D1D5DB;stroke-width:1.4px",[379,380,384],"text",{"x":381,"y":382,"style":383},"84","115","fill:#374151;font-size:11.5px;font-weight:600;text-anchor:middle","Client",[370,386],{"x":387,"y":388,"width":389,"height":390,"rx":391,"style":392},"196","80","150","62","9","fill:#FFF8EC;stroke:#E0A93B;stroke-width:1.8px",[379,394,398],{"x":395,"y":396,"style":397},"271","106","fill:#9A6B12;font-size:12px;font-weight:700;text-anchor:middle","Rate limiter",[379,400,403],{"x":395,"y":401,"style":402},"124","fill:#9A6B12;font-size:10px;text-anchor:middle","token bucket",[370,405],{"x":387,"y":406,"width":389,"height":407,"rx":376,"style":408},"166","40","fill:#FFFFFF;stroke:#4DB6AC;stroke-width:1.5px",[379,410,413],{"x":395,"y":411,"style":412},"191","fill:#00695C;font-size:11px;text-anchor:middle","Redis counter",[370,415],{"x":416,"y":417,"width":418,"height":419,"rx":376,"style":420},"430","32","160","44","fill:#E0F2F1;stroke:#009688;stroke-width:1.6px",[379,422,426],{"x":423,"y":424,"style":425},"510","59","fill:#00695C;font-size:11.5px;text-anchor:middle","under → handler",[370,428],{"x":416,"y":389,"width":418,"height":419,"rx":376,"style":429},"fill:#FFF4F4;stroke:#E08585;stroke-width:1.6px",[379,431,434],{"x":423,"y":432,"style":433},"172","fill:#B23A3A;font-size:11.5px;text-anchor:middle","over → 429",[379,436,439],{"x":423,"y":437,"style":438},"186","fill:#9A3030;font-size:10px;text-anchor:middle","Retry-After",[441,442],"line",{"x1":443,"y1":444,"x2":445,"y2":444,"style":446},"144","111","194","stroke:#00796B;stroke-width:1.8px",[448,449],"polygon",{"points":450,"style":451},"194,107 202,111 194,115","fill:#00796B",[441,453],{"x1":395,"y1":454,"x2":395,"y2":455,"style":456},"142","164","stroke:#4DB6AC;stroke-width:1.5px",[448,458],{"points":459,"style":460},"267,164 271,170 275,164","fill:#4DB6AC",[441,462],{"x1":463,"y1":464,"x2":465,"y2":466,"style":467},"346","100","428","58","stroke:#00796B;stroke-width:1.5px",[448,469],{"points":470,"style":451},"424,54 430,57 425,64",[441,472],{"x1":463,"y1":374,"x2":465,"y2":406,"style":473},"stroke:#B23A3A;stroke-width:1.5px",[448,475],{"points":476,"style":477},"424,161 430,165 421,169","fill:#B23A3A",[479,480,481],"figcaption",{},"The limiter consults a shared Redis counter so the limit holds across all workers, admitting requests under quota and rejecting the rest with a 429.",[483,484,486],"h2",{"id":485},"core-mechanics-a-shared-distributed-counter","Core Mechanics: A Shared, Distributed Counter",[320,488,489],{},"Because an API spans many workers and machines, the limit must be enforced from a shared store. Redis holds one authoritative counter per client so the policy is global, not per process. A token-bucket check, run atomically, decides whether the request proceeds.",[491,492,497],"pre",{"className":493,"code":494,"language":495,"meta":496,"style":496},"language-python shiki shiki-themes github-light","from fastapi import HTTPException, Request\nfrom redis.asyncio import Redis\n\n\nasync def enforce_limit(request: Request, redis: Redis, limit: int, window: int) -> None:\n    client_id = request.headers.get(\"x-api-key\", request.client.host)\n    key = f\"rl:{client_id}\"\n    # Atomic increment + first-hit expiry gives a fixed-window counter across workers.\n    count = await redis.incr(key)\n    if count == 1:\n        await redis.expire(key, window)\n    if count > limit:\n        ttl = await redis.ttl(key)\n        raise HTTPException(429, \"rate limit exceeded\",\n                            headers={\"Retry-After\": str(ttl)})\n","python","",[498,499,500,518,531,538,543,577,596,622,629,643,660,669,682,695,716],"code",{"__ignoreMap":496},[501,502,504,508,512,515],"span",{"class":441,"line":503},1,[501,505,507],{"class":506},"sD7c4","from",[501,509,511],{"class":510},"sgsFI"," fastapi ",[501,513,514],{"class":506},"import",[501,516,517],{"class":510}," HTTPException, Request\n",[501,519,521,523,526,528],{"class":441,"line":520},2,[501,522,507],{"class":506},[501,524,525],{"class":510}," redis.asyncio ",[501,527,514],{"class":506},[501,529,530],{"class":510}," Redis\n",[501,532,534],{"class":441,"line":533},3,[501,535,537],{"emptyLinePlaceholder":536},true,"\n",[501,539,541],{"class":441,"line":540},4,[501,542,537],{"emptyLinePlaceholder":536},[501,544,546,549,552,556,559,563,566,568,571,574],{"class":441,"line":545},5,[501,547,548],{"class":506},"async",[501,550,551],{"class":506}," def",[501,553,555],{"class":554},"s7eDp"," enforce_limit",[501,557,558],{"class":510},"(request: Request, redis: Redis, limit: ",[501,560,562],{"class":561},"sYu0t","int",[501,564,565],{"class":510},", window: ",[501,567,562],{"class":561},[501,569,570],{"class":510},") -> ",[501,572,573],{"class":561},"None",[501,575,576],{"class":510},":\n",[501,578,580,583,586,589,593],{"class":441,"line":579},6,[501,581,582],{"class":510},"    client_id ",[501,584,585],{"class":506},"=",[501,587,588],{"class":510}," request.headers.get(",[501,590,592],{"class":591},"sYBdl","\"x-api-key\"",[501,594,595],{"class":510},", request.client.host)\n",[501,597,599,602,604,607,610,613,616,619],{"class":441,"line":598},7,[501,600,601],{"class":510},"    key ",[501,603,585],{"class":506},[501,605,606],{"class":506}," f",[501,608,609],{"class":591},"\"rl:",[501,611,612],{"class":561},"{",[501,614,615],{"class":510},"client_id",[501,617,618],{"class":561},"}",[501,620,621],{"class":591},"\"\n",[501,623,625],{"class":441,"line":624},8,[501,626,628],{"class":627},"sAwPA","    # Atomic increment + first-hit expiry gives a fixed-window counter across workers.\n",[501,630,632,635,637,640],{"class":441,"line":631},9,[501,633,634],{"class":510},"    count ",[501,636,585],{"class":506},[501,638,639],{"class":506}," await",[501,641,642],{"class":510}," redis.incr(key)\n",[501,644,646,649,652,655,658],{"class":441,"line":645},10,[501,647,648],{"class":506},"    if",[501,650,651],{"class":510}," count ",[501,653,654],{"class":506},"==",[501,656,657],{"class":561}," 1",[501,659,576],{"class":510},[501,661,663,666],{"class":441,"line":662},11,[501,664,665],{"class":506},"        await",[501,667,668],{"class":510}," redis.expire(key, window)\n",[501,670,672,674,676,679],{"class":441,"line":671},12,[501,673,648],{"class":506},[501,675,651],{"class":510},[501,677,678],{"class":506},">",[501,680,681],{"class":510}," limit:\n",[501,683,685,688,690,692],{"class":441,"line":684},13,[501,686,687],{"class":510},"        ttl ",[501,689,585],{"class":506},[501,691,639],{"class":506},[501,693,694],{"class":510}," redis.ttl(key)\n",[501,696,698,701,704,707,710,713],{"class":441,"line":697},14,[501,699,700],{"class":506},"        raise",[501,702,703],{"class":510}," HTTPException(",[501,705,706],{"class":561},"429",[501,708,709],{"class":510},", ",[501,711,712],{"class":591},"\"rate limit exceeded\"",[501,714,715],{"class":510},",\n",[501,717,719,723,725,727,730,733,736],{"class":441,"line":718},15,[501,720,722],{"class":721},"sqxcx","                            headers",[501,724,585],{"class":506},[501,726,612],{"class":510},[501,728,729],{"class":591},"\"Retry-After\"",[501,731,732],{"class":510},": ",[501,734,735],{"class":561},"str",[501,737,738],{"class":510},"(ttl)})\n",[483,740,742],{"id":741},"production-implementation-policies-and-responses","Production Implementation: Policies and Responses",[320,744,745],{},"Apply a broad default and stricter per-route or per-tier limits, and always return a well-formed 429 so clients can back off.",[491,747,749],{"className":493,"code":748,"language":495,"meta":496,"style":496},"from typing import Annotated\n\nfrom fastapi import APIRouter, Depends, Request\n\n\ndef rate_limit(limit: int, window: int):\n    async def _dep(request: Request) -> None:\n        await enforce_limit(request, request.app.state.redis, limit, window)\n    return _dep\n\n\nrouter = APIRouter()\n\n\n# Expensive endpoint gets a tighter, per-route policy as a dependency.\n@router.post(\"\u002Fexports\", dependencies=[Depends(rate_limit(limit=5, window=60))])\nasync def create_export() -> dict[str, str]:\n    return {\"status\": \"queued\"}\n",[498,750,751,763,767,778,782,786,806,823,830,838,842,846,856,860,864,869,912,934],{"__ignoreMap":496},[501,752,753,755,758,760],{"class":441,"line":503},[501,754,507],{"class":506},[501,756,757],{"class":510}," typing ",[501,759,514],{"class":506},[501,761,762],{"class":510}," Annotated\n",[501,764,765],{"class":441,"line":520},[501,766,537],{"emptyLinePlaceholder":536},[501,768,769,771,773,775],{"class":441,"line":533},[501,770,507],{"class":506},[501,772,511],{"class":510},[501,774,514],{"class":506},[501,776,777],{"class":510}," APIRouter, Depends, Request\n",[501,779,780],{"class":441,"line":540},[501,781,537],{"emptyLinePlaceholder":536},[501,783,784],{"class":441,"line":545},[501,785,537],{"emptyLinePlaceholder":536},[501,787,788,791,794,797,799,801,803],{"class":441,"line":579},[501,789,790],{"class":506},"def",[501,792,793],{"class":554}," rate_limit",[501,795,796],{"class":510},"(limit: ",[501,798,562],{"class":561},[501,800,565],{"class":510},[501,802,562],{"class":561},[501,804,805],{"class":510},"):\n",[501,807,808,811,813,816,819,821],{"class":441,"line":598},[501,809,810],{"class":506},"    async",[501,812,551],{"class":506},[501,814,815],{"class":554}," _dep",[501,817,818],{"class":510},"(request: Request) -> ",[501,820,573],{"class":561},[501,822,576],{"class":510},[501,824,825,827],{"class":441,"line":624},[501,826,665],{"class":506},[501,828,829],{"class":510}," enforce_limit(request, request.app.state.redis, limit, window)\n",[501,831,832,835],{"class":441,"line":631},[501,833,834],{"class":506},"    return",[501,836,837],{"class":510}," _dep\n",[501,839,840],{"class":441,"line":645},[501,841,537],{"emptyLinePlaceholder":536},[501,843,844],{"class":441,"line":662},[501,845,537],{"emptyLinePlaceholder":536},[501,847,848,851,853],{"class":441,"line":671},[501,849,850],{"class":510},"router ",[501,852,585],{"class":506},[501,854,855],{"class":510}," APIRouter()\n",[501,857,858],{"class":441,"line":684},[501,859,537],{"emptyLinePlaceholder":536},[501,861,862],{"class":441,"line":697},[501,863,537],{"emptyLinePlaceholder":536},[501,865,866],{"class":441,"line":718},[501,867,868],{"class":627},"# Expensive endpoint gets a tighter, per-route policy as a dependency.\n",[501,870,872,875,878,881,883,886,888,891,894,896,899,901,904,906,909],{"class":441,"line":871},16,[501,873,874],{"class":554},"@router.post",[501,876,877],{"class":510},"(",[501,879,880],{"class":591},"\"\u002Fexports\"",[501,882,709],{"class":510},[501,884,885],{"class":721},"dependencies",[501,887,585],{"class":506},[501,889,890],{"class":510},"[Depends(rate_limit(",[501,892,893],{"class":721},"limit",[501,895,585],{"class":506},[501,897,898],{"class":561},"5",[501,900,709],{"class":510},[501,902,903],{"class":721},"window",[501,905,585],{"class":506},[501,907,908],{"class":561},"60",[501,910,911],{"class":510},"))])\n",[501,913,915,917,919,922,925,927,929,931],{"class":441,"line":914},17,[501,916,548],{"class":506},[501,918,551],{"class":506},[501,920,921],{"class":554}," create_export",[501,923,924],{"class":510},"() -> dict[",[501,926,735],{"class":561},[501,928,709],{"class":510},[501,930,735],{"class":561},[501,932,933],{"class":510},"]:\n",[501,935,937,939,942,945,947,950],{"class":441,"line":936},18,[501,938,834],{"class":506},[501,940,941],{"class":510}," {",[501,943,944],{"class":591},"\"status\"",[501,946,732],{"class":510},[501,948,949],{"class":591},"\"queued\"",[501,951,952],{"class":510},"}\n",[320,954,955,956,346],{},"The library-based approach using SlowAPI and a token bucket is detailed in ",[327,957,201],{"href":958},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002F",[483,960,962],{"id":961},"async-and-performance-notes","Async and Performance Notes",[320,964,965],{},"The limiter runs on every request, so its store calls must be async and cheap — a single atomic Redis operation per check. Prefer a Lua script or a token-bucket library that performs the read-modify-write atomically, avoiding a race where two workers both read under the limit and both admit. Keep keys short-lived so memory stays bounded.",[483,967,969],{"id":968},"testing-strategy","Testing Strategy",[320,971,972],{},"Assert that the limit triggers and that the response is well-formed:",[491,974,976],{"className":493,"code":975,"language":495,"meta":496,"style":496},"def test_rate_limit_returns_429(client):\n    for _ in range(5):\n        assert client.post(\"\u002Fexports\").status_code == 200\n    blocked = client.post(\"\u002Fexports\")\n    assert blocked.status_code == 429\n    assert \"Retry-After\" in blocked.headers      # Lets clients back off correctly.\n",[498,977,978,988,1008,1026,1040,1053],{"__ignoreMap":496},[501,979,980,982,985],{"class":441,"line":503},[501,981,790],{"class":506},[501,983,984],{"class":554}," test_rate_limit_returns_429",[501,986,987],{"class":510},"(client):\n",[501,989,990,993,996,999,1002,1004,1006],{"class":441,"line":520},[501,991,992],{"class":506},"    for",[501,994,995],{"class":510}," _ ",[501,997,998],{"class":506},"in",[501,1000,1001],{"class":561}," range",[501,1003,877],{"class":510},[501,1005,898],{"class":561},[501,1007,805],{"class":510},[501,1009,1010,1013,1016,1018,1021,1023],{"class":441,"line":533},[501,1011,1012],{"class":506},"        assert",[501,1014,1015],{"class":510}," client.post(",[501,1017,880],{"class":591},[501,1019,1020],{"class":510},").status_code ",[501,1022,654],{"class":506},[501,1024,1025],{"class":561}," 200\n",[501,1027,1028,1031,1033,1035,1037],{"class":441,"line":540},[501,1029,1030],{"class":510},"    blocked ",[501,1032,585],{"class":506},[501,1034,1015],{"class":510},[501,1036,880],{"class":591},[501,1038,1039],{"class":510},")\n",[501,1041,1042,1045,1048,1050],{"class":441,"line":545},[501,1043,1044],{"class":506},"    assert",[501,1046,1047],{"class":510}," blocked.status_code ",[501,1049,654],{"class":506},[501,1051,1052],{"class":561}," 429\n",[501,1054,1055,1057,1060,1063,1066],{"class":441,"line":579},[501,1056,1044],{"class":506},[501,1058,1059],{"class":591}," \"Retry-After\"",[501,1061,1062],{"class":506}," in",[501,1064,1065],{"class":510}," blocked.headers      ",[501,1067,1068],{"class":627},"# Lets clients back off correctly.\n",[483,1070,1072],{"id":1071},"failure-modes-and-debugging","Failure Modes and Debugging",[1074,1075,1076,1084,1090,1096],"ul",{},[1077,1078,1079,1083],"li",{},[1080,1081,1082],"strong",{},"Per-process counters."," In-memory limits under-count across workers; use a shared store.",[1077,1085,1086,1089],{},[1080,1087,1088],{},"Race conditions."," Non-atomic increment-and-check lets bursts slip through; make the check atomic.",[1077,1091,1092,1095],{},[1080,1093,1094],{},"Missing Retry-After."," A bare 429 leaves clients guessing; always include the header.",[1077,1097,1098,1101],{},[1080,1099,1100],{},"Limiting by spoofable identity."," Trusting an unauthenticated header lets clients evade limits; key on the authenticated principal where possible.",[483,1103,1105],{"id":1104},"related-reading","Related Reading",[1074,1107,1108,1116,1123],{},[1077,1109,1110,1113,1114,346],{},[1080,1111,1112],{},"Up to the section:"," ",[327,1115,330],{"href":329},[1077,1117,1118,1113,1121,346],{},[1080,1119,1120],{},"Hands-on guide:",[327,1122,201],{"href":958},[1077,1124,1125,1113,1128,1131,1132,346],{},[1080,1126,1127],{},"Composes with:",[327,1129,1130],{"href":339},"Middleware Implementation"," and ",[327,1133,1134],{"href":334},"Async Database Sessions",[1136,1137,1138],"style",{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":496,"searchDepth":520,"depth":520,"links":1140},[1141,1142,1143,1144,1145,1146],{"id":485,"depth":520,"text":486},{"id":741,"depth":520,"text":742},{"id":961,"depth":520,"text":962},{"id":968,"depth":520,"text":969},{"id":1071,"depth":520,"text":1072},{"id":1104,"depth":520,"text":1105},"Protect FastAPI APIs with rate limiting: token bucket and sliding window algorithms, distributed limits in Redis, per-user and per-route policies, 429 responses, and Retry-After.","md",{"slug":1150,"type":1151,"breadcrumb":1152,"datePublished":1161,"dateModified":1162,"faq":1163},"rate-limiting-throttling","cluster",[1153,1156,1158],{"label":1154,"path":1155},"Home","\u002F",{"label":1157,"path":329},"Async, Background Tasks & Observability",{"label":1159,"path":1160},"Rate Limiting & Throttling","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F","2026-02-16","2026-06-18",[1164,1167,1170,1173],{"q":1165,"a":1166},"Why does rate limiting need a shared store like Redis?","Because an API runs many worker processes and often many machines, an in-memory counter on one worker only sees that worker's traffic. A client load-balanced across workers could exceed the limit while each worker thinks it is under. A shared store such as Redis gives every worker one authoritative counter, so the limit is enforced globally.",{"q":1168,"a":1169},"What is the difference between token bucket and sliding window?","Token bucket allows bursts up to a bucket size and refills at a steady rate, which suits clients that occasionally spike. Sliding window counts requests over a moving time window for a smoother, more precise limit. Token bucket is more forgiving of bursts; sliding window is stricter and simpler to reason about for fixed quotas.",{"q":1171,"a":1172},"What should a rate-limited response look like?","Return HTTP 429 Too Many Requests with a Retry-After header telling the client how long to wait, and ideally rate-limit headers showing the limit, remaining quota, and reset time. A well-formed 429 lets well-behaved clients back off correctly instead of hammering the endpoint.",{"q":1174,"a":1175},"Should rate limiting be middleware or a dependency?","Use middleware for a global default that applies to every request, and a dependency for per-route or per-tier policies that need typed access to the authenticated user. Many APIs combine both: a broad limit in middleware and stricter limits as dependencies on expensive endpoints.",{"title":195,"description":1147},"vbA-6NshMoKvMxnJ_smtooxqopLw5W2Y8hpP_QgTxT0",[1179,1179],null,1781809863387]