[{"data":1,"prerenderedAt":2198},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002F":580,"surround-\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002F":2197},[4,186,386],{"title":5,"path":6,"stem":7,"children":8},"Advanced Pydantic Validation Serialization","\u002Fadvanced-pydantic-validation-serialization","advanced-pydantic-validation-serialization",[9,12,42,66,90,114,150,174],{"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,30,36],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Before, After and Wrap Validators in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fbefore-after-and-wrap-validators\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"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",[29],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Cross-Field Validation Patterns in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns","advanced-pydantic-validation-serialization\u002Fcustom-validators-field-constraints\u002Fcross-field-validation-patterns\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"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",[41],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"JSON Schema Customization in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Findex",[47,48,54,60],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"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",[53],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Discriminated Unions in OpenAPI with Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fdiscriminated-unions-in-openapi\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Examples in the OpenAPI Schema with FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema","advanced-pydantic-validation-serialization\u002Fjson-schema-customization\u002Fexamples-in-openapi-schema\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Nested Model Serialization in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Findex",[71,72,78,84],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Excluding Fields Per Endpoint in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fexcluding-fields-per-endpoint\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"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",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"Self-Referencing and Recursive Models in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models","advanced-pydantic-validation-serialization\u002Fnested-model-serialization\u002Fself-referencing-and-recursive-models\u002Findex",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Performance Optimization for Pydantic Models in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Findex",[95,96,102,108],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"model_construct and When to Skip Validation in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Fmodel-construct-when-to-skip-validation\u002Findex",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"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",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"TypeAdapter for Non-Model Types in Pydantic","\u002Fadvanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types","advanced-pydantic-validation-serialization\u002Fperformance-optimization-for-models\u002Ftypeadapter-for-non-model-types\u002Findex",[113],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Pydantic V2 Migration Guide for FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Findex",[119,120,126,132,138,144],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"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",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"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",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"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",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Replacing json_encoders with field_serializer in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Freplacing-json-encoders-with-field-serializer\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Migrating @root_validator to @model_validator in Pydantic v2","\u002Fadvanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator","advanced-pydantic-validation-serialization\u002Fpydantic-v2-migration-guide\u002Froot-validator-to-model-validator\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Request Validation Patterns in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Findex",[155,156,162,168],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Optional vs Nullable Fields in Pydantic and FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Foptional-vs-nullable-fields\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Query, Path and Body Parameter Validation in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fquery-path-and-body-parameter-validation\u002Findex",[167],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Validating File Uploads and Forms in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms","advanced-pydantic-validation-serialization\u002Frequest-validation-patterns\u002Fvalidating-file-uploads-and-forms\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Type Hinting and IDE Integration in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Findex",[179,180],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"Annotated Dependencies and Reusable Types in FastAPI","\u002Fadvanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types","advanced-pydantic-validation-serialization\u002Ftype-hinting-ide-integration\u002Fannotated-dependencies-and-reusable-types\u002Findex",[185],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Async Background Tasks Observability","\u002Fasync-background-tasks-observability","async-background-tasks-observability",[191,194,224,254,284,308,338,362],{"title":192,"path":188,"stem":193},"Async, Background Tasks, and Observability in FastAPI","async-background-tasks-observability\u002Findex",{"title":195,"path":196,"stem":197,"children":198},"Async Correctness and Concurrency in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Findex",[199,200,206,212,218],{"title":195,"path":196,"stem":197},{"title":201,"path":202,"stem":203,"children":204},"Concurrent Requests with asyncio.gather in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002Findex",[205],{"title":201,"path":202,"stem":203},{"title":207,"path":208,"stem":209,"children":210},"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",[211],{"title":207,"path":208,"stem":209},{"title":213,"path":214,"stem":215,"children":216},"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",[217],{"title":213,"path":214,"stem":215},{"title":219,"path":220,"stem":221,"children":222},"Running Sync Code in a Threadpool in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool","async-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002Findex",[223],{"title":219,"path":220,"stem":221},{"title":225,"path":226,"stem":227,"children":228},"Async Database Sessions in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions","async-background-tasks-observability\u002Fasync-database-sessions\u002Findex",[229,230,236,242,248],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"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",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"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",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Testing with Async Database Fixtures in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures\u002Findex",[247],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Transaction Management and Rollback in FastAPI","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback","async-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Background Task Processing in FastAPI","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing","async-background-tasks-observability\u002Fbackground-task-processing\u002Findex",[259,260,266,272,278],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"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",[265],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Retry and Idempotency for FastAPI Background Tasks","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks","async-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"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",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"When FastAPI BackgroundTasks Silently Fails","\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails","async-background-tasks-observability\u002Fbackground-task-processing\u002Fwhen-backgroundtasks-silently-fails\u002Findex",[283],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Caching Strategies in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies","async-background-tasks-observability\u002Fcaching-strategies\u002Findex",[289,290,296,302],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"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",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Caching Dependency Results in FastAPI","\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results","async-background-tasks-observability\u002Fcaching-strategies\u002Fcaching-dependency-results\u002Findex",[301],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"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",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Observability and Tracing in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing","async-background-tasks-observability\u002Fobservability-and-tracing\u002Findex",[313,314,320,326,332],{"title":309,"path":310,"stem":311},{"title":315,"path":316,"stem":317,"children":318},"Correlating Logs, Traces and Errors in FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fcorrelating-logs-traces-and-errors\u002Findex",[319],{"title":315,"path":316,"stem":317},{"title":321,"path":322,"stem":323,"children":324},"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",[325],{"title":321,"path":322,"stem":323},{"title":327,"path":328,"stem":329,"children":330},"Prometheus Metrics for FastAPI","\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi","async-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi\u002Findex",[331],{"title":327,"path":328,"stem":329},{"title":333,"path":334,"stem":335,"children":336},"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",[337],{"title":333,"path":334,"stem":335},{"title":339,"path":340,"stem":341,"children":342},"Rate Limiting and Throttling in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling","async-background-tasks-observability\u002Frate-limiting-throttling\u002Findex",[343,344,350,356],{"title":339,"path":340,"stem":341},{"title":345,"path":346,"stem":347,"children":348},"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",[349],{"title":345,"path":346,"stem":347},{"title":351,"path":352,"stem":353,"children":354},"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",[355],{"title":351,"path":352,"stem":353},{"title":357,"path":358,"stem":359,"children":360},"Rate Limit Headers and 429 Responses in FastAPI","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses","async-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002Findex",[361],{"title":357,"path":358,"stem":359},{"title":363,"path":364,"stem":365,"children":366},"Testing FastAPI Applications","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Findex",[367,368,374,380],{"title":363,"path":364,"stem":365},{"title":369,"path":370,"stem":371,"children":372},"Mocking External Services in FastAPI Tests","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests\u002Findex",[373],{"title":369,"path":370,"stem":371},{"title":375,"path":376,"stem":377,"children":378},"TestClient vs httpx AsyncClient in FastAPI","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002Findex",[379],{"title":375,"path":376,"stem":377},{"title":381,"path":382,"stem":383,"children":384},"Testing Async FastAPI Endpoints with pytest-asyncio","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio","async-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio\u002Findex",[385],{"title":381,"path":382,"stem":383},{"title":387,"path":388,"stem":389,"children":390},"Core Architecture Routing Patterns","\u002Fcore-architecture-routing-patterns","core-architecture-routing-patterns",[391,394,412,436,472,496,526,556],{"title":392,"path":388,"stem":393},"FastAPI Core Architecture and Routing Patterns","core-architecture-routing-patterns\u002Findex",{"title":395,"path":396,"stem":397,"children":398},"Application Factory Patterns in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Findex",[399,400,406],{"title":395,"path":396,"stem":397},{"title":401,"path":402,"stem":403,"children":404},"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",[405],{"title":401,"path":402,"stem":403},{"title":407,"path":408,"stem":409,"children":410},"Lifespan Events vs Startup and Shutdown in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown","core-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown\u002Findex",[411],{"title":407,"path":408,"stem":409},{"title":413,"path":414,"stem":415,"children":416},"Configuration Management in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management","core-architecture-routing-patterns\u002Fconfiguration-management\u002Findex",[417,418,424,430],{"title":413,"path":414,"stem":415},{"title":419,"path":420,"stem":421,"children":422},"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",[423],{"title":419,"path":420,"stem":421},{"title":425,"path":426,"stem":427,"children":428},"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",[429],{"title":425,"path":426,"stem":427},{"title":431,"path":432,"stem":433,"children":434},"Secrets and .env Files Per Environment in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment","core-architecture-routing-patterns\u002Fconfiguration-management\u002Fsecrets-and-env-files-per-environment\u002Findex",[435],{"title":431,"path":432,"stem":433},{"title":437,"path":438,"stem":439,"children":440},"Dependency Injection Strategies in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Findex",[441,442,448,454,460,466],{"title":437,"path":438,"stem":439},{"title":443,"path":444,"stem":445,"children":446},"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",[447],{"title":443,"path":444,"stem":445},{"title":449,"path":450,"stem":451,"children":452},"Dependency Caching and use_cache in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fdependency-caching-and-use-cache\u002Findex",[453],{"title":449,"path":450,"stem":451},{"title":455,"path":456,"stem":457,"children":458},"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",[459],{"title":455,"path":456,"stem":457},{"title":461,"path":462,"stem":463,"children":464},"Overriding Dependencies in FastAPI Tests","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Foverriding-dependencies-in-tests\u002Findex",[465],{"title":461,"path":462,"stem":463},{"title":467,"path":468,"stem":469,"children":470},"Yield Dependencies and Cleanup Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order","core-architecture-routing-patterns\u002Fdependency-injection-strategies\u002Fyield-dependencies-and-cleanup-order\u002Findex",[471],{"title":467,"path":468,"stem":469},{"title":473,"path":474,"stem":475,"children":476},"Error Handling and Global Exceptions in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Findex",[477,478,484,490],{"title":473,"path":474,"stem":475},{"title":479,"path":480,"stem":481,"children":482},"Customising Validation Error Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fcustomising-validation-error-responses\u002Findex",[483],{"title":479,"path":480,"stem":481},{"title":485,"path":486,"stem":487,"children":488},"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",[489],{"title":485,"path":486,"stem":487},{"title":491,"path":492,"stem":493,"children":494},"HTTPException vs Custom Exception Classes in FastAPI","\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes","core-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fhttpexception-vs-custom-exception-classes\u002Findex",[495],{"title":491,"path":492,"stem":493},{"title":497,"path":498,"stem":499,"children":500},"Middleware Implementation in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Findex",[501,502,508,514,520],{"title":497,"path":498,"stem":499},{"title":503,"path":504,"stem":505,"children":506},"CORS Middleware Configuration in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fcors-middleware-configuration\u002Findex",[507],{"title":503,"path":504,"stem":505},{"title":509,"path":510,"stem":511,"children":512},"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",[513],{"title":509,"path":510,"stem":511},{"title":515,"path":516,"stem":517,"children":518},"Middleware Execution Order in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-execution-order\u002Findex",[519],{"title":515,"path":516,"stem":517},{"title":521,"path":522,"stem":523,"children":524},"Middleware vs Dependencies: When to Use Which","\u002Fcore-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which","core-architecture-routing-patterns\u002Fmiddleware-implementation\u002Fmiddleware-vs-dependencies-when-to-use-which\u002Findex",[525],{"title":521,"path":522,"stem":523},{"title":527,"path":528,"stem":529,"children":530},"Modular Router Organization in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Findex",[531,532,538,544,550],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"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",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541,"children":542},"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",[543],{"title":539,"path":540,"stem":541},{"title":545,"path":546,"stem":547,"children":548},"Router Tags and OpenAPI Grouping in FastAPI","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Frouter-tags-and-openapi-grouping\u002Findex",[549],{"title":545,"path":546,"stem":547},{"title":551,"path":552,"stem":553,"children":554},"Versioning APIs with FastAPI Routers","\u002Fcore-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers","core-architecture-routing-patterns\u002Fmodular-router-organization\u002Fversioning-apis-with-routers\u002Findex",[555],{"title":551,"path":552,"stem":553},{"title":557,"path":558,"stem":559,"children":560},"The FastAPI Request\u002FResponse Lifecycle","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Findex",[561,562,568,574],{"title":557,"path":558,"stem":559},{"title":563,"path":564,"stem":565,"children":566},"How a Request Flows Through FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fhow-a-request-flows-through-fastapi\u002Findex",[567],{"title":563,"path":564,"stem":565},{"title":569,"path":570,"stem":571,"children":572},"Response Model and Serialization Order","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fresponse-model-and-serialization-order\u002Findex",[573],{"title":569,"path":570,"stem":571},{"title":575,"path":576,"stem":577,"children":578},"Streaming and File Responses in FastAPI","\u002Fcore-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses","core-architecture-routing-patterns\u002Frequest-response-lifecycle\u002Fstreaming-and-file-responses\u002Findex",[579],{"title":575,"path":576,"stem":577},{"id":581,"title":357,"body":582,"dateModified":2165,"datePublished":2165,"description":2166,"extension":2167,"faq":2168,"howto":2179,"meta":2180,"navigation":891,"path":358,"seo":2194,"stem":359,"type":2195,"__hash__":2196},"content\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002Findex.md",{"type":583,"value":584,"toc":2154},"minimark",[585,589,596,635,644,753,758,773,792,820,824,831,836,845,852,858,862,865,1730,1733,1740,1743,1749,1762,1766,1794,1807,1819,1823,1826,1840,1852,1856,1859,2036,2044,2048,2061,2064,2070,2074,2080,2086,2092,2098,2104,2108,2150],[586,587,357],"h1",{"id":588},"rate-limit-headers-and-429-responses-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,608,622,625,632],"ul",{},[600,601,602,603,607],"li",{},"Send ",[604,605,606],"code",{},"Retry-After"," on every 429. Without it, clients guess, and their guesses synchronise.",[600,609,602,610,613,614,617,618,621],{},[604,611,612],{},"X-RateLimit-Limit",", ",[604,615,616],{},"-Remaining"," and ",[604,619,620],{},"-Reset"," on successful responses too, so callers can pace themselves.",[600,623,624],{},"429 is \"you exceeded your quota\"; 503 is \"the server is overloaded\". Do not use one to mean the other.",[600,626,627,628,631],{},"The body should name the limit, the window and the wait — a bare ",[604,629,630],{},"{\"detail\": \"Too Many Requests\"}"," costs your users a support ticket.",[600,633,634],{},"Emit the headers from the same middleware that makes the decision, so allowed and rejected paths cannot drift.",[590,636,637,638,643],{},"Your API started rejecting a client's traffic and their engineer wants to know what the limit is, how much of it they have left, and when they can try again. If the only thing your 429 carries is a status code, they will find out by retrying in a tight loop. This page is part of ",[639,640,642],"a",{"href":641},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F","Rate Limiting and Throttling"," and covers the response side of a limiter: what to send, and why each field earns its place.",[645,646,647,749],"figure",{},[648,649,657,658,657,662,657,666,657,675,657,682,657,687,657,695,657,700,657,704,657,707,657,714,657,720,657,725,657,729,657,733,657,737,657,741,657,745],"svg",{"viewBox":650,"role":651,"ariaLabelledBy":652,"xmlns":655,"style":656},"0 0 720 280","img",[653,654],"rl-title","rl-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[659,660,661],"title",{"id":653},"Headers on allowed and rejected responses",[663,664,665],"desc",{"id":654},"A limiter decision splitting into an allowed 200 response carrying limit and remaining headers, and a rejected 429 carrying the same headers plus Retry-After and a problem details body.",[667,668],"rect",{"x":669,"y":670,"width":671,"height":672,"rx":673,"style":674},"20","106","170","60","9","fill:#00796B;stroke:#00796B;stroke-width:1.6px",[676,677,681],"text",{"x":678,"y":679,"style":680},"105","132","text-anchor:middle;fill:#ffffff;font:600 14px sans-serif","limiter",[676,683,686],{"x":678,"y":684,"style":685},"152","text-anchor:middle;fill:#ffffff;font:400 12px sans-serif","take a token",[688,689],"line",{"x1":690,"y1":691,"x2":692,"y2":693,"style":694},"190","126","256","80","stroke:currentColor;stroke-width:1.4px",[696,697],"polygon",{"points":698,"style":699},"252,76 262,76 256,86","fill:currentColor",[688,701],{"x1":690,"y1":702,"x2":692,"y2":703,"style":694},"146","196",[696,705],{"points":706,"style":699},"252,190 262,200 250,198",[667,708],{"x":709,"y":710,"width":711,"height":712,"rx":673,"style":713},"270","24","420","86","fill:none;stroke:currentColor;stroke-width:1.4px",[676,715,719],{"x":716,"y":717,"style":718},"292","50","text-anchor:start;fill:currentColor;font:600 13px sans-serif","200 OK",[676,721,724],{"x":716,"y":722,"style":723},"72","text-anchor:start;fill:currentColor;font:400 12px monospace","X-RateLimit-Limit: 3",[676,726,728],{"x":716,"y":727,"style":723},"92","X-RateLimit-Remaining: 2",[667,730],{"x":709,"y":731,"width":711,"height":670,"rx":673,"style":732},"156","fill:none;stroke:#00796B;stroke-width:1.8px",[676,734,736],{"x":716,"y":735,"style":718},"182","429 Too Many Requests",[676,738,740],{"x":716,"y":739,"style":723},"204","X-RateLimit-Remaining: 0",[676,742,744],{"x":716,"y":743,"style":723},"224","X-RateLimit-Reset: 1060",[676,746,748],{"x":716,"y":747,"style":723},"244","Retry-After: 60",[750,751,752],"figcaption",{},"Both branches carry the quota headers; only the rejected one adds Retry-After and an explanatory body.",[754,755,757],"h2",{"id":756},"what-each-field-means","What Each Field Means",[590,759,760,764,765,767,768,772],{},[593,761,762],{},[604,763,606],{}," is the one header a client can act on without any prior knowledge of your API. It is defined by RFC 9110 for 429, 503 and 3xx responses, and takes either a delay in seconds (",[604,766,748],{},") or an HTTP-date. Send seconds — dates require the client to trust its own clock relative to yours. Round ",[769,770,771],"em",{},"up",": telling a caller to come back in 59.4 seconds when the window resets at 60 guarantees a wasted request.",[590,774,775,779,780,785,786,791],{},[593,776,777],{},[604,778,612],{}," is the ceiling for the window. ",[593,781,782],{},[604,783,784],{},"X-RateLimit-Remaining"," is how many requests are left in it. ",[593,787,788],{},[604,789,790],{},"X-RateLimit-Reset"," is when the window resets — and this is the field everyone disagrees about. GitHub sends a Unix timestamp; other APIs send seconds-until-reset. Both are common, neither is standard, and a client integrating with several APIs will get it wrong at least once. Document which you send in the same place you document the limit.",[590,793,794,795,798,799,613,802,617,805,808,809,812,813,816,817,819],{},"None of the ",[604,796,797],{},"X-RateLimit-*"," trio is standardised. The IETF's draft on rate limit header fields proposes ",[604,800,801],{},"RateLimit-Limit",[604,803,804],{},"RateLimit-Remaining",[604,806,807],{},"RateLimit-Reset"," without the ",[604,810,811],{},"X-"," prefix, alongside a structured ",[604,814,815],{},"RateLimit"," field. Until that stabilises, the ",[604,818,811],{}," names remain the pragmatic choice because that is what client libraries look for. Whichever you pick, be consistent across every endpoint.",[754,821,823],{"id":822},"_429-versus-503","429 Versus 503",[590,825,826,827,830],{},"The distinction is about ",[769,828,829],{},"whose"," fault it is, and it changes what the client should do.",[590,832,833,835],{},[593,834,736],{}," means this particular caller exceeded a quota that applies to them. Other callers are unaffected. The remedy is on the client side: slow down, and the response tells them by how much. A 429 is not an incident on your side — it is the system working.",[590,837,838,841,842,844],{},[593,839,840],{},"503 Service Unavailable"," means the server cannot serve requests right now, for reasons that have nothing to do with which client is asking. A dependency is down, a queue is saturated, a deploy is in progress. Everyone gets it. ",[604,843,606],{}," is meaningful here too, and it is a hint about recovery rather than about quota.",[590,846,847,848,851],{},"Getting this backwards has real consequences. Sending 429 for a server-side overload tells every client that ",[769,849,850],{},"they"," are misbehaving, so well-implemented clients back off individually while your dashboards show a client-error spike rather than a server problem — the error budget is charged to the wrong side, and the alert that should have paged you does not. In the other direction, sending 503 for quota exhaustion makes a single abusive client look like an outage.",[590,853,854,855,857],{},"There is a third case worth naming: shedding load at the edge when a downstream is failing. That is neither the client's quota nor a total outage; 503 with a short ",[604,856,606],{}," is the honest answer.",[754,859,861],{"id":860},"a-limiter-that-emits-real-headers","A Limiter That Emits Real Headers",[590,863,864],{},"The example below is a fixed-window counter with a frozen clock, so the numbers in the transcript are reproducible. The decision and the headers live in one middleware, which is the structural point — an allowed request and a rejected one cannot disagree about the quota if the same code computes both.",[866,867,872],"pre",{"className":868,"code":869,"language":870,"meta":871,"style":871},"language-python shiki shiki-themes github-light-high-contrast","import math\n\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\n\napp = FastAPI()\n\nLIMIT = 3            # Requests allowed per window.\nWINDOW = 60.0        # Window length in seconds.\nNOW = {\"t\": 1000.0}  # A frozen clock: real code uses time.monotonic().\n\nBUCKETS: dict[str, dict[str, float]] = {}\n\n\ndef take_token(key: str) -> tuple[bool, int, float]:\n    \"\"\"Fixed-window counter. Returns (allowed, remaining, reset_epoch).\"\"\"\n    now = NOW[\"t\"]\n    bucket = BUCKETS.get(key)\n    if bucket is None or now >= bucket[\"reset\"]:\n        bucket = {\"used\": 0.0, \"reset\": now + WINDOW}\n        BUCKETS[key] = bucket\n    if bucket[\"used\"] >= LIMIT:\n        return False, 0, bucket[\"reset\"]\n    bucket[\"used\"] += 1\n    return True, int(LIMIT - bucket[\"used\"]), bucket[\"reset\"]\n\n\n@app.middleware(\"http\")\nasync def rate_limit(request: Request, call_next):\n    if request.url.path in {\"\u002Fsent-headers\", \"\u002Fdocs\", \"\u002Fopenapi.json\"}:\n        return await call_next(request)   # Never rate-limit the docs or the inspection route.\n\n    key = request.client.host if request.client else \"anon\"\n    allowed, remaining, reset = take_token(key)\n    common = {\n        \"X-RateLimit-Limit\": str(LIMIT),\n        \"X-RateLimit-Remaining\": str(remaining),\n        \"X-RateLimit-Reset\": str(int(reset)),\n    }\n    if not allowed:\n        retry_after = max(1, math.ceil(reset - NOW[\"t\"]))\n        return JSONResponse(\n            status_code=429,\n            headers={**common, \"Retry-After\": str(retry_after)},\n            content={\n                \"type\": \"https:\u002F\u002Fexample.com\u002Ferrors\u002Frate-limited\",\n                \"title\": \"Too Many Requests\",\n                \"status\": 429,\n                \"detail\": f\"Limit of {LIMIT} requests per {int(WINDOW)}s exceeded for this key.\",\n                \"retry_after_seconds\": retry_after,\n                \"limit\": LIMIT,\n                \"window_seconds\": int(WINDOW),\n            },\n        )\n    response = await call_next(request)\n    response.headers.update(common)\n    return response\n","python","",[604,873,874,886,893,907,920,925,937,942,959,973,1000,1005,1035,1040,1045,1078,1084,1103,1117,1149,1183,1197,1217,1238,1254,1286,1291,1296,1310,1325,1354,1368,1373,1396,1407,1418,1435,1448,1465,1471,1482,1513,1521,1536,1563,1574,1587,1600,1612,1654,1663,1675,1691,1697,1703,1716,1722],{"__ignoreMap":871},[875,876,878,882],"span",{"class":688,"line":877},1,[875,879,881],{"class":880},"sTJeM","import",[875,883,885],{"class":884},"sigWx"," math\n",[875,887,889],{"class":688,"line":888},2,[875,890,892],{"emptyLinePlaceholder":891},true,"\n",[875,894,896,899,902,904],{"class":688,"line":895},3,[875,897,898],{"class":880},"from",[875,900,901],{"class":884}," fastapi ",[875,903,881],{"class":880},[875,905,906],{"class":884}," FastAPI, Request\n",[875,908,910,912,915,917],{"class":688,"line":909},4,[875,911,898],{"class":880},[875,913,914],{"class":884}," fastapi.responses ",[875,916,881],{"class":880},[875,918,919],{"class":884}," JSONResponse\n",[875,921,923],{"class":688,"line":922},5,[875,924,892],{"emptyLinePlaceholder":891},[875,926,928,931,934],{"class":688,"line":927},6,[875,929,930],{"class":884},"app ",[875,932,933],{"class":880},"=",[875,935,936],{"class":884}," FastAPI()\n",[875,938,940],{"class":688,"line":939},7,[875,941,892],{"emptyLinePlaceholder":891},[875,943,945,949,952,955],{"class":688,"line":944},8,[875,946,948],{"class":947},"sacAq","LIMIT",[875,950,951],{"class":880}," =",[875,953,954],{"class":947}," 3",[875,956,958],{"class":957},"sFeEa","            # Requests allowed per window.\n",[875,960,962,965,967,970],{"class":688,"line":961},9,[875,963,964],{"class":947},"WINDOW",[875,966,951],{"class":880},[875,968,969],{"class":947}," 60.0",[875,971,972],{"class":957},"        # Window length in seconds.\n",[875,974,976,979,981,984,988,991,994,997],{"class":688,"line":975},10,[875,977,978],{"class":947},"NOW",[875,980,951],{"class":880},[875,982,983],{"class":884}," {",[875,985,987],{"class":986},"sYEJz","\"t\"",[875,989,990],{"class":884},": ",[875,992,993],{"class":947},"1000.0",[875,995,996],{"class":884},"}  ",[875,998,999],{"class":957},"# A frozen clock: real code uses time.monotonic().\n",[875,1001,1003],{"class":688,"line":1002},11,[875,1004,892],{"emptyLinePlaceholder":891},[875,1006,1008,1011,1014,1017,1020,1022,1024,1027,1030,1032],{"class":688,"line":1007},12,[875,1009,1010],{"class":947},"BUCKETS",[875,1012,1013],{"class":884},": dict[",[875,1015,1016],{"class":947},"str",[875,1018,1019],{"class":884},", dict[",[875,1021,1016],{"class":947},[875,1023,613],{"class":884},[875,1025,1026],{"class":947},"float",[875,1028,1029],{"class":884},"]] ",[875,1031,933],{"class":880},[875,1033,1034],{"class":884}," {}\n",[875,1036,1038],{"class":688,"line":1037},13,[875,1039,892],{"emptyLinePlaceholder":891},[875,1041,1043],{"class":688,"line":1042},14,[875,1044,892],{"emptyLinePlaceholder":891},[875,1046,1048,1051,1055,1058,1060,1063,1066,1068,1071,1073,1075],{"class":688,"line":1047},15,[875,1049,1050],{"class":880},"def",[875,1052,1054],{"class":1053},"s3dhs"," take_token",[875,1056,1057],{"class":884},"(key: ",[875,1059,1016],{"class":947},[875,1061,1062],{"class":884},") -> tuple[",[875,1064,1065],{"class":947},"bool",[875,1067,613],{"class":884},[875,1069,1070],{"class":947},"int",[875,1072,613],{"class":884},[875,1074,1026],{"class":947},[875,1076,1077],{"class":884},"]:\n",[875,1079,1081],{"class":688,"line":1080},16,[875,1082,1083],{"class":986},"    \"\"\"Fixed-window counter. Returns (allowed, remaining, reset_epoch).\"\"\"\n",[875,1085,1087,1090,1092,1095,1098,1100],{"class":688,"line":1086},17,[875,1088,1089],{"class":884},"    now ",[875,1091,933],{"class":880},[875,1093,1094],{"class":947}," NOW",[875,1096,1097],{"class":884},"[",[875,1099,987],{"class":986},[875,1101,1102],{"class":884},"]\n",[875,1104,1106,1109,1111,1114],{"class":688,"line":1105},18,[875,1107,1108],{"class":884},"    bucket ",[875,1110,933],{"class":880},[875,1112,1113],{"class":947}," BUCKETS",[875,1115,1116],{"class":884},".get(key)\n",[875,1118,1120,1123,1126,1129,1132,1135,1138,1141,1144,1147],{"class":688,"line":1119},19,[875,1121,1122],{"class":880},"    if",[875,1124,1125],{"class":884}," bucket ",[875,1127,1128],{"class":880},"is",[875,1130,1131],{"class":947}," None",[875,1133,1134],{"class":880}," or",[875,1136,1137],{"class":884}," now ",[875,1139,1140],{"class":880},">=",[875,1142,1143],{"class":884}," bucket[",[875,1145,1146],{"class":986},"\"reset\"",[875,1148,1077],{"class":884},[875,1150,1152,1155,1157,1159,1162,1164,1167,1169,1171,1174,1177,1180],{"class":688,"line":1151},20,[875,1153,1154],{"class":884},"        bucket ",[875,1156,933],{"class":880},[875,1158,983],{"class":884},[875,1160,1161],{"class":986},"\"used\"",[875,1163,990],{"class":884},[875,1165,1166],{"class":947},"0.0",[875,1168,613],{"class":884},[875,1170,1146],{"class":986},[875,1172,1173],{"class":884},": now ",[875,1175,1176],{"class":880},"+",[875,1178,1179],{"class":947}," WINDOW",[875,1181,1182],{"class":884},"}\n",[875,1184,1186,1189,1192,1194],{"class":688,"line":1185},21,[875,1187,1188],{"class":947},"        BUCKETS",[875,1190,1191],{"class":884},"[key] ",[875,1193,933],{"class":880},[875,1195,1196],{"class":884}," bucket\n",[875,1198,1200,1202,1204,1206,1209,1211,1214],{"class":688,"line":1199},22,[875,1201,1122],{"class":880},[875,1203,1143],{"class":884},[875,1205,1161],{"class":986},[875,1207,1208],{"class":884},"] ",[875,1210,1140],{"class":880},[875,1212,1213],{"class":947}," LIMIT",[875,1215,1216],{"class":884},":\n",[875,1218,1220,1223,1226,1228,1231,1234,1236],{"class":688,"line":1219},23,[875,1221,1222],{"class":880},"        return",[875,1224,1225],{"class":947}," False",[875,1227,613],{"class":884},[875,1229,1230],{"class":947},"0",[875,1232,1233],{"class":884},", bucket[",[875,1235,1146],{"class":986},[875,1237,1102],{"class":884},[875,1239,1241,1244,1246,1248,1251],{"class":688,"line":1240},24,[875,1242,1243],{"class":884},"    bucket[",[875,1245,1161],{"class":986},[875,1247,1208],{"class":884},[875,1249,1250],{"class":880},"+=",[875,1252,1253],{"class":947}," 1\n",[875,1255,1257,1260,1263,1265,1267,1270,1272,1275,1277,1279,1282,1284],{"class":688,"line":1256},25,[875,1258,1259],{"class":880},"    return",[875,1261,1262],{"class":947}," True",[875,1264,613],{"class":884},[875,1266,1070],{"class":947},[875,1268,1269],{"class":884},"(",[875,1271,948],{"class":947},[875,1273,1274],{"class":880}," -",[875,1276,1143],{"class":884},[875,1278,1161],{"class":986},[875,1280,1281],{"class":884},"]), bucket[",[875,1283,1146],{"class":986},[875,1285,1102],{"class":884},[875,1287,1289],{"class":688,"line":1288},26,[875,1290,892],{"emptyLinePlaceholder":891},[875,1292,1294],{"class":688,"line":1293},27,[875,1295,892],{"emptyLinePlaceholder":891},[875,1297,1299,1302,1304,1307],{"class":688,"line":1298},28,[875,1300,1301],{"class":1053},"@app.middleware",[875,1303,1269],{"class":884},[875,1305,1306],{"class":986},"\"http\"",[875,1308,1309],{"class":884},")\n",[875,1311,1313,1316,1319,1322],{"class":688,"line":1312},29,[875,1314,1315],{"class":880},"async",[875,1317,1318],{"class":880}," def",[875,1320,1321],{"class":1053}," rate_limit",[875,1323,1324],{"class":884},"(request: Request, call_next):\n",[875,1326,1328,1330,1333,1336,1338,1341,1343,1346,1348,1351],{"class":688,"line":1327},30,[875,1329,1122],{"class":880},[875,1331,1332],{"class":884}," request.url.path ",[875,1334,1335],{"class":880},"in",[875,1337,983],{"class":884},[875,1339,1340],{"class":986},"\"\u002Fsent-headers\"",[875,1342,613],{"class":884},[875,1344,1345],{"class":986},"\"\u002Fdocs\"",[875,1347,613],{"class":884},[875,1349,1350],{"class":986},"\"\u002Fopenapi.json\"",[875,1352,1353],{"class":884},"}:\n",[875,1355,1357,1359,1362,1365],{"class":688,"line":1356},31,[875,1358,1222],{"class":880},[875,1360,1361],{"class":880}," await",[875,1363,1364],{"class":884}," call_next(request)   ",[875,1366,1367],{"class":957},"# Never rate-limit the docs or the inspection route.\n",[875,1369,1371],{"class":688,"line":1370},32,[875,1372,892],{"emptyLinePlaceholder":891},[875,1374,1376,1379,1381,1384,1387,1390,1393],{"class":688,"line":1375},33,[875,1377,1378],{"class":884},"    key ",[875,1380,933],{"class":880},[875,1382,1383],{"class":884}," request.client.host ",[875,1385,1386],{"class":880},"if",[875,1388,1389],{"class":884}," request.client ",[875,1391,1392],{"class":880},"else",[875,1394,1395],{"class":986}," \"anon\"\n",[875,1397,1399,1402,1404],{"class":688,"line":1398},34,[875,1400,1401],{"class":884},"    allowed, remaining, reset ",[875,1403,933],{"class":880},[875,1405,1406],{"class":884}," take_token(key)\n",[875,1408,1410,1413,1415],{"class":688,"line":1409},35,[875,1411,1412],{"class":884},"    common ",[875,1414,933],{"class":880},[875,1416,1417],{"class":884}," {\n",[875,1419,1421,1424,1426,1428,1430,1432],{"class":688,"line":1420},36,[875,1422,1423],{"class":986},"        \"X-RateLimit-Limit\"",[875,1425,990],{"class":884},[875,1427,1016],{"class":947},[875,1429,1269],{"class":884},[875,1431,948],{"class":947},[875,1433,1434],{"class":884},"),\n",[875,1436,1438,1441,1443,1445],{"class":688,"line":1437},37,[875,1439,1440],{"class":986},"        \"X-RateLimit-Remaining\"",[875,1442,990],{"class":884},[875,1444,1016],{"class":947},[875,1446,1447],{"class":884},"(remaining),\n",[875,1449,1451,1454,1456,1458,1460,1462],{"class":688,"line":1450},38,[875,1452,1453],{"class":986},"        \"X-RateLimit-Reset\"",[875,1455,990],{"class":884},[875,1457,1016],{"class":947},[875,1459,1269],{"class":884},[875,1461,1070],{"class":947},[875,1463,1464],{"class":884},"(reset)),\n",[875,1466,1468],{"class":688,"line":1467},39,[875,1469,1470],{"class":884},"    }\n",[875,1472,1474,1476,1479],{"class":688,"line":1473},40,[875,1475,1122],{"class":880},[875,1477,1478],{"class":880}," not",[875,1480,1481],{"class":884}," allowed:\n",[875,1483,1485,1488,1490,1493,1495,1498,1501,1504,1506,1508,1510],{"class":688,"line":1484},41,[875,1486,1487],{"class":884},"        retry_after ",[875,1489,933],{"class":880},[875,1491,1492],{"class":947}," max",[875,1494,1269],{"class":884},[875,1496,1497],{"class":947},"1",[875,1499,1500],{"class":884},", math.ceil(reset ",[875,1502,1503],{"class":880},"-",[875,1505,1094],{"class":947},[875,1507,1097],{"class":884},[875,1509,987],{"class":986},[875,1511,1512],{"class":884},"]))\n",[875,1514,1516,1518],{"class":688,"line":1515},42,[875,1517,1222],{"class":880},[875,1519,1520],{"class":884}," JSONResponse(\n",[875,1522,1524,1528,1530,1533],{"class":688,"line":1523},43,[875,1525,1527],{"class":1526},"sV4o_","            status_code",[875,1529,933],{"class":880},[875,1531,1532],{"class":947},"429",[875,1534,1535],{"class":884},",\n",[875,1537,1539,1542,1544,1547,1550,1553,1556,1558,1560],{"class":688,"line":1538},44,[875,1540,1541],{"class":1526},"            headers",[875,1543,933],{"class":880},[875,1545,1546],{"class":884},"{",[875,1548,1549],{"class":880},"**",[875,1551,1552],{"class":884},"common, ",[875,1554,1555],{"class":986},"\"Retry-After\"",[875,1557,990],{"class":884},[875,1559,1016],{"class":947},[875,1561,1562],{"class":884},"(retry_after)},\n",[875,1564,1566,1569,1571],{"class":688,"line":1565},45,[875,1567,1568],{"class":1526},"            content",[875,1570,933],{"class":880},[875,1572,1573],{"class":884},"{\n",[875,1575,1577,1580,1582,1585],{"class":688,"line":1576},46,[875,1578,1579],{"class":986},"                \"type\"",[875,1581,990],{"class":884},[875,1583,1584],{"class":986},"\"https:\u002F\u002Fexample.com\u002Ferrors\u002Frate-limited\"",[875,1586,1535],{"class":884},[875,1588,1590,1593,1595,1598],{"class":688,"line":1589},47,[875,1591,1592],{"class":986},"                \"title\"",[875,1594,990],{"class":884},[875,1596,1597],{"class":986},"\"Too Many Requests\"",[875,1599,1535],{"class":884},[875,1601,1603,1606,1608,1610],{"class":688,"line":1602},48,[875,1604,1605],{"class":986},"                \"status\"",[875,1607,990],{"class":884},[875,1609,1532],{"class":947},[875,1611,1535],{"class":884},[875,1613,1615,1618,1620,1623,1626,1628,1630,1633,1636,1638,1640,1642,1644,1647,1649,1652],{"class":688,"line":1614},49,[875,1616,1617],{"class":986},"                \"detail\"",[875,1619,990],{"class":884},[875,1621,1622],{"class":880},"f",[875,1624,1625],{"class":986},"\"Limit of ",[875,1627,1546],{"class":880},[875,1629,948],{"class":947},[875,1631,1632],{"class":880},"}",[875,1634,1635],{"class":986}," requests per ",[875,1637,1546],{"class":880},[875,1639,1070],{"class":947},[875,1641,1269],{"class":884},[875,1643,964],{"class":947},[875,1645,1646],{"class":884},")",[875,1648,1632],{"class":880},[875,1650,1651],{"class":986},"s exceeded for this key.\"",[875,1653,1535],{"class":884},[875,1655,1657,1660],{"class":688,"line":1656},50,[875,1658,1659],{"class":986},"                \"retry_after_seconds\"",[875,1661,1662],{"class":884},": retry_after,\n",[875,1664,1666,1669,1671,1673],{"class":688,"line":1665},51,[875,1667,1668],{"class":986},"                \"limit\"",[875,1670,990],{"class":884},[875,1672,948],{"class":947},[875,1674,1535],{"class":884},[875,1676,1678,1681,1683,1685,1687,1689],{"class":688,"line":1677},52,[875,1679,1680],{"class":986},"                \"window_seconds\"",[875,1682,990],{"class":884},[875,1684,1070],{"class":947},[875,1686,1269],{"class":884},[875,1688,964],{"class":947},[875,1690,1434],{"class":884},[875,1692,1694],{"class":688,"line":1693},53,[875,1695,1696],{"class":884},"            },\n",[875,1698,1700],{"class":688,"line":1699},54,[875,1701,1702],{"class":884},"        )\n",[875,1704,1706,1709,1711,1713],{"class":688,"line":1705},55,[875,1707,1708],{"class":884},"    response ",[875,1710,933],{"class":880},[875,1712,1361],{"class":880},[875,1714,1715],{"class":884}," call_next(request)\n",[875,1717,1719],{"class":688,"line":1718},56,[875,1720,1721],{"class":884},"    response.headers.update(common)\n",[875,1723,1725,1727],{"class":688,"line":1724},57,[875,1726,1259],{"class":880},[875,1728,1729],{"class":884}," response\n",[590,1731,1732],{},"The fourth request in the window produces this — a real 429 body:",[866,1734,1738],{"className":1735,"code":1737,"language":676,"meta":871},[1736],"language-text","$ GET \u002Fsearch\n429 Too Many Requests\n{\n  \"type\": \"https:\u002F\u002Fexample.com\u002Ferrors\u002Frate-limited\",\n  \"title\": \"Too Many Requests\",\n  \"status\": 429,\n  \"detail\": \"Limit of 3 requests per 60s exceeded for this key.\",\n  \"retry_after_seconds\": 60,\n  \"limit\": 3,\n  \"window_seconds\": 60\n}\n",[604,1739,1737],{"__ignoreMap":871},[590,1741,1742],{},"And these are the headers that actually left the process, recorded by a pure-ASGI wrapper around the whole stack so nothing could be added or lost afterwards:",[866,1744,1747],{"className":1745,"code":1746,"language":676,"meta":871},[1736],"$ GET \u002Fsent-headers\n200 OK\n{\n  \"responses\": [\n    {\n      \"path\": \"\u002Fsearch\",\n      \"status\": 200,\n      \"headers\": {\n        \"x-ratelimit-limit\": \"3\",\n        \"x-ratelimit-remaining\": \"2\",\n        \"x-ratelimit-reset\": \"1060\"\n      }\n    },\n    {\n      \"path\": \"\u002Fsearch\",\n      \"status\": 200,\n      \"headers\": {\n        \"x-ratelimit-limit\": \"3\",\n        \"x-ratelimit-remaining\": \"1\",\n        \"x-ratelimit-reset\": \"1060\"\n      }\n    },\n    {\n      \"path\": \"\u002Fsearch\",\n      \"status\": 200,\n      \"headers\": {\n        \"x-ratelimit-limit\": \"3\",\n        \"x-ratelimit-remaining\": \"0\",\n        \"x-ratelimit-reset\": \"1060\"\n      }\n    },\n    {\n      \"path\": \"\u002Fsearch\",\n      \"status\": 429,\n      \"headers\": {\n        \"x-ratelimit-limit\": \"3\",\n        \"x-ratelimit-remaining\": \"0\",\n        \"x-ratelimit-reset\": \"1060\",\n        \"retry-after\": \"60\"\n      }\n    }\n  ]\n}\n",[604,1748,1746],{"__ignoreMap":871},[590,1750,1751,1752,1755,1756,1758,1759,1761],{},"The remaining count walks 2, 1, 0 across the successful requests — a client watching that header knows it is about to be limited ",[769,1753,1754],{},"before"," it is, which is the entire value of sending quota headers on 200s. The 429 repeats the same quota headers and adds ",[604,1757,748],{},". Note the header names are lowercase in the transcript because that is how they exist on the wire in HTTP\u002F2 and in the ASGI message; HTTP header names are case-insensitive, so clients matching on ",[604,1760,784],{}," still find them.",[754,1763,1765],{"id":1764},"designing-the-error-body","Designing the Error Body",[590,1767,1768,1769,1772,1773,1775,1776,1779,1780,1783,1784,613,1787,613,1790,1793],{},"The body above follows RFC 9457 problem details: a ",[604,1770,1771],{},"type"," URI that identifies the error class, a human-readable ",[604,1774,659],{},", the ",[604,1777,1778],{},"status",", and a ",[604,1781,1782],{},"detail"," describing this occurrence. The extra members (",[604,1785,1786],{},"limit",[604,1788,1789],{},"window_seconds",[604,1791,1792],{},"retry_after_seconds",") are permitted by the spec and are what make the response machine-actionable without header parsing.",[590,1795,1796,1797,1800,1801,1803,1804,1806],{},"Three things not to put in it. Do not include the identity of ",[769,1798,1799],{},"other"," clients or global capacity numbers — that is information disclosure about your infrastructure. Do not include a stack trace or an internal limiter key. And do not vary the wording per request; a stable ",[604,1802,1771],{}," URI is what lets a client branch on the error programmatically, and a ",[604,1805,1782],{}," string that changes on every occurrence defeats log aggregation.",[590,1808,1809,1810,1813,1814,1818],{},"If your API already returns FastAPI's default ",[604,1811,1812],{},"{\"detail\": \"...\"}"," shape everywhere, keep the shape and enrich it rather than introducing a second error format for one status code. Consistency across an API is worth more than conformance to a spec on one endpoint; the general approach is in ",[639,1815,1817],{"href":1816},"\u002Fcore-architecture-routing-patterns\u002Ferror-handling-global-exceptions\u002Fglobal-exception-handlers-for-consistent-api-responses\u002F","global exception handlers for consistent API responses",".",[754,1820,1822],{"id":1821},"where-the-limiter-belongs","Where the Limiter Belongs",[590,1824,1825],{},"Middleware, for this shape of limiter, because it needs to touch every response including the rejected ones. That is what the example does and it is the simplest thing that keeps both paths consistent.",[590,1827,1828,1829,1832,1833,1836,1837,1818],{},"A dependency is the better fit when limits are per-route and per-user rather than global — different quotas on a search endpoint and an export endpoint, say. The trade-off is that a dependency raising ",[604,1830,1831],{},"HTTPException(429, headers={...})"," cannot easily add quota headers to the ",[769,1834,1835],{},"successful"," responses, so you end up with headers on rejections only. If you go that way, either accept that or pair the dependency with a thin middleware that reads a value the dependency stashed on ",[604,1838,1839],{},"request.state",[590,1841,1842,1843,1847,1848,1818],{},"Either way, the counter itself must be shared across workers. An in-process dict, like the one above, means four uvicorn workers enforce four separate limits and the effective ceiling is four times what you documented. Redis is the standard answer — see ",[639,1844,1846],{"href":1845},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Ffastapi-rate-limiting-with-redis-slowapi\u002F","FastAPI rate limiting with Redis and SlowAPI"," — and the algorithm choice, particularly the burst behaviour that a fixed window gets wrong at window boundaries, is covered in ",[639,1849,1851],{"href":1850},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Fper-user-token-bucket-throttling\u002F","per-user token bucket throttling",[754,1853,1855],{"id":1854},"verification","Verification",[590,1857,1858],{},"Assert the headers, not just the status code. A limiter that returns 429 with nothing else passes a naive test and fails its users:",[866,1860,1862],{"className":868,"code":1861,"language":870,"meta":871,"style":871},"def test_429_carries_retry_after_and_quota(client):\n    for _ in range(3):\n        assert client.get(\"\u002Fsearch\").status_code == 200\n    response = client.get(\"\u002Fsearch\")\n    assert response.status_code == 429\n    assert response.headers[\"retry-after\"] == \"60\"\n    assert response.headers[\"x-ratelimit-remaining\"] == \"0\"\n    assert response.json()[\"limit\"] == 3\n\n\ndef test_successful_responses_expose_remaining(client):\n    first = client.get(\"\u002Fsearch\")\n    assert first.headers[\"x-ratelimit-remaining\"] == \"2\"\n",[604,1863,1864,1874,1895,1915,1927,1940,1957,1973,1990,1994,1998,2007,2020],{"__ignoreMap":871},[875,1865,1866,1868,1871],{"class":688,"line":877},[875,1867,1050],{"class":880},[875,1869,1870],{"class":1053}," test_429_carries_retry_after_and_quota",[875,1872,1873],{"class":884},"(client):\n",[875,1875,1876,1879,1882,1884,1887,1889,1892],{"class":688,"line":888},[875,1877,1878],{"class":880},"    for",[875,1880,1881],{"class":884}," _ ",[875,1883,1335],{"class":880},[875,1885,1886],{"class":947}," range",[875,1888,1269],{"class":884},[875,1890,1891],{"class":947},"3",[875,1893,1894],{"class":884},"):\n",[875,1896,1897,1900,1903,1906,1909,1912],{"class":688,"line":895},[875,1898,1899],{"class":880},"        assert",[875,1901,1902],{"class":884}," client.get(",[875,1904,1905],{"class":986},"\"\u002Fsearch\"",[875,1907,1908],{"class":884},").status_code ",[875,1910,1911],{"class":880},"==",[875,1913,1914],{"class":947}," 200\n",[875,1916,1917,1919,1921,1923,1925],{"class":688,"line":909},[875,1918,1708],{"class":884},[875,1920,933],{"class":880},[875,1922,1902],{"class":884},[875,1924,1905],{"class":986},[875,1926,1309],{"class":884},[875,1928,1929,1932,1935,1937],{"class":688,"line":922},[875,1930,1931],{"class":880},"    assert",[875,1933,1934],{"class":884}," response.status_code ",[875,1936,1911],{"class":880},[875,1938,1939],{"class":947}," 429\n",[875,1941,1942,1944,1947,1950,1952,1954],{"class":688,"line":927},[875,1943,1931],{"class":880},[875,1945,1946],{"class":884}," response.headers[",[875,1948,1949],{"class":986},"\"retry-after\"",[875,1951,1208],{"class":884},[875,1953,1911],{"class":880},[875,1955,1956],{"class":986}," \"60\"\n",[875,1958,1959,1961,1963,1966,1968,1970],{"class":688,"line":939},[875,1960,1931],{"class":880},[875,1962,1946],{"class":884},[875,1964,1965],{"class":986},"\"x-ratelimit-remaining\"",[875,1967,1208],{"class":884},[875,1969,1911],{"class":880},[875,1971,1972],{"class":986}," \"0\"\n",[875,1974,1975,1977,1980,1983,1985,1987],{"class":688,"line":944},[875,1976,1931],{"class":880},[875,1978,1979],{"class":884}," response.json()[",[875,1981,1982],{"class":986},"\"limit\"",[875,1984,1208],{"class":884},[875,1986,1911],{"class":880},[875,1988,1989],{"class":947}," 3\n",[875,1991,1992],{"class":688,"line":961},[875,1993,892],{"emptyLinePlaceholder":891},[875,1995,1996],{"class":688,"line":975},[875,1997,892],{"emptyLinePlaceholder":891},[875,1999,2000,2002,2005],{"class":688,"line":1002},[875,2001,1050],{"class":880},[875,2003,2004],{"class":1053}," test_successful_responses_expose_remaining",[875,2006,1873],{"class":884},[875,2008,2009,2012,2014,2016,2018],{"class":688,"line":1007},[875,2010,2011],{"class":884},"    first ",[875,2013,933],{"class":880},[875,2015,1902],{"class":884},[875,2017,1905],{"class":986},[875,2019,1309],{"class":884},[875,2021,2022,2024,2027,2029,2031,2033],{"class":688,"line":1037},[875,2023,1931],{"class":880},[875,2025,2026],{"class":884}," first.headers[",[875,2028,1965],{"class":986},[875,2030,1208],{"class":884},[875,2032,1911],{"class":880},[875,2034,2035],{"class":986}," \"2\"\n",[590,2037,2038,2039,2043],{},"In production, count 429s per client key as a metric — the technique is in ",[639,2040,2042],{"href":2041},"\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fprometheus-metrics-for-fastapi\u002F","Prometheus metrics for FastAPI",", with the caveat that the client key must not become a metric label. A sustained 429 rate on one key is either an abusive client or a limit set too low for a legitimate integration, and you cannot tell which without looking.",[754,2045,2047],{"id":2046},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2049,2050,2051,2053,2054,2057,2058,2060],{},"Quota headers leak information. ",[604,2052,612],{}," tells an attacker exactly how much probing they get per window, and ",[604,2055,2056],{},"Remaining"," tells them how much they have left. For a public API that is a fair trade for usability. For an authentication endpoint being brute-forced it is assistance; there, send the 429 with ",[604,2059,606],{}," and nothing else.",[590,2062,2063],{},"Computing headers on every successful response costs a limiter read on the hot path even when nothing is near the limit. With a Redis-backed limiter that is a round trip per request, which may be more than you want to pay on your highest-volume endpoints — a common compromise is quota headers on write endpoints and 429-only headers on reads.",[590,2065,2066,2067,2069],{},"Finally, ",[604,2068,606],{}," is a hint, not a contract. Clients ignore it, and a client that retries immediately gets another 429, which is fine and costs you one cheap rejection. What is not fine is a fleet of clients that all wake at exactly the moment you told them to; add a little jitter on the server side by varying the advertised delay slightly, or accept the thundering herd at each window boundary.",[754,2071,2073],{"id":2072},"faq","FAQ",[590,2075,2076,2079],{},[593,2077,2078],{},"What is the difference between 429 and 503?","\n429 means this client exceeded its own quota and other clients are unaffected; the fix is for the caller to slow down. 503 means the server as a whole cannot serve right now. Sending 429 for a server-side overload misleads clients into thinking they did something wrong.",[590,2081,2082,2085],{},[593,2083,2084],{},"Is Retry-After required on a 429 response?","\nNot by the specification, but omit it and every client has to guess. Retry-After takes either a number of seconds or an HTTP date, and it turns retry behaviour from guesswork into a schedule the server controls.",[590,2087,2088,2091],{},[593,2089,2090],{},"Are X-RateLimit headers standardised?","\nNo. The X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset trio is a widely copied convention, not a standard. An IETF draft defines RateLimit-Limit and RateLimit-Remaining without the prefix; pick one shape and document it.",[590,2093,2094,2097],{},[593,2095,2096],{},"Should rate limit headers appear on successful responses too?","\nYes. Sending the remaining quota on every response lets a well-behaved client pace itself before it hits the limit, which is far better than discovering the ceiling by being rejected.",[590,2099,2100,2103],{},[593,2101,2102],{},"Why does HTTPException make it awkward to set rate limit headers?","\nIt does accept a headers argument, but a limiter usually needs to add headers to allowed responses too, and that happens in middleware. Returning a JSONResponse directly from the middleware keeps both paths in one place.",[754,2105,2107],{"id":2106},"related-reading","Related Reading",[597,2109,2110,2118,2125,2133,2141],{},[600,2111,2112,2115,2116,1818],{},[593,2113,2114],{},"Up to the topic:"," ",[639,2117,642],{"href":641},[600,2119,2120,2115,2123,1818],{},[593,2121,2122],{},"A shared counter:",[639,2124,1846],{"href":1845},[600,2126,2127,2115,2130,1818],{},[593,2128,2129],{},"Choosing the algorithm:",[639,2131,2132],{"href":1850},"Per-user token bucket throttling",[600,2134,2135,2115,2138,1818],{},[593,2136,2137],{},"Consistent error shapes:",[639,2139,2140],{"href":1816},"Global exception handlers for consistent API responses",[600,2142,2143,2115,2146,1818],{},[593,2144,2145],{},"Clients that back off:",[639,2147,2149],{"href":2148},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks\u002F","Retry and idempotency for tasks",[2151,2152,2153],"style",{},"html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}html pre.shiki code .sYEJz, html code.shiki .sYEJz{--shiki-default:#032563}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}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":871,"searchDepth":888,"depth":888,"links":2155},[2156,2157,2158,2159,2160,2161,2162,2163,2164],{"id":756,"depth":888,"text":757},{"id":822,"depth":888,"text":823},{"id":860,"depth":888,"text":861},{"id":1764,"depth":888,"text":1765},{"id":1821,"depth":888,"text":1822},{"id":1854,"depth":888,"text":1855},{"id":2046,"depth":888,"text":2047},{"id":2072,"depth":888,"text":2073},{"id":2106,"depth":888,"text":2107},"2026-07-20","Return a useful 429 from FastAPI: Retry-After, X-RateLimit headers, a problem details body, and when 503 is the honest status code instead of throttling.","md",[2169,2171,2173,2175,2177],{"q":2078,"a":2170},"429 means this client exceeded its own quota and other clients are unaffected; the fix is for the caller to slow down. 503 means the server as a whole cannot serve right now. Sending 429 for a server-side overload misleads clients into thinking they did something wrong.",{"q":2084,"a":2172},"Not by the specification, but omit it and every client has to guess. Retry-After takes either a number of seconds or an HTTP date, and it turns retry behaviour from guesswork into a schedule the server controls.",{"q":2090,"a":2174},"No. The X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset trio is a widely copied convention, not a standard. An IETF draft defines RateLimit-Limit and RateLimit-Remaining without the prefix; pick one shape and document it.",{"q":2096,"a":2176},"Yes. Sending the remaining quota on every response lets a well-behaved client pace itself before it hits the limit, which is far better than discovering the ceiling by being rejected.",{"q":2102,"a":2178},"It does accept a headers argument, but a limiter usually needs to add headers to allowed responses too, and that happens in middleware. Returning a JSONResponse directly from the middleware keeps both paths in one place.",null,{"slug":2181,"breadcrumb":2182},"rate-limit-headers-and-429-responses",[2183,2186,2189,2191],{"label":2184,"path":2185},"Home","\u002F",{"label":2187,"path":2188},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":2190,"path":641},"Rate Limiting & Throttling",{"label":2192,"path":2193},"Rate Limit Headers and 429 Responses","\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002Frate-limit-headers-and-429-responses\u002F",{"title":357,"description":2166},"article","p54KSswqFcc7EKFfLctyLPCqcO9yAt3VCzUGfY_6vek",[2179,2179],1784588203038]