[{"data":1,"prerenderedAt":3288},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002F":580,"surround-\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002F":3287},[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":201,"body":582,"dateModified":3253,"datePublished":3253,"description":3254,"extension":3255,"faq":3256,"howto":3269,"meta":3270,"navigation":964,"path":202,"seo":3284,"stem":203,"type":3285,"__hash__":3286},"content\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002Findex.md",{"type":583,"value":584,"toc":3239},"minimark",[585,589,596,642,656,776,781,787,790,802,817,820,1107,1110,1117,1139,1143,1149,1152,1350,1356,1377,1385,1654,1660,1663,1683,1689,1695,1983,1989,2004,2039,2043,2049,2052,2228,2234,2245,2248,2444,2450,2457,2461,2467,2734,2740,2747,2754,2758,2761,3038,3044,3048,3057,3067,3081,3092,3106,3110,3122,3138,3153,3165,3171,3180,3184,3235],[586,587,201],"h1",{"id":588},"concurrent-requests-with-asynciogather-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,608,619,633,639],"ul",{},[600,601,602,603,607],"li",{},"A fan-out costs the slowest upstream, not the sum of all of them — but only if you never ",[604,605,606],"code",{},"await"," them one at a time.",[600,609,610,611,614,615,618],{},"Plain ",[604,612,613],{},"asyncio.gather"," throws away good results the moment one branch fails; ",[604,616,617],{},"return_exceptions=True"," keeps them.",[600,620,621,624,625,628,629,632],{},[604,622,623],{},"asyncio.TaskGroup"," cancels siblings on failure and raises an ",[604,626,627],{},"ExceptionGroup",", which you handle with ",[604,630,631],{},"except*",".",[600,634,635,638],{},[604,636,637],{},"asyncio.timeout"," gives the endpoint a latency budget; a per-call budget lets it degrade instead of failing.",[600,640,641],{},"A semaphore converts an unbounded connection spike into controlled queuing.",[590,643,644,645,650,651,655],{},"This page is the constructive half of ",[646,647,649],"a",{"href":648},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002F","Async Correctness and Concurrency",". Once you have followed ",[646,652,654],{"href":653},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002F","Running Sync Code in a Threadpool"," and the loop is genuinely free, this is what you do with that freedom.",[657,658,659,769],"figure",{},[660,661,669,670,669,674,669,678,669,685,669,691,669,699,669,705,669,709,669,713,669,717,669,722,669,726,669,731,669,736,669,740,669,743,669,747,669,750,669,755,669,762,669,765],"svg",{"viewBox":662,"role":663,"ariaLabelledBy":664,"xmlns":667,"style":668},"0 0 720 320","img",[665,666],"fan-title","fan-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[671,672,673],"title",{"id":665},"Serial awaits versus a concurrent fan-out",[675,676,677],"desc",{"id":666},"Awaiting three upstreams of 300, 200 and 100 milliseconds one after another takes 600 milliseconds. Issuing them together takes 300 milliseconds, the duration of the slowest one.",[679,680,684],"text",{"x":681,"y":682,"style":683},"360","26","text-anchor:middle;fill:currentColor;font:600 15px sans-serif","Three upstreams: 300ms, 200ms, 100ms",[679,686,690],{"x":687,"y":688,"style":689},"20","62","text-anchor:start;fill:currentColor;font:600 13px sans-serif","Serial awaits",[692,693],"rect",{"x":687,"y":694,"width":695,"height":696,"rx":697,"style":698},"74","240","34","5","fill:none;stroke:currentColor;stroke-width:1.4",[679,700,704],{"x":701,"y":702,"style":703},"140","96","text-anchor:middle;fill:currentColor;font:12px sans-serif","profile 300ms",[692,706],{"x":707,"y":694,"width":708,"height":696,"rx":697,"style":698},"264","160",[679,710,712],{"x":711,"y":702,"style":703},"344","orders 200ms",[692,714],{"x":715,"y":694,"width":716,"height":696,"rx":697,"style":698},"428","80",[679,718,721],{"x":719,"y":702,"style":720},"468","text-anchor:middle;fill:currentColor;font:11px sans-serif","prefs",[679,723,725],{"x":724,"y":702,"style":689},"530","= 0.6s",[679,727,730],{"x":687,"y":728,"style":729},"158","text-anchor:start;fill:#00796B;font:600 13px sans-serif","Concurrent fan-out",[692,732],{"x":687,"y":733,"width":695,"height":734,"rx":697,"style":735},"170","30","fill:none;stroke:#00796B;stroke-width:1.8",[679,737,704],{"x":701,"y":738,"style":739},"190","text-anchor:middle;fill:#00796B;font:12px sans-serif",[692,741],{"x":687,"y":742,"width":708,"height":734,"rx":697,"style":735},"206",[679,744,712],{"x":745,"y":746,"style":739},"100","226",[692,748],{"x":687,"y":749,"width":716,"height":734,"rx":697,"style":735},"242",[679,751,721],{"x":752,"y":753,"style":754},"60","262","text-anchor:middle;fill:#00796B;font:11px sans-serif",[756,757],"line",{"x1":758,"y1":759,"x2":758,"y2":760,"style":761},"260","164","280","stroke:#00796B;stroke-width:1.4;stroke-dasharray:4 3",[679,763,764],{"x":724,"y":746,"style":729},"= 0.3s",[679,766,768],{"x":681,"y":767,"style":703},"306","Both timings below are measured by the endpoint itself, not estimated.",[770,771,772,773,775],"figcaption",{},"A fan-out is bounded by its slowest branch. Every serial ",[604,774,606],{}," you remove is latency the caller stops paying.",[777,778,780],"h2",{"id":779},"the-problem-this-solves","The Problem This Solves",[590,782,783,784,786],{},"An endpoint assembles a response from three or four internal services: a profile service, an order history, a preferences store. Written naively it awaits each in turn, and the endpoint's latency becomes the sum of theirs. The loop is not blocked — every ",[604,785,606],{}," yields correctly — but nothing overlaps, because nothing was ever started concurrently.",[590,788,789],{},"The fix is trivially small and the failure modes are not. Fanning out changes what happens when one upstream is down, when one is slow, and when a traffic spike multiplies your fan-out factor by your request rate and lands on a service that was sized for something gentler. This guide covers all four.",[777,791,793,794,796,797,801],{"id":792},"why-it-happens-await-starts-and-waits","Why It Happens: ",[604,795,606],{}," Starts ",[798,799,800],"em",{},"and"," Waits",[590,803,804,807,808,810,811,813,814,816],{},[604,805,806],{},"await coro"," does two things at once — it schedules the coroutine and it suspends until the result is ready. Because those are fused, a sequence of ",[604,809,606],{},"s can never overlap. To overlap, you must first turn the coroutines into scheduled tasks and only then wait for them, which is exactly what ",[604,812,613],{}," and ",[604,815,623],{}," do.",[590,818,819],{},"The measured baseline, from an app whose three \"upstreams\" sleep 300 ms, 200 ms and 100 ms:",[821,822,827],"pre",{"className":823,"code":824,"language":825,"meta":826,"style":826},"language-python shiki shiki-themes github-light-high-contrast","@app.get(\"\u002Fserial\")\nasync def serial() -> dict:\n    \"\"\"The baseline nobody wants: awaiting each call one after another.\"\"\"\n    started = time.perf_counter()\n    results = [await call_upstream(n, d) for n, d in UPSTREAMS.items()]\n    return {\"elapsed_s\": round(time.perf_counter() - started, 2), \"results\": results}\n\n\n@app.get(\"\u002Fgather\")\nasync def gather() -> dict:\n    \"\"\"The same three calls issued concurrently: wall clock collapses to the slowest one.\"\"\"\n    started = time.perf_counter()\n    results = await asyncio.gather(*[call_upstream(n, d) for n, d in UPSTREAMS.items()])\n    return {\n        \"elapsed_s\": round(time.perf_counter() - started, 2),\n        \"slowest_upstream_s\": max(UPSTREAMS.values()),\n        \"results\": results,\n    }\n","python","",[604,828,829,848,871,877,889,920,959,966,971,983,999,1005,1014,1044,1052,1073,1092,1101],{"__ignoreMap":826},[830,831,833,837,841,845],"span",{"class":756,"line":832},1,[830,834,836],{"class":835},"s3dhs","@app.get",[830,838,840],{"class":839},"sigWx","(",[830,842,844],{"class":843},"sYEJz","\"\u002Fserial\"",[830,846,847],{"class":839},")\n",[830,849,851,855,858,861,864,868],{"class":756,"line":850},2,[830,852,854],{"class":853},"sTJeM","async",[830,856,857],{"class":853}," def",[830,859,860],{"class":835}," serial",[830,862,863],{"class":839},"() -> ",[830,865,867],{"class":866},"sacAq","dict",[830,869,870],{"class":839},":\n",[830,872,874],{"class":756,"line":873},3,[830,875,876],{"class":843},"    \"\"\"The baseline nobody wants: awaiting each call one after another.\"\"\"\n",[830,878,880,883,886],{"class":756,"line":879},4,[830,881,882],{"class":839},"    started ",[830,884,885],{"class":853},"=",[830,887,888],{"class":839}," time.perf_counter()\n",[830,890,892,895,897,900,902,905,908,911,914,917],{"class":756,"line":891},5,[830,893,894],{"class":839},"    results ",[830,896,885],{"class":853},[830,898,899],{"class":839}," [",[830,901,606],{"class":853},[830,903,904],{"class":839}," call_upstream(n, d) ",[830,906,907],{"class":853},"for",[830,909,910],{"class":839}," n, d ",[830,912,913],{"class":853},"in",[830,915,916],{"class":866}," UPSTREAMS",[830,918,919],{"class":839},".items()]\n",[830,921,923,926,929,932,935,938,941,944,947,950,953,956],{"class":756,"line":922},6,[830,924,925],{"class":853},"    return",[830,927,928],{"class":839}," {",[830,930,931],{"class":843},"\"elapsed_s\"",[830,933,934],{"class":839},": ",[830,936,937],{"class":866},"round",[830,939,940],{"class":839},"(time.perf_counter() ",[830,942,943],{"class":853},"-",[830,945,946],{"class":839}," started, ",[830,948,949],{"class":866},"2",[830,951,952],{"class":839},"), ",[830,954,955],{"class":843},"\"results\"",[830,957,958],{"class":839},": results}\n",[830,960,962],{"class":756,"line":961},7,[830,963,965],{"emptyLinePlaceholder":964},true,"\n",[830,967,969],{"class":756,"line":968},8,[830,970,965],{"emptyLinePlaceholder":964},[830,972,974,976,978,981],{"class":756,"line":973},9,[830,975,836],{"class":835},[830,977,840],{"class":839},[830,979,980],{"class":843},"\"\u002Fgather\"",[830,982,847],{"class":839},[830,984,986,988,990,993,995,997],{"class":756,"line":985},10,[830,987,854],{"class":853},[830,989,857],{"class":853},[830,991,992],{"class":835}," gather",[830,994,863],{"class":839},[830,996,867],{"class":866},[830,998,870],{"class":839},[830,1000,1002],{"class":756,"line":1001},11,[830,1003,1004],{"class":843},"    \"\"\"The same three calls issued concurrently: wall clock collapses to the slowest one.\"\"\"\n",[830,1006,1008,1010,1012],{"class":756,"line":1007},12,[830,1009,882],{"class":839},[830,1011,885],{"class":853},[830,1013,888],{"class":839},[830,1015,1017,1019,1021,1024,1027,1030,1033,1035,1037,1039,1041],{"class":756,"line":1016},13,[830,1018,894],{"class":839},[830,1020,885],{"class":853},[830,1022,1023],{"class":853}," await",[830,1025,1026],{"class":839}," asyncio.gather(",[830,1028,1029],{"class":853},"*",[830,1031,1032],{"class":839},"[call_upstream(n, d) ",[830,1034,907],{"class":853},[830,1036,910],{"class":839},[830,1038,913],{"class":853},[830,1040,916],{"class":866},[830,1042,1043],{"class":839},".items()])\n",[830,1045,1047,1049],{"class":756,"line":1046},14,[830,1048,925],{"class":853},[830,1050,1051],{"class":839}," {\n",[830,1053,1055,1058,1060,1062,1064,1066,1068,1070],{"class":756,"line":1054},15,[830,1056,1057],{"class":843},"        \"elapsed_s\"",[830,1059,934],{"class":839},[830,1061,937],{"class":866},[830,1063,940],{"class":839},[830,1065,943],{"class":853},[830,1067,946],{"class":839},[830,1069,949],{"class":866},[830,1071,1072],{"class":839},"),\n",[830,1074,1076,1079,1081,1084,1086,1089],{"class":756,"line":1075},16,[830,1077,1078],{"class":843},"        \"slowest_upstream_s\"",[830,1080,934],{"class":839},[830,1082,1083],{"class":866},"max",[830,1085,840],{"class":839},[830,1087,1088],{"class":866},"UPSTREAMS",[830,1090,1091],{"class":839},".values()),\n",[830,1093,1095,1098],{"class":756,"line":1094},17,[830,1096,1097],{"class":843},"        \"results\"",[830,1099,1100],{"class":839},": results,\n",[830,1102,1104],{"class":756,"line":1103},18,[830,1105,1106],{"class":839},"    }\n",[590,1108,1109],{},"Real output from the verification harness:",[821,1111,1115],{"className":1112,"code":1114,"language":679,"meta":826},[1113],"language-text","$ GET \u002Fserial\n200 OK\n{\n  \"elapsed_s\": 0.6,\n  \"results\": [\n    {\n      \"upstream\": \"profile\",\n      \"took_s\": \"0.3\"\n    },\n    {\n      \"upstream\": \"orders\",\n      \"took_s\": \"0.2\"\n    },\n    {\n      \"upstream\": \"prefs\",\n      \"took_s\": \"0.1\"\n    }\n  ]\n}\n\n$ GET \u002Fgather\n200 OK\n{\n  \"elapsed_s\": 0.3,\n  \"slowest_upstream_s\": 0.3,\n  \"results\": [\n    {\n      \"upstream\": \"profile\",\n      \"took_s\": \"0.3\"\n    },\n    {\n      \"upstream\": \"orders\",\n      \"took_s\": \"0.2\"\n    },\n    {\n      \"upstream\": \"prefs\",\n      \"took_s\": \"0.1\"\n    }\n  ]\n}\n",[604,1116,1114],{"__ignoreMap":826},[590,1118,1119,1120,1123,1124,1127,1128,1131,1132,1134,1135,1138],{},"0.6 s becomes 0.3 s, and ",[604,1121,1122],{},"elapsed_s"," equals ",[604,1125,1126],{},"slowest_upstream_s"," exactly. Note also that ",[604,1129,1130],{},"gather"," preserved argument order in its results even though ",[604,1133,721],{}," finished first — result ordering is positional, never completion order, which is what makes it safe to ",[604,1136,1137],{},"zip"," the results back against your input list.",[777,1140,1142],{"id":1141},"partial-failure-the-argument-that-actually-matters","Partial Failure: the Argument That Actually Matters",[590,1144,1145,1146,1148],{},"Once three upstreams are involved, the probability that all three are healthy is lower than the probability that any one of them is. What ",[604,1147,1130],{}," does by default is therefore the first thing to get right.",[590,1150,1151],{},"By default, the first exception propagates to the caller the moment it occurs:",[821,1153,1155],{"className":823,"code":1154,"language":825,"meta":826,"style":826},"@app.get(\"\u002Fgather-fail-fast\")\nasync def gather_fail_fast() -> dict:\n    \"\"\"Default gather: the first exception propagates and the good results are lost.\"\"\"\n    started = time.perf_counter()\n    try:\n        await asyncio.gather(call_upstream(\"prefs\", 0.10), flaky(), call_upstream(\"orders\", 0.20))\n    except UpstreamError as exc:\n        return {\n            \"elapsed_s\": round(time.perf_counter() - started, 2),\n            \"raised\": f\"{type(exc).__name__}: {exc}\",\n            \"results_kept\": 0,\n        }\n    return {\"unreachable\": True}\n",[604,1156,1157,1168,1183,1188,1196,1203,1234,1248,1255,1274,1316,1328,1333],{"__ignoreMap":826},[830,1158,1159,1161,1163,1166],{"class":756,"line":832},[830,1160,836],{"class":835},[830,1162,840],{"class":839},[830,1164,1165],{"class":843},"\"\u002Fgather-fail-fast\"",[830,1167,847],{"class":839},[830,1169,1170,1172,1174,1177,1179,1181],{"class":756,"line":850},[830,1171,854],{"class":853},[830,1173,857],{"class":853},[830,1175,1176],{"class":835}," gather_fail_fast",[830,1178,863],{"class":839},[830,1180,867],{"class":866},[830,1182,870],{"class":839},[830,1184,1185],{"class":756,"line":873},[830,1186,1187],{"class":843},"    \"\"\"Default gather: the first exception propagates and the good results are lost.\"\"\"\n",[830,1189,1190,1192,1194],{"class":756,"line":879},[830,1191,882],{"class":839},[830,1193,885],{"class":853},[830,1195,888],{"class":839},[830,1197,1198,1201],{"class":756,"line":891},[830,1199,1200],{"class":853},"    try",[830,1202,870],{"class":839},[830,1204,1205,1208,1211,1214,1217,1220,1223,1226,1228,1231],{"class":756,"line":922},[830,1206,1207],{"class":853},"        await",[830,1209,1210],{"class":839}," asyncio.gather(call_upstream(",[830,1212,1213],{"class":843},"\"prefs\"",[830,1215,1216],{"class":839},", ",[830,1218,1219],{"class":866},"0.10",[830,1221,1222],{"class":839},"), flaky(), call_upstream(",[830,1224,1225],{"class":843},"\"orders\"",[830,1227,1216],{"class":839},[830,1229,1230],{"class":866},"0.20",[830,1232,1233],{"class":839},"))\n",[830,1235,1236,1239,1242,1245],{"class":756,"line":961},[830,1237,1238],{"class":853},"    except",[830,1240,1241],{"class":839}," UpstreamError ",[830,1243,1244],{"class":853},"as",[830,1246,1247],{"class":839}," exc:\n",[830,1249,1250,1253],{"class":756,"line":968},[830,1251,1252],{"class":853},"        return",[830,1254,1051],{"class":839},[830,1256,1257,1260,1262,1264,1266,1268,1270,1272],{"class":756,"line":973},[830,1258,1259],{"class":843},"            \"elapsed_s\"",[830,1261,934],{"class":839},[830,1263,937],{"class":866},[830,1265,940],{"class":839},[830,1267,943],{"class":853},[830,1269,946],{"class":839},[830,1271,949],{"class":866},[830,1273,1072],{"class":839},[830,1275,1276,1279,1281,1284,1287,1290,1293,1296,1299,1302,1304,1306,1309,1311,1313],{"class":756,"line":985},[830,1277,1278],{"class":843},"            \"raised\"",[830,1280,934],{"class":839},[830,1282,1283],{"class":853},"f",[830,1285,1286],{"class":843},"\"",[830,1288,1289],{"class":853},"{",[830,1291,1292],{"class":866},"type",[830,1294,1295],{"class":839},"(exc).",[830,1297,1298],{"class":866},"__name__",[830,1300,1301],{"class":853},"}",[830,1303,934],{"class":843},[830,1305,1289],{"class":853},[830,1307,1308],{"class":839},"exc",[830,1310,1301],{"class":853},[830,1312,1286],{"class":843},[830,1314,1315],{"class":839},",\n",[830,1317,1318,1321,1323,1326],{"class":756,"line":1001},[830,1319,1320],{"class":843},"            \"results_kept\"",[830,1322,934],{"class":839},[830,1324,1325],{"class":866},"0",[830,1327,1315],{"class":839},[830,1329,1330],{"class":756,"line":1007},[830,1331,1332],{"class":839},"        }\n",[830,1334,1335,1337,1339,1342,1344,1347],{"class":756,"line":1016},[830,1336,925],{"class":853},[830,1338,928],{"class":839},[830,1340,1341],{"class":843},"\"unreachable\"",[830,1343,934],{"class":839},[830,1345,1346],{"class":866},"True",[830,1348,1349],{"class":839},"}\n",[821,1351,1354],{"className":1352,"code":1353,"language":679,"meta":826},[1113],"$ GET \u002Fgather-fail-fast\n200 OK\n{\n  \"elapsed_s\": 0.05,\n  \"raised\": \"UpstreamError: billing returned 503\",\n  \"results_kept\": 0\n}\n",[604,1355,1353],{"__ignoreMap":826},[590,1357,1358,1359,1362,1363,1366,1367,813,1369,1372,1373,1376],{},"It returned after 0.05 s — the moment ",[604,1360,1361],{},"flaky()"," raised — and ",[604,1364,1365],{},"results_kept"," is 0. Worse than the lost data is what the transcript does not show: ",[604,1368,721],{},[604,1370,1371],{},"orders"," were ",[798,1374,1375],{},"not"," cancelled. They are still running, detached, and if one of them later raises you get an \"exception was never retrieved\" warning from a task nobody owns.",[590,1378,1379,1381,1382,1384],{},[604,1380,617],{}," changes the contract completely. ",[604,1383,1130],{}," now waits for everything and hands you exceptions as values:",[821,1386,1388],{"className":823,"code":1387,"language":825,"meta":826,"style":826},"@app.get(\"\u002Fgather-partial\")\nasync def gather_partial() -> dict:\n    \"\"\"return_exceptions=True: failures arrive as values, so the good data survives.\"\"\"\n    started = time.perf_counter()\n    names = [\"prefs\", \"billing\", \"orders\"]\n    raw = await asyncio.gather(\n        call_upstream(\"prefs\", 0.10),\n        flaky(),\n        call_upstream(\"orders\", 0.20),\n        return_exceptions=True,\n    )\n    ok, failed = {}, {}\n    for name, item in zip(names, raw):\n        if isinstance(item, BaseException):\n            failed[name] = f\"{type(item).__name__}: {item}\"\n        else:\n            ok[name] = item\n    return {\n        \"elapsed_s\": round(time.perf_counter() - started, 2),\n        \"ok\": ok,\n        \"failed\": failed,\n    }\n",[604,1389,1390,1401,1416,1421,1429,1452,1464,1477,1482,1494,1506,1511,1521,1537,1554,1589,1596,1606,1612,1631,1640,1649],{"__ignoreMap":826},[830,1391,1392,1394,1396,1399],{"class":756,"line":832},[830,1393,836],{"class":835},[830,1395,840],{"class":839},[830,1397,1398],{"class":843},"\"\u002Fgather-partial\"",[830,1400,847],{"class":839},[830,1402,1403,1405,1407,1410,1412,1414],{"class":756,"line":850},[830,1404,854],{"class":853},[830,1406,857],{"class":853},[830,1408,1409],{"class":835}," gather_partial",[830,1411,863],{"class":839},[830,1413,867],{"class":866},[830,1415,870],{"class":839},[830,1417,1418],{"class":756,"line":873},[830,1419,1420],{"class":843},"    \"\"\"return_exceptions=True: failures arrive as values, so the good data survives.\"\"\"\n",[830,1422,1423,1425,1427],{"class":756,"line":879},[830,1424,882],{"class":839},[830,1426,885],{"class":853},[830,1428,888],{"class":839},[830,1430,1431,1434,1436,1438,1440,1442,1445,1447,1449],{"class":756,"line":891},[830,1432,1433],{"class":839},"    names ",[830,1435,885],{"class":853},[830,1437,899],{"class":839},[830,1439,1213],{"class":843},[830,1441,1216],{"class":839},[830,1443,1444],{"class":843},"\"billing\"",[830,1446,1216],{"class":839},[830,1448,1225],{"class":843},[830,1450,1451],{"class":839},"]\n",[830,1453,1454,1457,1459,1461],{"class":756,"line":922},[830,1455,1456],{"class":839},"    raw ",[830,1458,885],{"class":853},[830,1460,1023],{"class":853},[830,1462,1463],{"class":839}," asyncio.gather(\n",[830,1465,1466,1469,1471,1473,1475],{"class":756,"line":961},[830,1467,1468],{"class":839},"        call_upstream(",[830,1470,1213],{"class":843},[830,1472,1216],{"class":839},[830,1474,1219],{"class":866},[830,1476,1072],{"class":839},[830,1478,1479],{"class":756,"line":968},[830,1480,1481],{"class":839},"        flaky(),\n",[830,1483,1484,1486,1488,1490,1492],{"class":756,"line":973},[830,1485,1468],{"class":839},[830,1487,1225],{"class":843},[830,1489,1216],{"class":839},[830,1491,1230],{"class":866},[830,1493,1072],{"class":839},[830,1495,1496,1500,1502,1504],{"class":756,"line":985},[830,1497,1499],{"class":1498},"sV4o_","        return_exceptions",[830,1501,885],{"class":853},[830,1503,1346],{"class":866},[830,1505,1315],{"class":839},[830,1507,1508],{"class":756,"line":1001},[830,1509,1510],{"class":839},"    )\n",[830,1512,1513,1516,1518],{"class":756,"line":1007},[830,1514,1515],{"class":839},"    ok, failed ",[830,1517,885],{"class":853},[830,1519,1520],{"class":839}," {}, {}\n",[830,1522,1523,1526,1529,1531,1534],{"class":756,"line":1016},[830,1524,1525],{"class":853},"    for",[830,1527,1528],{"class":839}," name, item ",[830,1530,913],{"class":853},[830,1532,1533],{"class":866}," zip",[830,1535,1536],{"class":839},"(names, raw):\n",[830,1538,1539,1542,1545,1548,1551],{"class":756,"line":1046},[830,1540,1541],{"class":853},"        if",[830,1543,1544],{"class":866}," isinstance",[830,1546,1547],{"class":839},"(item, ",[830,1549,1550],{"class":866},"BaseException",[830,1552,1553],{"class":839},"):\n",[830,1555,1556,1559,1561,1564,1566,1568,1570,1573,1575,1577,1579,1581,1584,1586],{"class":756,"line":1054},[830,1557,1558],{"class":839},"            failed[name] ",[830,1560,885],{"class":853},[830,1562,1563],{"class":853}," f",[830,1565,1286],{"class":843},[830,1567,1289],{"class":853},[830,1569,1292],{"class":866},[830,1571,1572],{"class":839},"(item).",[830,1574,1298],{"class":866},[830,1576,1301],{"class":853},[830,1578,934],{"class":843},[830,1580,1289],{"class":853},[830,1582,1583],{"class":839},"item",[830,1585,1301],{"class":853},[830,1587,1588],{"class":843},"\"\n",[830,1590,1591,1594],{"class":756,"line":1075},[830,1592,1593],{"class":853},"        else",[830,1595,870],{"class":839},[830,1597,1598,1601,1603],{"class":756,"line":1094},[830,1599,1600],{"class":839},"            ok[name] ",[830,1602,885],{"class":853},[830,1604,1605],{"class":839}," item\n",[830,1607,1608,1610],{"class":756,"line":1103},[830,1609,925],{"class":853},[830,1611,1051],{"class":839},[830,1613,1615,1617,1619,1621,1623,1625,1627,1629],{"class":756,"line":1614},19,[830,1616,1057],{"class":843},[830,1618,934],{"class":839},[830,1620,937],{"class":866},[830,1622,940],{"class":839},[830,1624,943],{"class":853},[830,1626,946],{"class":839},[830,1628,949],{"class":866},[830,1630,1072],{"class":839},[830,1632,1634,1637],{"class":756,"line":1633},20,[830,1635,1636],{"class":843},"        \"ok\"",[830,1638,1639],{"class":839},": ok,\n",[830,1641,1643,1646],{"class":756,"line":1642},21,[830,1644,1645],{"class":843},"        \"failed\"",[830,1647,1648],{"class":839},": failed,\n",[830,1650,1652],{"class":756,"line":1651},22,[830,1653,1106],{"class":839},[821,1655,1658],{"className":1656,"code":1657,"language":679,"meta":826},[1113],"$ GET \u002Fgather-partial\n200 OK\n{\n  \"elapsed_s\": 0.2,\n  \"ok\": {\n    \"prefs\": {\n      \"upstream\": \"prefs\",\n      \"took_s\": \"0.1\"\n    },\n    \"orders\": {\n      \"upstream\": \"orders\",\n      \"took_s\": \"0.2\"\n    }\n  },\n  \"failed\": {\n    \"billing\": \"UpstreamError: billing returned 503\"\n  }\n}\n",[604,1659,1657],{"__ignoreMap":826},[590,1661,1662],{},"Two real results plus one classified failure, in 0.2 s. The endpoint can now answer with degraded data and a machine-readable note about what is missing, which is almost always better for the caller than a 500.",[590,1664,1665,1666,1668,1669,1672,1673,1675,1676,1679,1680,1682],{},"Two details that bite people here. Results are ",[604,1667,1550],{}," instances, not raised — so you must test with ",[604,1670,1671],{},"isinstance"," and you will get no traceback unless you log one yourself. And ",[604,1674,617],{}," swallows ",[604,1677,1678],{},"asyncio.CancelledError"," too, which means a ",[604,1681,1130],{}," in a cancelled request can quietly keep going; if your handler needs to be cancellable, prefer a task group.",[777,1684,1686,1688],{"id":1685},"asynciotaskgroup-structured-concurrency",[604,1687,623],{},": Structured Concurrency",[590,1690,1691,1692,1694],{},"On Python 3.11+, ",[604,1693,623],{}," is the better default whenever a failed branch makes the whole operation pointless. It guarantees that no task outlives the block and cancels siblings on the first error:",[821,1696,1698],{"className":823,"code":1697,"language":825,"meta":826,"style":826},"@app.get(\"\u002Ftaskgroup\")\nasync def taskgroup() -> dict:\n    \"\"\"asyncio.TaskGroup (3.11+): a sibling failure cancels the rest and raises ExceptionGroup.\"\"\"\n    started = time.perf_counter()\n    collected: dict[str, dict] = {}\n\n    async def record(name: str, delay: float) -> None:\n        collected[name] = await call_upstream(name, delay)\n\n    errors: list[str] = []\n    try:\n        async with asyncio.TaskGroup() as tg:\n            tg.create_task(record(\"prefs\", 0.10))\n            tg.create_task(record(\"profile\", 0.30))\n            tg.create_task(flaky())\n    except* UpstreamError as eg:\n        errors = [f\"{type(e).__name__}: {e}\" for e in eg.exceptions]\n    return {\n        \"elapsed_s\": round(time.perf_counter() - started, 2),\n        \"exception_group\": errors,\n        \"completed_before_cancel\": sorted(collected),\n    }\n",[604,1699,1700,1711,1726,1731,1739,1759,1763,1792,1804,1808,1822,1828,1844,1857,1871,1876,1888,1934,1940,1958,1966,1979],{"__ignoreMap":826},[830,1701,1702,1704,1706,1709],{"class":756,"line":832},[830,1703,836],{"class":835},[830,1705,840],{"class":839},[830,1707,1708],{"class":843},"\"\u002Ftaskgroup\"",[830,1710,847],{"class":839},[830,1712,1713,1715,1717,1720,1722,1724],{"class":756,"line":850},[830,1714,854],{"class":853},[830,1716,857],{"class":853},[830,1718,1719],{"class":835}," taskgroup",[830,1721,863],{"class":839},[830,1723,867],{"class":866},[830,1725,870],{"class":839},[830,1727,1728],{"class":756,"line":873},[830,1729,1730],{"class":843},"    \"\"\"asyncio.TaskGroup (3.11+): a sibling failure cancels the rest and raises ExceptionGroup.\"\"\"\n",[830,1732,1733,1735,1737],{"class":756,"line":879},[830,1734,882],{"class":839},[830,1736,885],{"class":853},[830,1738,888],{"class":839},[830,1740,1741,1744,1747,1749,1751,1754,1756],{"class":756,"line":891},[830,1742,1743],{"class":839},"    collected: dict[",[830,1745,1746],{"class":866},"str",[830,1748,1216],{"class":839},[830,1750,867],{"class":866},[830,1752,1753],{"class":839},"] ",[830,1755,885],{"class":853},[830,1757,1758],{"class":839}," {}\n",[830,1760,1761],{"class":756,"line":922},[830,1762,965],{"emptyLinePlaceholder":964},[830,1764,1765,1768,1770,1773,1776,1778,1781,1784,1787,1790],{"class":756,"line":961},[830,1766,1767],{"class":853},"    async",[830,1769,857],{"class":853},[830,1771,1772],{"class":835}," record",[830,1774,1775],{"class":839},"(name: ",[830,1777,1746],{"class":866},[830,1779,1780],{"class":839},", delay: ",[830,1782,1783],{"class":866},"float",[830,1785,1786],{"class":839},") -> ",[830,1788,1789],{"class":866},"None",[830,1791,870],{"class":839},[830,1793,1794,1797,1799,1801],{"class":756,"line":968},[830,1795,1796],{"class":839},"        collected[name] ",[830,1798,885],{"class":853},[830,1800,1023],{"class":853},[830,1802,1803],{"class":839}," call_upstream(name, delay)\n",[830,1805,1806],{"class":756,"line":973},[830,1807,965],{"emptyLinePlaceholder":964},[830,1809,1810,1813,1815,1817,1819],{"class":756,"line":985},[830,1811,1812],{"class":839},"    errors: list[",[830,1814,1746],{"class":866},[830,1816,1753],{"class":839},[830,1818,885],{"class":853},[830,1820,1821],{"class":839}," []\n",[830,1823,1824,1826],{"class":756,"line":1001},[830,1825,1200],{"class":853},[830,1827,870],{"class":839},[830,1829,1830,1833,1836,1839,1841],{"class":756,"line":1007},[830,1831,1832],{"class":853},"        async",[830,1834,1835],{"class":853}," with",[830,1837,1838],{"class":839}," asyncio.TaskGroup() ",[830,1840,1244],{"class":853},[830,1842,1843],{"class":839}," tg:\n",[830,1845,1846,1849,1851,1853,1855],{"class":756,"line":1016},[830,1847,1848],{"class":839},"            tg.create_task(record(",[830,1850,1213],{"class":843},[830,1852,1216],{"class":839},[830,1854,1219],{"class":866},[830,1856,1233],{"class":839},[830,1858,1859,1861,1864,1866,1869],{"class":756,"line":1046},[830,1860,1848],{"class":839},[830,1862,1863],{"class":843},"\"profile\"",[830,1865,1216],{"class":839},[830,1867,1868],{"class":866},"0.30",[830,1870,1233],{"class":839},[830,1872,1873],{"class":756,"line":1054},[830,1874,1875],{"class":839},"            tg.create_task(flaky())\n",[830,1877,1878,1881,1883,1885],{"class":756,"line":1075},[830,1879,1880],{"class":853},"    except*",[830,1882,1241],{"class":839},[830,1884,1244],{"class":853},[830,1886,1887],{"class":839}," eg:\n",[830,1889,1890,1893,1895,1897,1899,1901,1903,1905,1908,1910,1912,1914,1916,1919,1921,1923,1926,1929,1931],{"class":756,"line":1094},[830,1891,1892],{"class":839},"        errors ",[830,1894,885],{"class":853},[830,1896,899],{"class":839},[830,1898,1283],{"class":853},[830,1900,1286],{"class":843},[830,1902,1289],{"class":853},[830,1904,1292],{"class":866},[830,1906,1907],{"class":839},"(e).",[830,1909,1298],{"class":866},[830,1911,1301],{"class":853},[830,1913,934],{"class":843},[830,1915,1289],{"class":853},[830,1917,1918],{"class":839},"e",[830,1920,1301],{"class":853},[830,1922,1286],{"class":843},[830,1924,1925],{"class":853}," for",[830,1927,1928],{"class":839}," e ",[830,1930,913],{"class":853},[830,1932,1933],{"class":839}," eg.exceptions]\n",[830,1935,1936,1938],{"class":756,"line":1103},[830,1937,925],{"class":853},[830,1939,1051],{"class":839},[830,1941,1942,1944,1946,1948,1950,1952,1954,1956],{"class":756,"line":1614},[830,1943,1057],{"class":843},[830,1945,934],{"class":839},[830,1947,937],{"class":866},[830,1949,940],{"class":839},[830,1951,943],{"class":853},[830,1953,946],{"class":839},[830,1955,949],{"class":866},[830,1957,1072],{"class":839},[830,1959,1960,1963],{"class":756,"line":1633},[830,1961,1962],{"class":843},"        \"exception_group\"",[830,1964,1965],{"class":839},": errors,\n",[830,1967,1968,1971,1973,1976],{"class":756,"line":1642},[830,1969,1970],{"class":843},"        \"completed_before_cancel\"",[830,1972,934],{"class":839},[830,1974,1975],{"class":866},"sorted",[830,1977,1978],{"class":839},"(collected),\n",[830,1980,1981],{"class":756,"line":1651},[830,1982,1106],{"class":839},[821,1984,1987],{"className":1985,"code":1986,"language":679,"meta":826},[1113],"$ GET \u002Ftaskgroup\n200 OK\n{\n  \"elapsed_s\": 0.05,\n  \"exception_group\": [\n    \"UpstreamError: billing returned 503\"\n  ],\n  \"completed_before_cancel\": []\n}\n",[604,1988,1986],{"__ignoreMap":826},[590,1990,1991,1994,1995,1997,1998,2000,2001,2003],{},[604,1992,1993],{},"completed_before_cancel"," is empty, and that is the point: ",[604,1996,721],{}," was 50 ms from finishing when ",[604,1999,1361],{}," raised at 0.05 s, and the task group cancelled it rather than letting it run on unobserved. Compare that with the fail-fast ",[604,2002,1130],{}," above, which returned at the same 0.05 s but left two tasks alive.",[590,2005,2006,2007,2009,2010,2012,2013,2016,2017,1216,2020,2023,2024,2027,2028,2030,2031,2034,2035,2038],{},"Note the syntax constraints, because they surprise people. ",[604,2008,631],{}," matches by member type and always hands you an ",[604,2011,627],{},", so you iterate ",[604,2014,2015],{},"eg.exceptions",". You cannot ",[604,2018,2019],{},"return",[604,2021,2022],{},"break"," or ",[604,2025,2026],{},"continue"," from inside an ",[604,2029,631],{}," block — the code above assigns to ",[604,2032,2033],{},"errors"," and returns afterwards for exactly that reason. And a bare ",[604,2036,2037],{},"except ExceptionGroup"," also works if you do not need the filtering.",[777,2040,2042],{"id":2041},"timeouts-one-budget-or-one-per-call","Timeouts: One Budget, or One per Call",[590,2044,2045,2046,2048],{},"A fan-out is only as reliable as its slowest member, so a deadline is mandatory. ",[604,2047,637],{}," is a context manager, which means it wraps whatever shape of concurrency you already have.",[590,2050,2051],{},"Around the whole fan-out, it enforces a single endpoint-level budget:",[821,2053,2055],{"className":823,"code":2054,"language":825,"meta":826,"style":826},"@app.get(\"\u002Ftimeout\")\nasync def timeout() -> dict:\n    \"\"\"asyncio.timeout wraps the whole fan-out in one deadline.\"\"\"\n    started = time.perf_counter()\n    try:\n        async with asyncio.timeout(0.35):\n            await asyncio.gather(call_upstream(\"prefs\", 0.10), slow())\n    except TimeoutError as exc:\n        return {\n            \"elapsed_s\": round(time.perf_counter() - started, 2),\n            \"budget_s\": 0.35,\n            \"raised\": type(exc).__name__,\n            \"detail\": repr(str(exc)),\n        }\n    return {\"unreachable\": True}\n",[604,2056,2057,2068,2083,2088,2096,2102,2116,2132,2144,2150,2168,2179,2193,2210,2214],{"__ignoreMap":826},[830,2058,2059,2061,2063,2066],{"class":756,"line":832},[830,2060,836],{"class":835},[830,2062,840],{"class":839},[830,2064,2065],{"class":843},"\"\u002Ftimeout\"",[830,2067,847],{"class":839},[830,2069,2070,2072,2074,2077,2079,2081],{"class":756,"line":850},[830,2071,854],{"class":853},[830,2073,857],{"class":853},[830,2075,2076],{"class":835}," timeout",[830,2078,863],{"class":839},[830,2080,867],{"class":866},[830,2082,870],{"class":839},[830,2084,2085],{"class":756,"line":873},[830,2086,2087],{"class":843},"    \"\"\"asyncio.timeout wraps the whole fan-out in one deadline.\"\"\"\n",[830,2089,2090,2092,2094],{"class":756,"line":879},[830,2091,882],{"class":839},[830,2093,885],{"class":853},[830,2095,888],{"class":839},[830,2097,2098,2100],{"class":756,"line":891},[830,2099,1200],{"class":853},[830,2101,870],{"class":839},[830,2103,2104,2106,2108,2111,2114],{"class":756,"line":922},[830,2105,1832],{"class":853},[830,2107,1835],{"class":853},[830,2109,2110],{"class":839}," asyncio.timeout(",[830,2112,2113],{"class":866},"0.35",[830,2115,1553],{"class":839},[830,2117,2118,2121,2123,2125,2127,2129],{"class":756,"line":961},[830,2119,2120],{"class":853},"            await",[830,2122,1210],{"class":839},[830,2124,1213],{"class":843},[830,2126,1216],{"class":839},[830,2128,1219],{"class":866},[830,2130,2131],{"class":839},"), slow())\n",[830,2133,2134,2136,2139,2142],{"class":756,"line":968},[830,2135,1238],{"class":853},[830,2137,2138],{"class":866}," TimeoutError",[830,2140,2141],{"class":853}," as",[830,2143,1247],{"class":839},[830,2145,2146,2148],{"class":756,"line":973},[830,2147,1252],{"class":853},[830,2149,1051],{"class":839},[830,2151,2152,2154,2156,2158,2160,2162,2164,2166],{"class":756,"line":985},[830,2153,1259],{"class":843},[830,2155,934],{"class":839},[830,2157,937],{"class":866},[830,2159,940],{"class":839},[830,2161,943],{"class":853},[830,2163,946],{"class":839},[830,2165,949],{"class":866},[830,2167,1072],{"class":839},[830,2169,2170,2173,2175,2177],{"class":756,"line":1001},[830,2171,2172],{"class":843},"            \"budget_s\"",[830,2174,934],{"class":839},[830,2176,2113],{"class":866},[830,2178,1315],{"class":839},[830,2180,2181,2183,2185,2187,2189,2191],{"class":756,"line":1007},[830,2182,1278],{"class":843},[830,2184,934],{"class":839},[830,2186,1292],{"class":866},[830,2188,1295],{"class":839},[830,2190,1298],{"class":866},[830,2192,1315],{"class":839},[830,2194,2195,2198,2200,2203,2205,2207],{"class":756,"line":1016},[830,2196,2197],{"class":843},"            \"detail\"",[830,2199,934],{"class":839},[830,2201,2202],{"class":866},"repr",[830,2204,840],{"class":839},[830,2206,1746],{"class":866},[830,2208,2209],{"class":839},"(exc)),\n",[830,2211,2212],{"class":756,"line":1046},[830,2213,1332],{"class":839},[830,2215,2216,2218,2220,2222,2224,2226],{"class":756,"line":1054},[830,2217,925],{"class":853},[830,2219,928],{"class":839},[830,2221,1341],{"class":843},[830,2223,934],{"class":839},[830,2225,1346],{"class":866},[830,2227,1349],{"class":839},[821,2229,2232],{"className":2230,"code":2231,"language":679,"meta":826},[1113],"$ GET \u002Ftimeout\n200 OK\n{\n  \"elapsed_s\": 0.35,\n  \"budget_s\": 0.35,\n  \"raised\": \"TimeoutError\",\n  \"detail\": \"''\"\n}\n",[604,2233,2231],{"__ignoreMap":826},[590,2235,2236,2237,2240,2241,2244],{},"It returned at exactly the budget. Two things in that transcript matter operationally. The exception is a plain builtin ",[604,2238,2239],{},"TimeoutError"," (on 3.11+, ",[604,2242,2243],{},"asyncio.TimeoutError"," is an alias of it), and its message is the empty string — so any log line or error response you build must add the context itself, because the exception carries none.",[590,2246,2247],{},"The same primitive applied per call turns a hard failure into graceful degradation:",[821,2249,2251],{"className":823,"code":2250,"language":825,"meta":826,"style":826},"@app.get(\"\u002Ftimeout-per-call\")\nasync def timeout_per_call() -> dict:\n    \"\"\"A per-call deadline lets the fast upstreams still answer while the slow one is dropped.\"\"\"\n    started = time.perf_counter()\n\n    async def with_budget(coro, name: str):\n        try:\n            async with asyncio.timeout(0.35):\n                return await coro\n        except TimeoutError:\n            return {\"upstream\": name, \"timed_out\": True}\n\n    results = await asyncio.gather(\n        with_budget(call_upstream(\"prefs\", 0.10), \"prefs\"),\n        with_budget(slow(), \"reports\"),\n    )\n    return {\"elapsed_s\": round(time.perf_counter() - started, 2), \"results\": results}\n",[604,2252,2253,2264,2279,2284,2292,2296,2312,2319,2332,2342,2351,2373,2377,2387,2404,2414,2418],{"__ignoreMap":826},[830,2254,2255,2257,2259,2262],{"class":756,"line":832},[830,2256,836],{"class":835},[830,2258,840],{"class":839},[830,2260,2261],{"class":843},"\"\u002Ftimeout-per-call\"",[830,2263,847],{"class":839},[830,2265,2266,2268,2270,2273,2275,2277],{"class":756,"line":850},[830,2267,854],{"class":853},[830,2269,857],{"class":853},[830,2271,2272],{"class":835}," timeout_per_call",[830,2274,863],{"class":839},[830,2276,867],{"class":866},[830,2278,870],{"class":839},[830,2280,2281],{"class":756,"line":873},[830,2282,2283],{"class":843},"    \"\"\"A per-call deadline lets the fast upstreams still answer while the slow one is dropped.\"\"\"\n",[830,2285,2286,2288,2290],{"class":756,"line":879},[830,2287,882],{"class":839},[830,2289,885],{"class":853},[830,2291,888],{"class":839},[830,2293,2294],{"class":756,"line":891},[830,2295,965],{"emptyLinePlaceholder":964},[830,2297,2298,2300,2302,2305,2308,2310],{"class":756,"line":922},[830,2299,1767],{"class":853},[830,2301,857],{"class":853},[830,2303,2304],{"class":835}," with_budget",[830,2306,2307],{"class":839},"(coro, name: ",[830,2309,1746],{"class":866},[830,2311,1553],{"class":839},[830,2313,2314,2317],{"class":756,"line":961},[830,2315,2316],{"class":853},"        try",[830,2318,870],{"class":839},[830,2320,2321,2324,2326,2328,2330],{"class":756,"line":968},[830,2322,2323],{"class":853},"            async",[830,2325,1835],{"class":853},[830,2327,2110],{"class":839},[830,2329,2113],{"class":866},[830,2331,1553],{"class":839},[830,2333,2334,2337,2339],{"class":756,"line":973},[830,2335,2336],{"class":853},"                return",[830,2338,1023],{"class":853},[830,2340,2341],{"class":839}," coro\n",[830,2343,2344,2347,2349],{"class":756,"line":985},[830,2345,2346],{"class":853},"        except",[830,2348,2138],{"class":866},[830,2350,870],{"class":839},[830,2352,2353,2356,2358,2361,2364,2367,2369,2371],{"class":756,"line":1001},[830,2354,2355],{"class":853},"            return",[830,2357,928],{"class":839},[830,2359,2360],{"class":843},"\"upstream\"",[830,2362,2363],{"class":839},": name, ",[830,2365,2366],{"class":843},"\"timed_out\"",[830,2368,934],{"class":839},[830,2370,1346],{"class":866},[830,2372,1349],{"class":839},[830,2374,2375],{"class":756,"line":1007},[830,2376,965],{"emptyLinePlaceholder":964},[830,2378,2379,2381,2383,2385],{"class":756,"line":1016},[830,2380,894],{"class":839},[830,2382,885],{"class":853},[830,2384,1023],{"class":853},[830,2386,1463],{"class":839},[830,2388,2389,2392,2394,2396,2398,2400,2402],{"class":756,"line":1046},[830,2390,2391],{"class":839},"        with_budget(call_upstream(",[830,2393,1213],{"class":843},[830,2395,1216],{"class":839},[830,2397,1219],{"class":866},[830,2399,952],{"class":839},[830,2401,1213],{"class":843},[830,2403,1072],{"class":839},[830,2405,2406,2409,2412],{"class":756,"line":1054},[830,2407,2408],{"class":839},"        with_budget(slow(), ",[830,2410,2411],{"class":843},"\"reports\"",[830,2413,1072],{"class":839},[830,2415,2416],{"class":756,"line":1075},[830,2417,1510],{"class":839},[830,2419,2420,2422,2424,2426,2428,2430,2432,2434,2436,2438,2440,2442],{"class":756,"line":1094},[830,2421,925],{"class":853},[830,2423,928],{"class":839},[830,2425,931],{"class":843},[830,2427,934],{"class":839},[830,2429,937],{"class":866},[830,2431,940],{"class":839},[830,2433,943],{"class":853},[830,2435,946],{"class":839},[830,2437,949],{"class":866},[830,2439,952],{"class":839},[830,2441,955],{"class":843},[830,2443,958],{"class":839},[821,2445,2448],{"className":2446,"code":2447,"language":679,"meta":826},[1113],"$ GET \u002Ftimeout-per-call\n200 OK\n{\n  \"elapsed_s\": 0.35,\n  \"results\": [\n    {\n      \"upstream\": \"prefs\",\n      \"took_s\": \"0.1\"\n    },\n    {\n      \"upstream\": \"reports\",\n      \"timed_out\": true\n    }\n  ]\n}\n",[604,2449,2447],{"__ignoreMap":826},[590,2451,2452,2453,2456],{},"Same 0.35 s wall clock, but the caller gets real preferences data plus an explicit ",[604,2454,2455],{},"timed_out"," marker instead of nothing at all. Both patterns compose: an outer budget for the endpoint's contract, inner budgets so one bad dependency cannot consume it.",[777,2458,2460],{"id":2459},"bounding-concurrency-with-a-semaphore","Bounding Concurrency with a Semaphore",[590,2462,2463,2464,2466],{},"The last failure mode is the one that only appears in production. If each request fans out to N upstream calls and you serve R requests per second, the downstream sees N x R concurrent connections. ",[604,2465,1130],{}," will happily open all of them.",[821,2468,2470],{"className":823,"code":2469,"language":825,"meta":826,"style":826},"@app.get(\"\u002Fsemaphore\u002F{limit}\")\nasync def semaphore(limit: int) -> dict:\n    \"\"\"Bound concurrency: 8 calls of 100ms through a semaphore of `limit`.\"\"\"\n    sem = asyncio.Semaphore(limit)\n    peak = 0\n    inflight = 0\n\n    async def guarded(i: int) -> int:\n        nonlocal peak, inflight\n        async with sem:\n            inflight += 1\n            peak = max(peak, inflight)\n            await asyncio.sleep(0.10)\n            inflight -= 1\n            return i\n\n    started = time.perf_counter()\n    await asyncio.gather(*[guarded(i) for i in range(8)])\n    return {\n        \"limit\": limit,\n        \"calls\": 8,\n        \"elapsed_s\": round(time.perf_counter() - started, 2),\n        \"peak_in_flight\": peak,\n    }\n",[604,2471,2472,2488,2509,2514,2524,2534,2543,2547,2567,2575,2584,2595,2608,2619,2628,2635,2639,2647,2677,2683,2691,2702,2720,2729],{"__ignoreMap":826},[830,2473,2474,2476,2478,2481,2484,2486],{"class":756,"line":832},[830,2475,836],{"class":835},[830,2477,840],{"class":839},[830,2479,2480],{"class":843},"\"\u002Fsemaphore\u002F",[830,2482,2483],{"class":853},"{limit}",[830,2485,1286],{"class":843},[830,2487,847],{"class":839},[830,2489,2490,2492,2494,2497,2500,2503,2505,2507],{"class":756,"line":850},[830,2491,854],{"class":853},[830,2493,857],{"class":853},[830,2495,2496],{"class":835}," semaphore",[830,2498,2499],{"class":839},"(limit: ",[830,2501,2502],{"class":866},"int",[830,2504,1786],{"class":839},[830,2506,867],{"class":866},[830,2508,870],{"class":839},[830,2510,2511],{"class":756,"line":873},[830,2512,2513],{"class":843},"    \"\"\"Bound concurrency: 8 calls of 100ms through a semaphore of `limit`.\"\"\"\n",[830,2515,2516,2519,2521],{"class":756,"line":879},[830,2517,2518],{"class":839},"    sem ",[830,2520,885],{"class":853},[830,2522,2523],{"class":839}," asyncio.Semaphore(limit)\n",[830,2525,2526,2529,2531],{"class":756,"line":891},[830,2527,2528],{"class":839},"    peak ",[830,2530,885],{"class":853},[830,2532,2533],{"class":866}," 0\n",[830,2535,2536,2539,2541],{"class":756,"line":922},[830,2537,2538],{"class":839},"    inflight ",[830,2540,885],{"class":853},[830,2542,2533],{"class":866},[830,2544,2545],{"class":756,"line":961},[830,2546,965],{"emptyLinePlaceholder":964},[830,2548,2549,2551,2553,2556,2559,2561,2563,2565],{"class":756,"line":968},[830,2550,1767],{"class":853},[830,2552,857],{"class":853},[830,2554,2555],{"class":835}," guarded",[830,2557,2558],{"class":839},"(i: ",[830,2560,2502],{"class":866},[830,2562,1786],{"class":839},[830,2564,2502],{"class":866},[830,2566,870],{"class":839},[830,2568,2569,2572],{"class":756,"line":973},[830,2570,2571],{"class":853},"        nonlocal",[830,2573,2574],{"class":839}," peak, inflight\n",[830,2576,2577,2579,2581],{"class":756,"line":985},[830,2578,1832],{"class":853},[830,2580,1835],{"class":853},[830,2582,2583],{"class":839}," sem:\n",[830,2585,2586,2589,2592],{"class":756,"line":1001},[830,2587,2588],{"class":839},"            inflight ",[830,2590,2591],{"class":853},"+=",[830,2593,2594],{"class":866}," 1\n",[830,2596,2597,2600,2602,2605],{"class":756,"line":1007},[830,2598,2599],{"class":839},"            peak ",[830,2601,885],{"class":853},[830,2603,2604],{"class":866}," max",[830,2606,2607],{"class":839},"(peak, inflight)\n",[830,2609,2610,2612,2615,2617],{"class":756,"line":1016},[830,2611,2120],{"class":853},[830,2613,2614],{"class":839}," asyncio.sleep(",[830,2616,1219],{"class":866},[830,2618,847],{"class":839},[830,2620,2621,2623,2626],{"class":756,"line":1046},[830,2622,2588],{"class":839},[830,2624,2625],{"class":853},"-=",[830,2627,2594],{"class":866},[830,2629,2630,2632],{"class":756,"line":1054},[830,2631,2355],{"class":853},[830,2633,2634],{"class":839}," i\n",[830,2636,2637],{"class":756,"line":1075},[830,2638,965],{"emptyLinePlaceholder":964},[830,2640,2641,2643,2645],{"class":756,"line":1094},[830,2642,882],{"class":839},[830,2644,885],{"class":853},[830,2646,888],{"class":839},[830,2648,2649,2652,2654,2656,2659,2661,2664,2666,2669,2671,2674],{"class":756,"line":1103},[830,2650,2651],{"class":853},"    await",[830,2653,1026],{"class":839},[830,2655,1029],{"class":853},[830,2657,2658],{"class":839},"[guarded(i) ",[830,2660,907],{"class":853},[830,2662,2663],{"class":839}," i ",[830,2665,913],{"class":853},[830,2667,2668],{"class":866}," range",[830,2670,840],{"class":839},[830,2672,2673],{"class":866},"8",[830,2675,2676],{"class":839},")])\n",[830,2678,2679,2681],{"class":756,"line":1614},[830,2680,925],{"class":853},[830,2682,1051],{"class":839},[830,2684,2685,2688],{"class":756,"line":1633},[830,2686,2687],{"class":843},"        \"limit\"",[830,2689,2690],{"class":839},": limit,\n",[830,2692,2693,2696,2698,2700],{"class":756,"line":1642},[830,2694,2695],{"class":843},"        \"calls\"",[830,2697,934],{"class":839},[830,2699,2673],{"class":866},[830,2701,1315],{"class":839},[830,2703,2704,2706,2708,2710,2712,2714,2716,2718],{"class":756,"line":1651},[830,2705,1057],{"class":843},[830,2707,934],{"class":839},[830,2709,937],{"class":866},[830,2711,940],{"class":839},[830,2713,943],{"class":853},[830,2715,946],{"class":839},[830,2717,949],{"class":866},[830,2719,1072],{"class":839},[830,2721,2723,2726],{"class":756,"line":2722},23,[830,2724,2725],{"class":843},"        \"peak_in_flight\"",[830,2727,2728],{"class":839},": peak,\n",[830,2730,2732],{"class":756,"line":2731},24,[830,2733,1106],{"class":839},[821,2735,2738],{"className":2736,"code":2737,"language":679,"meta":826},[1113],"$ GET \u002Fsemaphore\u002F8\n200 OK\n{\n  \"limit\": 8,\n  \"calls\": 8,\n  \"elapsed_s\": 0.1,\n  \"peak_in_flight\": 8\n}\n\n$ GET \u002Fsemaphore\u002F2\n200 OK\n{\n  \"limit\": 2,\n  \"calls\": 8,\n  \"elapsed_s\": 0.4,\n  \"peak_in_flight\": 2\n}\n",[604,2739,2737],{"__ignoreMap":826},[590,2741,2742,2743,2746],{},"With eight tokens, all eight ran together and the batch took 0.1 s. With two, ",[604,2744,2745],{},"peak_in_flight"," never exceeded 2 and the batch took 0.4 s — four sequential pairs. The semaphore did not slow anything down; it made the queue explicit and put it somewhere you control instead of in the downstream's accept backlog.",[590,2748,2749,2750,2753],{},"A caveat that costs people an afternoon: an ",[604,2751,2752],{},"asyncio.Semaphore"," created at module scope binds to whichever event loop first awaits it. Under a multi-worker deployment each process has its own loop and therefore its own semaphore, so the effective global limit is your per-worker limit times the worker count. Size accordingly, and create per-request semaphores (as above) or loop-scoped ones in the lifespan rather than sharing module state across loops.",[777,2755,2757],{"id":2756},"verification","Verification",[590,2759,2760],{},"Timing assertions are the only honest test of concurrency, and they belong in your suite:",[821,2762,2764],{"className":823,"code":2763,"language":825,"meta":826,"style":826},"import asyncio\nimport time\n\nimport httpx\n\n\nasync def test_fan_out_overlaps():\n    transport = httpx.ASGITransport(app=app)\n    async with httpx.AsyncClient(transport=transport, base_url=\"http:\u002F\u002Ft\") as c:\n        body = (await c.get(\"\u002Fgather\")).json()\n    # Must equal the slowest branch, not the sum of the branches.\n    assert body[\"elapsed_s\"] \u003C= body[\"slowest_upstream_s\"] + 0.05\n\n\nasync def test_partial_failure_still_returns_data():\n    transport = httpx.ASGITransport(app=app)\n    async with httpx.AsyncClient(transport=transport, base_url=\"http:\u002F\u002Ft\") as c:\n        body = (await c.get(\"\u002Fgather-partial\")).json()\n    assert set(body[\"ok\"]) == {\"prefs\", \"orders\"}\n    assert \"billing\" in body[\"failed\"]\n",[604,2765,2766,2774,2781,2785,2792,2796,2800,2812,2830,2863,2883,2889,2917,2921,2925,2936,2950,2976,2992,3021],{"__ignoreMap":826},[830,2767,2768,2771],{"class":756,"line":832},[830,2769,2770],{"class":853},"import",[830,2772,2773],{"class":839}," asyncio\n",[830,2775,2776,2778],{"class":756,"line":850},[830,2777,2770],{"class":853},[830,2779,2780],{"class":839}," time\n",[830,2782,2783],{"class":756,"line":873},[830,2784,965],{"emptyLinePlaceholder":964},[830,2786,2787,2789],{"class":756,"line":879},[830,2788,2770],{"class":853},[830,2790,2791],{"class":839}," httpx\n",[830,2793,2794],{"class":756,"line":891},[830,2795,965],{"emptyLinePlaceholder":964},[830,2797,2798],{"class":756,"line":922},[830,2799,965],{"emptyLinePlaceholder":964},[830,2801,2802,2804,2806,2809],{"class":756,"line":961},[830,2803,854],{"class":853},[830,2805,857],{"class":853},[830,2807,2808],{"class":835}," test_fan_out_overlaps",[830,2810,2811],{"class":839},"():\n",[830,2813,2814,2817,2819,2822,2825,2827],{"class":756,"line":968},[830,2815,2816],{"class":839},"    transport ",[830,2818,885],{"class":853},[830,2820,2821],{"class":839}," httpx.ASGITransport(",[830,2823,2824],{"class":1498},"app",[830,2826,885],{"class":853},[830,2828,2829],{"class":839},"app)\n",[830,2831,2832,2834,2836,2839,2842,2844,2847,2850,2852,2855,2858,2860],{"class":756,"line":973},[830,2833,1767],{"class":853},[830,2835,1835],{"class":853},[830,2837,2838],{"class":839}," httpx.AsyncClient(",[830,2840,2841],{"class":1498},"transport",[830,2843,885],{"class":853},[830,2845,2846],{"class":839},"transport, ",[830,2848,2849],{"class":1498},"base_url",[830,2851,885],{"class":853},[830,2853,2854],{"class":843},"\"http:\u002F\u002Ft\"",[830,2856,2857],{"class":839},") ",[830,2859,1244],{"class":853},[830,2861,2862],{"class":839}," c:\n",[830,2864,2865,2868,2870,2873,2875,2878,2880],{"class":756,"line":985},[830,2866,2867],{"class":839},"        body ",[830,2869,885],{"class":853},[830,2871,2872],{"class":839}," (",[830,2874,606],{"class":853},[830,2876,2877],{"class":839}," c.get(",[830,2879,980],{"class":843},[830,2881,2882],{"class":839},")).json()\n",[830,2884,2885],{"class":756,"line":1001},[830,2886,2888],{"class":2887},"sFeEa","    # Must equal the slowest branch, not the sum of the branches.\n",[830,2890,2891,2894,2897,2899,2901,2904,2906,2909,2911,2914],{"class":756,"line":1007},[830,2892,2893],{"class":853},"    assert",[830,2895,2896],{"class":839}," body[",[830,2898,931],{"class":843},[830,2900,1753],{"class":839},[830,2902,2903],{"class":853},"\u003C=",[830,2905,2896],{"class":839},[830,2907,2908],{"class":843},"\"slowest_upstream_s\"",[830,2910,1753],{"class":839},[830,2912,2913],{"class":853},"+",[830,2915,2916],{"class":866}," 0.05\n",[830,2918,2919],{"class":756,"line":1016},[830,2920,965],{"emptyLinePlaceholder":964},[830,2922,2923],{"class":756,"line":1046},[830,2924,965],{"emptyLinePlaceholder":964},[830,2926,2927,2929,2931,2934],{"class":756,"line":1054},[830,2928,854],{"class":853},[830,2930,857],{"class":853},[830,2932,2933],{"class":835}," test_partial_failure_still_returns_data",[830,2935,2811],{"class":839},[830,2937,2938,2940,2942,2944,2946,2948],{"class":756,"line":1075},[830,2939,2816],{"class":839},[830,2941,885],{"class":853},[830,2943,2821],{"class":839},[830,2945,2824],{"class":1498},[830,2947,885],{"class":853},[830,2949,2829],{"class":839},[830,2951,2952,2954,2956,2958,2960,2962,2964,2966,2968,2970,2972,2974],{"class":756,"line":1094},[830,2953,1767],{"class":853},[830,2955,1835],{"class":853},[830,2957,2838],{"class":839},[830,2959,2841],{"class":1498},[830,2961,885],{"class":853},[830,2963,2846],{"class":839},[830,2965,2849],{"class":1498},[830,2967,885],{"class":853},[830,2969,2854],{"class":843},[830,2971,2857],{"class":839},[830,2973,1244],{"class":853},[830,2975,2862],{"class":839},[830,2977,2978,2980,2982,2984,2986,2988,2990],{"class":756,"line":1103},[830,2979,2867],{"class":839},[830,2981,885],{"class":853},[830,2983,2872],{"class":839},[830,2985,606],{"class":853},[830,2987,2877],{"class":839},[830,2989,1398],{"class":843},[830,2991,2882],{"class":839},[830,2993,2994,2996,2999,3002,3005,3008,3011,3013,3015,3017,3019],{"class":756,"line":1614},[830,2995,2893],{"class":853},[830,2997,2998],{"class":866}," set",[830,3000,3001],{"class":839},"(body[",[830,3003,3004],{"class":843},"\"ok\"",[830,3006,3007],{"class":839},"]) ",[830,3009,3010],{"class":853},"==",[830,3012,928],{"class":839},[830,3014,1213],{"class":843},[830,3016,1216],{"class":839},[830,3018,1225],{"class":843},[830,3020,1349],{"class":839},[830,3022,3023,3025,3028,3031,3033,3036],{"class":756,"line":1633},[830,3024,2893],{"class":853},[830,3026,3027],{"class":843}," \"billing\"",[830,3029,3030],{"class":853}," in",[830,3032,2896],{"class":839},[830,3034,3035],{"class":843},"\"failed\"",[830,3037,1451],{"class":839},[590,3039,3040,3041,632],{},"Write the assertion against a threshold that a serial implementation could not possibly meet, so a regression that removes the concurrency fails loudly. Structuring these around real HTTP fakes rather than mocks is covered in ",[646,3042,363],{"href":3043},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002F",[777,3045,3047],{"id":3046},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,3049,3050,3053,3054,3056],{},[593,3051,3052],{},"A fan-out multiplies your blast radius."," Three upstreams at 99.9% availability each give roughly 99.7% for the composite if all three are required. ",[604,3055,617],{}," plus per-call timeouts is what buys that back, and the decision of which upstreams are required is a product decision, not a technical one.",[590,3058,3059,3062,3063,3066],{},[593,3060,3061],{},"Concurrency is not free at the connection layer."," Share one ",[604,3064,3065],{},"httpx.AsyncClient"," across the fan-out so calls reuse pooled connections; creating a client per call means a fresh TLS handshake per call and often erases the gain entirely.",[590,3068,3069,3072,3073,3076,3077,632],{},[593,3070,3071],{},"Do not fan out over unbounded input."," ",[604,3074,3075],{},"asyncio.gather(*[fetch(u) for u in urls])"," is fine for three URLs and a denial-of-service against yourself for thirty thousand. Bound it with a semaphore, or move the work out of the request entirely — see ",[646,3078,3080],{"href":3079},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002F","Background Task Processing",[590,3082,3083,3086,3087,3091],{},[593,3084,3085],{},"If the data is stable, cache instead."," The fastest upstream call is the one you did not make; ",[646,3088,3090],{"href":3089},"\u002Fasync-background-tasks-observability\u002Fcaching-strategies\u002F","Caching Strategies"," often beats any amount of concurrency tuning.",[590,3093,3094,3072,3100,3102,3103,3105],{},[593,3095,3096,3097,3099],{},"Do not ",[604,3098,1130],{}," blocking functions.",[604,3101,1130],{}," only overlaps things that yield. A list of synchronous calls wrapped in coroutines still runs serially on the loop — they need ",[646,3104,654],{"href":653}," first.",[777,3107,3109],{"id":3108},"faq","FAQ",[590,3111,3112,3115,3116,3118,3119,3121],{},[593,3113,3114],{},"What does return_exceptions=True actually change?","\nWithout it, ",[604,3117,1130],{}," re-raises the first exception immediately and you lose every result that had already arrived. With it, ",[604,3120,1130],{}," always waits for all awaitables and returns exceptions as ordinary values in the result list, so you can build a partial response from the calls that succeeded.",[590,3123,3124,3127,3128,3131,3132,3134,3135,3137],{},[593,3125,3126],{},"Should I use asyncio.gather or asyncio.TaskGroup?","\nUse ",[604,3129,3130],{},"TaskGroup"," when a failure in any branch should abandon the whole operation, because it cancels the siblings for you and cannot leak an orphaned task. Use ",[604,3133,1130],{}," with ",[604,3136,617],{}," when a partial result is genuinely useful to the caller.",[590,3139,3140,3143,3144,3146,3147,3149,3150,3152],{},[593,3141,3142],{},"Why does TaskGroup raise ExceptionGroup instead of my exception?","\nBecause several tasks can fail at once, ",[604,3145,3130],{}," collects every error into an ",[604,3148,627],{}," rather than arbitrarily picking one. Handle it with ",[604,3151,631],{}," clauses, which match by member type and give you the matching sub-group.",[590,3154,3155,3158,3159,3161,3162,3164],{},[593,3156,3157],{},"Does gather stop the other calls when one fails?","\nNo. ",[604,3160,1130],{}," propagates the first exception to the caller but the remaining tasks keep running in the background, which is how orphaned tasks and unretrieved exception warnings appear. ",[604,3163,3130],{}," cancels its siblings, which is why it is the safer default.",[590,3166,3167,3170],{},[593,3168,3169],{},"Where should the timeout go, around the whole fan-out or each call?","\nAround the whole fan-out when the endpoint has one latency budget it must not exceed, and around each call when slow upstreams should be dropped individually so the fast ones still contribute. The two compose, and a per-call budget is what lets you degrade gracefully instead of failing entirely.",[590,3172,3173,3176,3177,3179],{},[593,3174,3175],{},"How do I stop a fan-out from overwhelming a downstream service?","\nWrap each call in an ",[604,3178,2752],{}," sized to the downstream's capacity so only that many run at once. The total work still completes, but in batches, which converts an unbounded connection spike into predictable queuing you control.",[777,3181,3183],{"id":3182},"related","Related",[597,3185,3186,3194,3205,3215,3225],{},[600,3187,3188,3072,3191,3193],{},[593,3189,3190],{},"Up to the topic:",[646,3192,649],{"href":648}," sets out the loop model these patterns depend on.",[600,3195,3196,3072,3199,3201,3202,3204],{},[593,3197,3198],{},"Prerequisite:",[646,3200,654],{"href":653},", because ",[604,3203,1130],{}," cannot overlap work that never yields.",[600,3206,3207,3072,3210,3214],{},[593,3208,3209],{},"When overlap does not happen:",[646,3211,3213],{"href":3212},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002F","Fixing Blocking Calls in Async Routes"," finds the call that is serialising your fan-out.",[600,3216,3217,3072,3220,3224],{},[593,3218,3219],{},"Protecting the downstream:",[646,3221,3223],{"href":3222},"\u002Fasync-background-tasks-observability\u002Frate-limiting-throttling\u002F","Rate Limiting and Throttling"," covers the same bounding problem at the edge rather than inside a handler.",[600,3226,3227,3072,3230,3234],{},[593,3228,3229],{},"Seeing it in production:",[646,3231,3233],{"href":3232},"\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002F","Observability and Tracing"," shows how to attribute latency to the branch that caused it.",[3236,3237,3238],"style",{},"html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sYEJz, html code.shiki .sYEJz{--shiki-default:#032563}html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}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 .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}",{"title":826,"searchDepth":850,"depth":850,"links":3240},[3241,3242,3244,3245,3247,3248,3249,3250,3251,3252],{"id":779,"depth":850,"text":780},{"id":792,"depth":850,"text":3243},"Why It Happens: await Starts and Waits",{"id":1141,"depth":850,"text":1142},{"id":1685,"depth":850,"text":3246},"asyncio.TaskGroup: Structured Concurrency",{"id":2041,"depth":850,"text":2042},{"id":2459,"depth":850,"text":2460},{"id":2756,"depth":850,"text":2757},{"id":3046,"depth":850,"text":3047},{"id":3108,"depth":850,"text":3109},{"id":3182,"depth":850,"text":3183},"2026-07-20","Fan out to several upstreams from one endpoint: asyncio.gather, return_exceptions, TaskGroup, asyncio.timeout budgets and semaphore-bounded concurrency.","md",[3257,3259,3261,3263,3265,3267],{"q":3114,"a":3258},"Without it, gather re-raises the first exception immediately and you lose every result that had already arrived. With it, gather always waits for all awaitables and returns exceptions as ordinary values in the result list, so you can build a partial response from the calls that succeeded.",{"q":3126,"a":3260},"Use TaskGroup when a failure in any branch should abandon the whole operation, because it cancels the siblings for you and cannot leak an orphaned task. Use gather with return_exceptions=True when a partial result is genuinely useful to the caller.",{"q":3142,"a":3262},"Because several tasks can fail at once, TaskGroup collects every error into an ExceptionGroup rather than arbitrarily picking one. Handle it with except* clauses, which match by member type and give you the matching sub-group.",{"q":3157,"a":3264},"No. gather propagates the first exception to the caller but the remaining tasks keep running in the background, which is how orphaned tasks and unretrieved exception warnings appear. TaskGroup cancels its siblings, which is why it is the safer default.",{"q":3169,"a":3266},"Around the whole fan-out when the endpoint has one latency budget it must not exceed, and around each call when slow upstreams should be dropped individually so the fast ones still contribute. The two compose, and a per-call budget is what lets you degrade gracefully instead of failing entirely.",{"q":3175,"a":3268},"Wrap each call in an asyncio.Semaphore sized to the downstream's capacity so only that many run at once. The total work still completes, but in batches, which converts an unbounded connection spike into predictable queuing you control.",null,{"slug":3271,"breadcrumb":3272},"concurrent-requests-with-asyncio-gather",[3273,3276,3279,3281],{"label":3274,"path":3275},"Home","\u002F",{"label":3277,"path":3278},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":3280,"path":648},"Async Correctness & Concurrency",{"label":3282,"path":3283},"Concurrent Requests with asyncio.gather","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002F",{"title":201,"description":3254},"article","2WSTJ9BYUKBzelzZ5meB_ZGVK9Og-ojcmSNJh4qGbVI",[3269,3269],1784588203038]