[{"data":1,"prerenderedAt":1109},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002F":310,"surround-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002F":1107},[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":207,"body":312,"description":1058,"extension":1059,"meta":1060,"navigation":536,"path":208,"seo":1105,"stem":209,"__hash__":1106},"content\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002Findex.md",{"type":313,"value":314,"toc":1046},"minimark",[315,319,326,350,364,369,372,376,388,392,397,498,502,786,790,874,878,905,909,1016,1020,1042],[316,317,207],"h1",{"id":318},"per-user-token-bucket-throttling-in-fastapi",[320,321,322],"p",{},[323,324,325],"strong",{},"Key takeaways:",[327,328,329,333,336,339,342],"ul",{},[330,331,332],"li",{},"A token bucket allows bursts up to a cap while enforcing a steady average rate.",[330,334,335],{},"Store tokens and a refill timestamp per user in Redis.",[330,337,338],{},"Run refill-and-consume as one atomic Lua script to avoid races.",[330,340,341],{},"Key on the authenticated principal for per-user fairness.",[330,343,344,345,349],{},"Return 429 with ",[346,347,348],"code",{},"Retry-After"," when the bucket is empty.",[320,351,352,353,358,359,363],{},"This guide builds the token-bucket algorithm behind ",[354,355,357],"a",{"href":356},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F","Rate Limiting and Throttling",", going lower-level than the ",[354,360,362],{"href":361},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002F","SlowAPI approach",".",[365,366,368],"h2",{"id":367},"the-problem-this-solves","The Problem This Solves",[320,370,371],{},"A fixed-window limit either rejects legitimate bursts or lets a double-limit slip past window boundaries, and a global limiter punishes users who share an IP. A per-user token bucket absorbs bursts, enforces a fair average rate, and isolates each principal.",[365,373,375],{"id":374},"prerequisites","Prerequisites",[327,377,378,381],{},[330,379,380],{},"An authenticated principal available on the request.",[330,382,383,384,387],{},"An async Redis client supporting ",[346,385,386],{},"EVAL","\u002FLua.",[365,389,391],{"id":390},"step-by-step-implementation","Step-by-Step Implementation",[393,394,396],"h3",{"id":395},"_1-the-atomic-refill-and-consume-script","1. The atomic refill-and-consume script",[398,399,404],"pre",{"className":400,"code":401,"language":402,"meta":403,"style":403},"language-lua shiki shiki-themes github-light","-- token_bucket.lua: KEYS[1]=bucket  ARGV: capacity, refill_per_sec, now, cost\nlocal data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')\nlocal tokens = tonumber(data[1]) or tonumber(ARGV[1])\nlocal ts = tonumber(data[2]) or tonumber(ARGV[3])\nlocal elapsed = math.max(0, tonumber(ARGV[3]) - ts)\n-- Refill proportional to elapsed time, capped at capacity.\ntokens = math.min(tonumber(ARGV[1]), tokens + elapsed * tonumber(ARGV[2]))\nlocal allowed = 0\nif tokens >= tonumber(ARGV[4]) then\n  tokens = tokens - tonumber(ARGV[4])\n  allowed = 1\nend\nredis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', ARGV[3])\nredis.call('EXPIRE', KEYS[1], 3600)\nreturn { allowed, tokens }\n","lua","",[346,405,406,414,420,426,432,438,444,450,456,462,468,474,480,486,492],{"__ignoreMap":403},[407,408,411],"span",{"class":409,"line":410},"line",1,[407,412,413],{},"-- token_bucket.lua: KEYS[1]=bucket  ARGV: capacity, refill_per_sec, now, cost\n",[407,415,417],{"class":409,"line":416},2,[407,418,419],{},"local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')\n",[407,421,423],{"class":409,"line":422},3,[407,424,425],{},"local tokens = tonumber(data[1]) or tonumber(ARGV[1])\n",[407,427,429],{"class":409,"line":428},4,[407,430,431],{},"local ts = tonumber(data[2]) or tonumber(ARGV[3])\n",[407,433,435],{"class":409,"line":434},5,[407,436,437],{},"local elapsed = math.max(0, tonumber(ARGV[3]) - ts)\n",[407,439,441],{"class":409,"line":440},6,[407,442,443],{},"-- Refill proportional to elapsed time, capped at capacity.\n",[407,445,447],{"class":409,"line":446},7,[407,448,449],{},"tokens = math.min(tonumber(ARGV[1]), tokens + elapsed * tonumber(ARGV[2]))\n",[407,451,453],{"class":409,"line":452},8,[407,454,455],{},"local allowed = 0\n",[407,457,459],{"class":409,"line":458},9,[407,460,461],{},"if tokens >= tonumber(ARGV[4]) then\n",[407,463,465],{"class":409,"line":464},10,[407,466,467],{},"  tokens = tokens - tonumber(ARGV[4])\n",[407,469,471],{"class":409,"line":470},11,[407,472,473],{},"  allowed = 1\n",[407,475,477],{"class":409,"line":476},12,[407,478,479],{},"end\n",[407,481,483],{"class":409,"line":482},13,[407,484,485],{},"redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', ARGV[3])\n",[407,487,489],{"class":409,"line":488},14,[407,490,491],{},"redis.call('EXPIRE', KEYS[1], 3600)\n",[407,493,495],{"class":409,"line":494},15,[407,496,497],{},"return { allowed, tokens }\n",[393,499,501],{"id":500},"_2-the-fastapi-dependency","2. The FastAPI dependency",[398,503,507],{"className":504,"code":505,"language":506,"meta":403,"style":403},"language-python shiki shiki-themes github-light","import time\nfrom typing import Annotated\n\nfrom fastapi import Depends, HTTPException, Request\n\n# Loaded once; reused for every check.\n_SCRIPT = open(\"token_bucket.lua\").read()\n\n\ndef throttle(capacity: int, refill_per_sec: float, cost: int = 1):\n    async def _dep(request: Request, user: Annotated[dict, Depends(current_user)]):\n        redis = request.app.state.redis\n        allowed, _ = await redis.eval(\n            _SCRIPT, 1, f\"tb:{user['sub']}\",     # Per-principal bucket key.\n            capacity, refill_per_sec, time.time(), cost,\n        )\n        if not allowed:\n            raise HTTPException(429, \"rate limit exceeded\",\n                                headers={\"Retry-After\": \"1\"})\n    return _dep\n","python",[346,508,509,519,532,538,550,554,560,582,586,590,624,644,655,668,711,716,722,734,754,777],{"__ignoreMap":403},[407,510,511,515],{"class":409,"line":410},[407,512,514],{"class":513},"sD7c4","import",[407,516,518],{"class":517},"sgsFI"," time\n",[407,520,521,524,527,529],{"class":409,"line":416},[407,522,523],{"class":513},"from",[407,525,526],{"class":517}," typing ",[407,528,514],{"class":513},[407,530,531],{"class":517}," Annotated\n",[407,533,534],{"class":409,"line":422},[407,535,537],{"emptyLinePlaceholder":536},true,"\n",[407,539,540,542,545,547],{"class":409,"line":428},[407,541,523],{"class":513},[407,543,544],{"class":517}," fastapi ",[407,546,514],{"class":513},[407,548,549],{"class":517}," Depends, HTTPException, Request\n",[407,551,552],{"class":409,"line":434},[407,553,537],{"emptyLinePlaceholder":536},[407,555,556],{"class":409,"line":440},[407,557,559],{"class":558},"sAwPA","# Loaded once; reused for every check.\n",[407,561,562,566,569,572,575,579],{"class":409,"line":446},[407,563,565],{"class":564},"sYu0t","_SCRIPT",[407,567,568],{"class":513}," =",[407,570,571],{"class":564}," open",[407,573,574],{"class":517},"(",[407,576,578],{"class":577},"sYBdl","\"token_bucket.lua\"",[407,580,581],{"class":517},").read()\n",[407,583,584],{"class":409,"line":452},[407,585,537],{"emptyLinePlaceholder":536},[407,587,588],{"class":409,"line":458},[407,589,537],{"emptyLinePlaceholder":536},[407,591,592,595,599,602,605,608,611,614,616,618,621],{"class":409,"line":464},[407,593,594],{"class":513},"def",[407,596,598],{"class":597},"s7eDp"," throttle",[407,600,601],{"class":517},"(capacity: ",[407,603,604],{"class":564},"int",[407,606,607],{"class":517},", refill_per_sec: ",[407,609,610],{"class":564},"float",[407,612,613],{"class":517},", cost: ",[407,615,604],{"class":564},[407,617,568],{"class":513},[407,619,620],{"class":564}," 1",[407,622,623],{"class":517},"):\n",[407,625,626,629,632,635,638,641],{"class":409,"line":470},[407,627,628],{"class":513},"    async",[407,630,631],{"class":513}," def",[407,633,634],{"class":597}," _dep",[407,636,637],{"class":517},"(request: Request, user: Annotated[",[407,639,640],{"class":564},"dict",[407,642,643],{"class":517},", Depends(current_user)]):\n",[407,645,646,649,652],{"class":409,"line":476},[407,647,648],{"class":517},"        redis ",[407,650,651],{"class":513},"=",[407,653,654],{"class":517}," request.app.state.redis\n",[407,656,657,660,662,665],{"class":409,"line":482},[407,658,659],{"class":517},"        allowed, _ ",[407,661,651],{"class":513},[407,663,664],{"class":513}," await",[407,666,667],{"class":517}," redis.eval(\n",[407,669,670,673,676,679,681,684,687,690,693,696,699,702,705,708],{"class":409,"line":488},[407,671,672],{"class":564},"            _SCRIPT",[407,674,675],{"class":517},", ",[407,677,678],{"class":564},"1",[407,680,675],{"class":517},[407,682,683],{"class":513},"f",[407,685,686],{"class":577},"\"tb:",[407,688,689],{"class":564},"{",[407,691,692],{"class":517},"user[",[407,694,695],{"class":577},"'sub'",[407,697,698],{"class":517},"]",[407,700,701],{"class":564},"}",[407,703,704],{"class":577},"\"",[407,706,707],{"class":517},",     ",[407,709,710],{"class":558},"# Per-principal bucket key.\n",[407,712,713],{"class":409,"line":494},[407,714,715],{"class":517},"            capacity, refill_per_sec, time.time(), cost,\n",[407,717,719],{"class":409,"line":718},16,[407,720,721],{"class":517},"        )\n",[407,723,725,728,731],{"class":409,"line":724},17,[407,726,727],{"class":513},"        if",[407,729,730],{"class":513}," not",[407,732,733],{"class":517}," allowed:\n",[407,735,737,740,743,746,748,751],{"class":409,"line":736},18,[407,738,739],{"class":513},"            raise",[407,741,742],{"class":517}," HTTPException(",[407,744,745],{"class":564},"429",[407,747,675],{"class":517},[407,749,750],{"class":577},"\"rate limit exceeded\"",[407,752,753],{"class":517},",\n",[407,755,757,761,763,765,768,771,774],{"class":409,"line":756},19,[407,758,760],{"class":759},"sqxcx","                                headers",[407,762,651],{"class":513},[407,764,689],{"class":517},[407,766,767],{"class":577},"\"Retry-After\"",[407,769,770],{"class":517},": ",[407,772,773],{"class":577},"\"1\"",[407,775,776],{"class":517},"})\n",[407,778,780,783],{"class":409,"line":779},20,[407,781,782],{"class":513},"    return",[407,784,785],{"class":517}," _dep\n",[393,787,789],{"id":788},"_3-apply-to-a-route","3. Apply to a route",[398,791,793],{"className":504,"code":792,"language":506,"meta":403,"style":403},"# 20-token bucket refilling at 5 tokens\u002Fsec → bursts of 20, ~5 rps sustained.\n@router.get(\"\u002Fsearch\", dependencies=[Depends(throttle(capacity=20, refill_per_sec=5))])\nasync def search(q: str) -> dict:\n    return await run_search(q)\n",[346,794,795,800,841,865],{"__ignoreMap":403},[407,796,797],{"class":409,"line":410},[407,798,799],{"class":558},"# 20-token bucket refilling at 5 tokens\u002Fsec → bursts of 20, ~5 rps sustained.\n",[407,801,802,805,807,810,812,815,817,820,823,825,828,830,833,835,838],{"class":409,"line":416},[407,803,804],{"class":597},"@router.get",[407,806,574],{"class":517},[407,808,809],{"class":577},"\"\u002Fsearch\"",[407,811,675],{"class":517},[407,813,814],{"class":759},"dependencies",[407,816,651],{"class":513},[407,818,819],{"class":517},"[Depends(throttle(",[407,821,822],{"class":759},"capacity",[407,824,651],{"class":513},[407,826,827],{"class":564},"20",[407,829,675],{"class":517},[407,831,832],{"class":759},"refill_per_sec",[407,834,651],{"class":513},[407,836,837],{"class":564},"5",[407,839,840],{"class":517},"))])\n",[407,842,843,846,848,851,854,857,860,862],{"class":409,"line":422},[407,844,845],{"class":513},"async",[407,847,631],{"class":513},[407,849,850],{"class":597}," search",[407,852,853],{"class":517},"(q: ",[407,855,856],{"class":564},"str",[407,858,859],{"class":517},") -> ",[407,861,640],{"class":564},[407,863,864],{"class":517},":\n",[407,866,867,869,871],{"class":409,"line":428},[407,868,782],{"class":513},[407,870,664],{"class":513},[407,872,873],{"class":517}," run_search(q)\n",[365,875,877],{"id":876},"edge-cases-and-gotchas","Edge Cases and Gotchas",[327,879,880,890,899],{},[330,881,882,885,886,889],{},[323,883,884],{},"Clock source."," Pass a server timestamp into the script; relying on multiple app clocks causes drift. Using Redis ",[346,887,888],{},"TIME"," inside the script is even safer.",[330,891,892,895,896,363],{},[323,893,894],{},"Cost-weighted requests."," Charge expensive endpoints more tokens by raising ",[346,897,898],{},"cost",[330,900,901,904],{},[323,902,903],{},"Cold buckets."," A first request initializes a full bucket; ensure capacity reflects an acceptable initial burst.",[365,906,908],{"id":907},"verification","Verification",[398,910,912],{"className":504,"code":911,"language":506,"meta":403,"style":403},"def test_burst_then_throttle(client, auth):\n    ok = sum(client.get(\"\u002Fsearch?q=x\", headers=auth).status_code == 200 for _ in range(20))\n    assert ok == 20                                   # Burst absorbed.\n    assert client.get(\"\u002Fsearch?q=x\", headers=auth).status_code == 429  # Then limited.\n",[346,913,914,924,975,991],{"__ignoreMap":403},[407,915,916,918,921],{"class":409,"line":410},[407,917,594],{"class":513},[407,919,920],{"class":597}," test_burst_then_throttle",[407,922,923],{"class":517},"(client, auth):\n",[407,925,926,929,931,934,937,940,942,945,947,950,953,956,959,962,965,968,970,972],{"class":409,"line":416},[407,927,928],{"class":517},"    ok ",[407,930,651],{"class":513},[407,932,933],{"class":564}," sum",[407,935,936],{"class":517},"(client.get(",[407,938,939],{"class":577},"\"\u002Fsearch?q=x\"",[407,941,675],{"class":517},[407,943,944],{"class":759},"headers",[407,946,651],{"class":513},[407,948,949],{"class":517},"auth).status_code ",[407,951,952],{"class":513},"==",[407,954,955],{"class":564}," 200",[407,957,958],{"class":513}," for",[407,960,961],{"class":517}," _ ",[407,963,964],{"class":513},"in",[407,966,967],{"class":564}," range",[407,969,574],{"class":517},[407,971,827],{"class":564},[407,973,974],{"class":517},"))\n",[407,976,977,980,983,985,988],{"class":409,"line":422},[407,978,979],{"class":513},"    assert",[407,981,982],{"class":517}," ok ",[407,984,952],{"class":513},[407,986,987],{"class":564}," 20",[407,989,990],{"class":558},"                                   # Burst absorbed.\n",[407,992,993,995,998,1000,1002,1004,1006,1008,1010,1013],{"class":409,"line":428},[407,994,979],{"class":513},[407,996,997],{"class":517}," client.get(",[407,999,939],{"class":577},[407,1001,675],{"class":517},[407,1003,944],{"class":759},[407,1005,651],{"class":513},[407,1007,949],{"class":517},[407,1009,952],{"class":513},[407,1011,1012],{"class":564}," 429",[407,1014,1015],{"class":558},"  # Then limited.\n",[365,1017,1019],{"id":1018},"related-reading","Related Reading",[327,1021,1022,1030],{},[330,1023,1024,1027,1028,363],{},[323,1025,1026],{},"Up to the topic:"," ",[354,1029,357],{"href":356},[330,1031,1032,1027,1035,1037,1038,363],{},[323,1033,1034],{},"Related guides:",[354,1036,201],{"href":361}," and ",[354,1039,1041],{"href":1040},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002F","Async Correctness and Concurrency",[1043,1044,1045],"style",{},"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);}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":403,"searchDepth":416,"depth":416,"links":1047},[1048,1049,1050,1055,1056,1057],{"id":367,"depth":416,"text":368},{"id":374,"depth":416,"text":375},{"id":390,"depth":416,"text":391,"children":1051},[1052,1053,1054],{"id":395,"depth":422,"text":396},{"id":500,"depth":422,"text":501},{"id":788,"depth":422,"text":789},{"id":876,"depth":416,"text":877},{"id":907,"depth":416,"text":908},{"id":1018,"depth":416,"text":1019},"Implement per-user token bucket throttling in FastAPI with an atomic Redis Lua script: burst allowance, steady refill, fair per-principal limits, and correct 429 responses.","md",{"slug":1061,"type":1062,"breadcrumb":1063,"datePublished":1075,"dateModified":1076,"howto":1077,"faq":1095},"per-user-token-bucket-throttling","long_tail",[1064,1067,1070,1072],{"label":1065,"path":1066},"Home","\u002F",{"label":1068,"path":1069},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":1071,"path":356},"Rate Limiting & Throttling",{"label":1073,"path":1074},"Per-User Token Bucket Throttling","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002F","2026-02-28","2026-06-18",{"name":1078,"steps":1079},"Implement per-user token bucket throttling in FastAPI",[1080,1083,1086,1089,1092],{"name":1081,"text":1082},"Model the bucket","Store tokens and a last-refill timestamp per user in Redis.",{"name":1084,"text":1085},"Refill on access","Add tokens proportional to elapsed time, capped at the bucket size.",{"name":1087,"text":1088},"Consume atomically","Run the refill-and-consume logic in a single Redis Lua script to avoid races.",{"name":1090,"text":1091},"Key by principal","Use the authenticated user id as the bucket key for fairness.",{"name":1093,"text":1094},"Reject when empty","Return 429 with Retry-After when no token is available.",[1096,1099,1102],{"q":1097,"a":1098},"Why use a token bucket instead of a fixed window?","A token bucket allows short bursts up to the bucket size while still enforcing a steady average rate through refill, which matches how real clients behave — mostly quiet with occasional spikes. A fixed window rejects a burst that a bucket would absorb, and it suffers from boundary effects where twice the limit can pass around the window edge.",{"q":1100,"a":1101},"Why must the bucket update be atomic?","Refilling and consuming a token is a read-modify-write. If two requests interleave between the read and the write, both can see a token and both consume it, letting more through than allowed. Running the logic as a single Redis Lua script makes the whole operation atomic, closing that race.",{"q":1103,"a":1104},"How do I make the throttle fair across users?","Key the bucket on the authenticated principal, not the IP, so each user gets their own allowance regardless of shared networks. One user exhausting their bucket then cannot affect another, which is the fairness property a shared limiter cannot provide.",{"title":207,"description":1058},"lwPpb2gwr6Mj90ukjNIj7S2_zkrJK77E8fHR14BEAiQ",[1108,1108],null,1781809863674]