[{"data":1,"prerenderedAt":2465},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002F":580,"surround-\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002F":2464},[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":375,"body":582,"dateModified":2432,"datePublished":2432,"description":2433,"extension":2434,"faq":2435,"howto":2448,"meta":2449,"navigation":953,"path":376,"seo":2461,"stem":377,"type":2462,"__hash__":2463},"content\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002Findex.md",{"type":583,"value":584,"toc":2419},"minimark",[585,589,596,651,663,769,774,777,780,784,791,809,828,831,1110,1117,1124,1143,1170,1174,1188,1191,1547,1553,1562,1568,1584,1588,1601,1607,1627,1633,1730,1745,1749,1755,1964,1970,1984,2000,2004,2007,2026,2032,2045,2061,2065,2068,2071,2167,2177,2181,2187,2193,2223,2227,2253,2264,2280,2286,2301,2305,2314,2320,2326,2338,2350,2363,2367,2415],[586,587,375],"h1",{"id":588},"testclient-vs-httpx-asyncclient-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,615,625,631,643],"ul",{},[600,601,602,606,607,611,612,614],"li",{},[603,604,605],"code",{},"TestClient"," runs your app on a ",[608,609,610],"em",{},"different"," event loop, on a ",[608,613,610],{}," thread, via an AnyIO blocking portal.",[600,616,617,620,621,624],{},[603,618,619],{},"AsyncClient"," + ",[603,622,623],{},"ASGITransport"," awaits your app on the loop the test is already running.",[600,626,627,628,630],{},"Calling ",[603,629,605],{}," from an async test freezes that test's loop completely — measured below as zero background ticks.",[600,632,633,635,636,639,640,642],{},[603,634,605],{}," in a ",[603,637,638],{},"with"," block runs lifespan; ",[603,641,623],{}," never does.",[600,644,645,647,648,650],{},[603,646,605],{}," requests cannot overlap; three 200 ms calls took 0.6 s versus 0.2 s under ",[603,649,619],{},".",[590,652,653,654,658,659,662],{},"This page goes one level below ",[655,656,363],"a",{"href":657},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002F",", which gives the decision table. Here we establish ",[608,660,661],{},"why"," that table says what it says, with real transcripts from real pytest runs.",[664,665,666,765],"figure",{},[667,668,676,677,676,681,676,685,676,692,676,700,676,706,676,711,676,715,676,722,676,727,676,730,676,735,676,738,676,741,676,746,676,751,676,756,676,761],"svg",{"viewBox":669,"role":670,"ariaLabelledBy":671,"xmlns":674,"style":675},"0 0 720 310","img",[672,673],"cl-title","cl-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[678,679,680],"title",{"id":672},"Thread and loop layout of the two test clients",[682,683,684],"desc",{"id":673},"With TestClient the test thread blocks while a portal thread runs the app on its own event loop. With AsyncClient the app is awaited on the test's own event loop on the same thread.",[686,687,691],"text",{"x":688,"y":689,"style":690},"20","28","text-anchor:start;fill:currentColor;font:600 14px sans-serif","TestClient: two threads, two loops",[693,694],"rect",{"x":688,"y":695,"width":696,"height":697,"rx":698,"style":699},"42","300","86","9","fill:none;stroke:currentColor;stroke-width:1.6",[686,701,705],{"x":702,"y":703,"style":704},"170","66","text-anchor:middle;fill:currentColor;font:600 12px sans-serif","MainThread",[686,707,710],{"x":702,"y":708,"style":709},"88","text-anchor:middle;fill:currentColor;font:12px sans-serif","test code, blocked",[686,712,714],{"x":702,"y":713,"style":709},"110","loop makes no progress",[716,717],"line",{"x1":718,"y1":719,"x2":720,"y2":719,"style":721},"320","85","368","stroke:currentColor;stroke-width:1.4",[723,724],"polygon",{"points":725,"style":726},"368,81 378,85 368,89","fill:currentColor",[693,728],{"x":729,"y":695,"width":718,"height":697,"rx":698,"style":699},"380",[686,731,734],{"x":732,"y":703,"style":733},"540","text-anchor:middle;fill:currentColor;font:600 12px monospace","asyncio-portal thread",[686,736,737],{"x":732,"y":708,"style":709},"its own event loop",[686,739,740],{"x":732,"y":713,"style":709},"runs the ASGI app",[686,742,745],{"x":688,"y":743,"style":744},"176","text-anchor:start;fill:#00796B;font:600 14px sans-serif","AsyncClient: one thread, one loop",[693,747],{"x":688,"y":748,"width":749,"height":697,"rx":698,"style":750},"190","680","fill:none;stroke:#00796B;stroke-width:1.8",[686,752,705],{"x":753,"y":754,"style":755},"360","214","text-anchor:middle;fill:#00796B;font:600 12px sans-serif",[686,757,760],{"x":753,"y":758,"style":759},"236","text-anchor:middle;fill:#00796B;font:12px sans-serif","test coroutine and ASGI app share one event loop",[686,762,764],{"x":753,"y":763,"style":759},"258","other tasks keep running while the request is in flight",[766,767,768],"figcaption",{},"Everything else about the two clients follows from this layout.",[770,771,773],"h2",{"id":772},"the-problem-this-solves","The Problem This Solves",[590,775,776],{},"Two clients, both officially supported, both apparently doing the same thing. The advice you find is usually stylistic — \"use AsyncClient if you like async\" — which is unhelpful, because the situations where the choice actually matters are not stylistic at all: an async fixture that will not connect, startup state that is mysteriously missing, a test that hangs, a concurrency assertion that never passes.",[590,778,779],{},"All of those trace back to one structural fact, so it is worth establishing precisely rather than by rule of thumb.",[770,781,783],{"id":782},"why-it-happens-one-owns-a-loop-one-borrows-yours","Why It Happens: One Owns a Loop, One Borrows Yours",[590,785,786,787,790],{},"Your FastAPI app is an ASGI callable — ",[603,788,789],{},"await app(scope, receive, send)",". Running it requires an event loop. The two clients differ only in where that loop comes from.",[590,792,793,800,801,804,805,808],{},[593,794,795,620,797,799],{},[603,796,619],{},[603,798,623],{}," borrows yours."," ",[603,802,803],{},"ASGITransport.handle_async_request"," is itself a coroutine that awaits your app directly. There is no thread, no second loop, no bridge. When you ",[603,806,807],{},"await client.get(...)"," from an async test, the app runs as an ordinary part of that test's loop, interleaved with everything else scheduled on it.",[590,810,811,816,817,819,820,823,824,827],{},[593,812,813,815],{},[603,814,605],{}," owns one."," Starlette's ",[603,818,605],{}," presents a synchronous API, so it opens an AnyIO ",[593,821,822],{},"blocking portal",": a dedicated thread running its own event loop, plus a handle that lets synchronous code submit a coroutine and block until the result comes back. ",[603,825,826],{},"client.get(\"\u002Fx\")"," submits the ASGI call to that portal and waits.",[590,829,830],{},"The measurement:",[832,833,838],"pre",{"className":834,"code":835,"language":836,"meta":837,"style":837},"language-python shiki shiki-themes github-light-high-contrast","def test_testclient_is_a_sync_api():\n    \"\"\"No await anywhere: TestClient starts a portal that drives the loop on another thread.\"\"\"\n    REQUEST_ID.set(\"set-by-the-test\")\n    with TestClient(app) as client:\n        SEEN[\"testclient\"] = client.get(\"\u002Fwhoami\").json()\n    assert SEEN[\"testclient\"][\"thread\"] != threading.current_thread().name\n\n\nasync def test_asyncclient_runs_in_the_calling_loop():\n    \"\"\"ASGITransport awaits the app directly, on the loop the test is already running.\"\"\"\n    REQUEST_ID.set(\"set-by-the-test\")\n    async with AsyncClient(transport=ASGITransport(app=app), base_url=\"http:\u002F\u002Ftest\") as client:\n        SEEN[\"asyncclient\"] = (await client.get(\"\u002Fwhoami\")).json()\n    assert SEEN[\"asyncclient\"][\"loop_id\"] == id(asyncio.get_running_loop())\n    assert SEEN[\"asyncclient\"][\"thread\"] == threading.current_thread().name\n","python","",[603,839,840,856,863,879,894,921,948,955,960,974,980,991,1035,1062,1089],{"__ignoreMap":837},[841,842,844,848,852],"span",{"class":716,"line":843},1,[841,845,847],{"class":846},"sTJeM","def",[841,849,851],{"class":850},"s3dhs"," test_testclient_is_a_sync_api",[841,853,855],{"class":854},"sigWx","():\n",[841,857,859],{"class":716,"line":858},2,[841,860,862],{"class":861},"sYEJz","    \"\"\"No await anywhere: TestClient starts a portal that drives the loop on another thread.\"\"\"\n",[841,864,866,870,873,876],{"class":716,"line":865},3,[841,867,869],{"class":868},"sacAq","    REQUEST_ID",[841,871,872],{"class":854},".set(",[841,874,875],{"class":861},"\"set-by-the-test\"",[841,877,878],{"class":854},")\n",[841,880,882,885,888,891],{"class":716,"line":881},4,[841,883,884],{"class":846},"    with",[841,886,887],{"class":854}," TestClient(app) ",[841,889,890],{"class":846},"as",[841,892,893],{"class":854}," client:\n",[841,895,897,900,903,906,909,912,915,918],{"class":716,"line":896},5,[841,898,899],{"class":868},"        SEEN",[841,901,902],{"class":854},"[",[841,904,905],{"class":861},"\"testclient\"",[841,907,908],{"class":854},"] ",[841,910,911],{"class":846},"=",[841,913,914],{"class":854}," client.get(",[841,916,917],{"class":861},"\"\u002Fwhoami\"",[841,919,920],{"class":854},").json()\n",[841,922,924,927,930,932,934,937,940,942,945],{"class":716,"line":923},6,[841,925,926],{"class":846},"    assert",[841,928,929],{"class":868}," SEEN",[841,931,902],{"class":854},[841,933,905],{"class":861},[841,935,936],{"class":854},"][",[841,938,939],{"class":861},"\"thread\"",[841,941,908],{"class":854},[841,943,944],{"class":846},"!=",[841,946,947],{"class":854}," threading.current_thread().name\n",[841,949,951],{"class":716,"line":950},7,[841,952,954],{"emptyLinePlaceholder":953},true,"\n",[841,956,958],{"class":716,"line":957},8,[841,959,954],{"emptyLinePlaceholder":953},[841,961,963,966,969,972],{"class":716,"line":962},9,[841,964,965],{"class":846},"async",[841,967,968],{"class":846}," def",[841,970,971],{"class":850}," test_asyncclient_runs_in_the_calling_loop",[841,973,855],{"class":854},[841,975,977],{"class":716,"line":976},10,[841,978,979],{"class":861},"    \"\"\"ASGITransport awaits the app directly, on the loop the test is already running.\"\"\"\n",[841,981,983,985,987,989],{"class":716,"line":982},11,[841,984,869],{"class":868},[841,986,872],{"class":854},[841,988,875],{"class":861},[841,990,878],{"class":854},[841,992,994,997,1000,1003,1007,1009,1012,1015,1017,1020,1023,1025,1028,1031,1033],{"class":716,"line":993},12,[841,995,996],{"class":846},"    async",[841,998,999],{"class":846}," with",[841,1001,1002],{"class":854}," AsyncClient(",[841,1004,1006],{"class":1005},"sV4o_","transport",[841,1008,911],{"class":846},[841,1010,1011],{"class":854},"ASGITransport(",[841,1013,1014],{"class":1005},"app",[841,1016,911],{"class":846},[841,1018,1019],{"class":854},"app), ",[841,1021,1022],{"class":1005},"base_url",[841,1024,911],{"class":846},[841,1026,1027],{"class":861},"\"http:\u002F\u002Ftest\"",[841,1029,1030],{"class":854},") ",[841,1032,890],{"class":846},[841,1034,893],{"class":854},[841,1036,1038,1040,1042,1045,1047,1049,1052,1055,1057,1059],{"class":716,"line":1037},13,[841,1039,899],{"class":868},[841,1041,902],{"class":854},[841,1043,1044],{"class":861},"\"asyncclient\"",[841,1046,908],{"class":854},[841,1048,911],{"class":846},[841,1050,1051],{"class":854}," (",[841,1053,1054],{"class":846},"await",[841,1056,914],{"class":854},[841,1058,917],{"class":861},[841,1060,1061],{"class":854},")).json()\n",[841,1063,1065,1067,1069,1071,1073,1075,1078,1080,1083,1086],{"class":716,"line":1064},14,[841,1066,926],{"class":846},[841,1068,929],{"class":868},[841,1070,902],{"class":854},[841,1072,1044],{"class":861},[841,1074,936],{"class":854},[841,1076,1077],{"class":861},"\"loop_id\"",[841,1079,908],{"class":854},[841,1081,1082],{"class":846},"==",[841,1084,1085],{"class":868}," id",[841,1087,1088],{"class":854},"(asyncio.get_running_loop())\n",[841,1090,1092,1094,1096,1098,1100,1102,1104,1106,1108],{"class":716,"line":1091},15,[841,1093,926],{"class":846},[841,1095,929],{"class":868},[841,1097,902],{"class":854},[841,1099,1044],{"class":861},[841,1101,936],{"class":854},[841,1103,939],{"class":861},[841,1105,908],{"class":854},[841,1107,1082],{"class":846},[841,1109,947],{"class":854},[590,1111,1112,1113,1116],{},"The endpoint reports ",[603,1114,1115],{},"id(asyncio.get_running_loop())",", its thread name, and a context variable. Real output:",[832,1118,1122],{"className":1119,"code":1121,"language":686,"meta":837},[1120],"language-text","$ GET \u002Fpytest\u002Floops\n200 OK\n..\nsame_loop            = False\ntestclient_thread    = asyncio-portal-\u003Cid>\nasyncclient_thread   = MainThread\ncontextvar_seen_by_testclient  = 'set-by-the-test'\ncontextvar_seen_by_asyncclient = 'set-by-the-test'\n.\n3 passed in 0.00s\n",[603,1123,1121],{"__ignoreMap":837},[590,1125,1126,1129,1130,1132,1133,1136,1137,1139,1140,1142],{},[603,1127,1128],{},"same_loop = False"," is the whole story in one line. Under ",[603,1131,605],{}," the app ran on a thread literally named ",[603,1134,1135],{},"asyncio-portal-\u003Cid>","; under ",[603,1138,619],{}," it ran on ",[603,1141,705],{}," on the very loop the test was using — the assertion comparing loop ids passed.",[590,1144,1145,1146,1149,1150,1153,1154,1158,1159,1161,1162,1165,1166,1169],{},"The contextvar result is worth pausing on, because it contradicts a common assumption. AnyIO copies the caller's ",[603,1147,1148],{},"contextvars.Context"," into the portal, so a value set in the test ",[593,1151,1152],{},"was"," visible inside the endpoint under both clients. Request-id context propagation, of the kind described in ",[655,1155,1157],{"href":1156},"\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002Fstructured-json-logging-with-request-ids\u002F","Structured JSON Logging with Request IDs",", therefore works fine under ",[603,1160,605],{},". What does not transfer is anything holding a reference to a ",[608,1163,1164],{},"specific loop"," — an ",[603,1167,1168],{},"asyncio.Lock"," that has been awaited, a connection pool that registered callbacks, a task created in your test. Those are loop-bound in a way contextvars are not.",[770,1171,1173],{"id":1172},"where-testclient-breaks-it-blocks-your-loop","Where TestClient Breaks: It Blocks Your Loop",[590,1175,1176,1177,1180,1181,1184,1185,1187],{},"The most consequential difference is invisible until you look for it. ",[603,1178,1179],{},"client.get()"," is a ",[608,1182,1183],{},"blocking"," wait. Inside a synchronous test that is fine — there is nothing else to run. Inside an ",[603,1186,965],{}," test, the thread it blocks is the thread running your event loop.",[590,1189,1190],{},"This suite quantifies it. A background task ticks every 10 ms; we count how many ticks land during a 200 ms request.",[832,1192,1194],{"className":834,"code":1193,"language":836,"meta":837,"style":837},"async def count_ticks_while(action) -> int:\n    \"\"\"A background task ticks every 10ms; how many ticks land during `action`?\"\"\"\n    ticks = 0\n\n    async def ticker():\n        nonlocal ticks\n        while True:\n            await asyncio.sleep(0.01)\n            ticks += 1\n\n    task = asyncio.create_task(ticker())\n    await asyncio.sleep(0.05)\n    before = ticks\n    await action()\n    task.cancel()\n    return ticks - before\n\n\nasync def test_testclient_freezes_the_calling_loop():\n    async def call():\n        with TestClient(app) as client:\n            client.get(\"\u002Fslow\")  # 200ms of app time, spent blocking this thread\n\n    ticks = await count_ticks_while(call)\n    assert ticks == 0\n\n\nasync def test_asyncclient_leaves_the_loop_free():\n    async def call():\n        async with AsyncClient(transport=ASGITransport(app=app), base_url=\"http:\u002F\u002Ft\") as client:\n            await client.get(\"\u002Fslow\")\n\n    ticks = await count_ticks_while(call)\n    assert ticks >= 15\n",[603,1195,1196,1214,1219,1229,1233,1244,1252,1262,1275,1286,1290,1300,1312,1321,1328,1333,1348,1353,1358,1370,1382,1394,1410,1415,1428,1439,1444,1449,1461,1472,1507,1518,1523,1534],{"__ignoreMap":837},[841,1197,1198,1200,1202,1205,1208,1211],{"class":716,"line":843},[841,1199,965],{"class":846},[841,1201,968],{"class":846},[841,1203,1204],{"class":850}," count_ticks_while",[841,1206,1207],{"class":854},"(action) -> ",[841,1209,1210],{"class":868},"int",[841,1212,1213],{"class":854},":\n",[841,1215,1216],{"class":716,"line":858},[841,1217,1218],{"class":861},"    \"\"\"A background task ticks every 10ms; how many ticks land during `action`?\"\"\"\n",[841,1220,1221,1224,1226],{"class":716,"line":865},[841,1222,1223],{"class":854},"    ticks ",[841,1225,911],{"class":846},[841,1227,1228],{"class":868}," 0\n",[841,1230,1231],{"class":716,"line":881},[841,1232,954],{"emptyLinePlaceholder":953},[841,1234,1235,1237,1239,1242],{"class":716,"line":896},[841,1236,996],{"class":846},[841,1238,968],{"class":846},[841,1240,1241],{"class":850}," ticker",[841,1243,855],{"class":854},[841,1245,1246,1249],{"class":716,"line":923},[841,1247,1248],{"class":846},"        nonlocal",[841,1250,1251],{"class":854}," ticks\n",[841,1253,1254,1257,1260],{"class":716,"line":950},[841,1255,1256],{"class":846},"        while",[841,1258,1259],{"class":868}," True",[841,1261,1213],{"class":854},[841,1263,1264,1267,1270,1273],{"class":716,"line":957},[841,1265,1266],{"class":846},"            await",[841,1268,1269],{"class":854}," asyncio.sleep(",[841,1271,1272],{"class":868},"0.01",[841,1274,878],{"class":854},[841,1276,1277,1280,1283],{"class":716,"line":962},[841,1278,1279],{"class":854},"            ticks ",[841,1281,1282],{"class":846},"+=",[841,1284,1285],{"class":868}," 1\n",[841,1287,1288],{"class":716,"line":976},[841,1289,954],{"emptyLinePlaceholder":953},[841,1291,1292,1295,1297],{"class":716,"line":982},[841,1293,1294],{"class":854},"    task ",[841,1296,911],{"class":846},[841,1298,1299],{"class":854}," asyncio.create_task(ticker())\n",[841,1301,1302,1305,1307,1310],{"class":716,"line":993},[841,1303,1304],{"class":846},"    await",[841,1306,1269],{"class":854},[841,1308,1309],{"class":868},"0.05",[841,1311,878],{"class":854},[841,1313,1314,1317,1319],{"class":716,"line":1037},[841,1315,1316],{"class":854},"    before ",[841,1318,911],{"class":846},[841,1320,1251],{"class":854},[841,1322,1323,1325],{"class":716,"line":1064},[841,1324,1304],{"class":846},[841,1326,1327],{"class":854}," action()\n",[841,1329,1330],{"class":716,"line":1091},[841,1331,1332],{"class":854},"    task.cancel()\n",[841,1334,1336,1339,1342,1345],{"class":716,"line":1335},16,[841,1337,1338],{"class":846},"    return",[841,1340,1341],{"class":854}," ticks ",[841,1343,1344],{"class":846},"-",[841,1346,1347],{"class":854}," before\n",[841,1349,1351],{"class":716,"line":1350},17,[841,1352,954],{"emptyLinePlaceholder":953},[841,1354,1356],{"class":716,"line":1355},18,[841,1357,954],{"emptyLinePlaceholder":953},[841,1359,1361,1363,1365,1368],{"class":716,"line":1360},19,[841,1362,965],{"class":846},[841,1364,968],{"class":846},[841,1366,1367],{"class":850}," test_testclient_freezes_the_calling_loop",[841,1369,855],{"class":854},[841,1371,1373,1375,1377,1380],{"class":716,"line":1372},20,[841,1374,996],{"class":846},[841,1376,968],{"class":846},[841,1378,1379],{"class":850}," call",[841,1381,855],{"class":854},[841,1383,1385,1388,1390,1392],{"class":716,"line":1384},21,[841,1386,1387],{"class":846},"        with",[841,1389,887],{"class":854},[841,1391,890],{"class":846},[841,1393,893],{"class":854},[841,1395,1397,1400,1403,1406],{"class":716,"line":1396},22,[841,1398,1399],{"class":854},"            client.get(",[841,1401,1402],{"class":861},"\"\u002Fslow\"",[841,1404,1405],{"class":854},")  ",[841,1407,1409],{"class":1408},"sFeEa","# 200ms of app time, spent blocking this thread\n",[841,1411,1413],{"class":716,"line":1412},23,[841,1414,954],{"emptyLinePlaceholder":953},[841,1416,1418,1420,1422,1425],{"class":716,"line":1417},24,[841,1419,1223],{"class":854},[841,1421,911],{"class":846},[841,1423,1424],{"class":846}," await",[841,1426,1427],{"class":854}," count_ticks_while(call)\n",[841,1429,1431,1433,1435,1437],{"class":716,"line":1430},25,[841,1432,926],{"class":846},[841,1434,1341],{"class":854},[841,1436,1082],{"class":846},[841,1438,1228],{"class":868},[841,1440,1442],{"class":716,"line":1441},26,[841,1443,954],{"emptyLinePlaceholder":953},[841,1445,1447],{"class":716,"line":1446},27,[841,1448,954],{"emptyLinePlaceholder":953},[841,1450,1452,1454,1456,1459],{"class":716,"line":1451},28,[841,1453,965],{"class":846},[841,1455,968],{"class":846},[841,1457,1458],{"class":850}," test_asyncclient_leaves_the_loop_free",[841,1460,855],{"class":854},[841,1462,1464,1466,1468,1470],{"class":716,"line":1463},29,[841,1465,996],{"class":846},[841,1467,968],{"class":846},[841,1469,1379],{"class":850},[841,1471,855],{"class":854},[841,1473,1475,1478,1480,1482,1484,1486,1488,1490,1492,1494,1496,1498,1501,1503,1505],{"class":716,"line":1474},30,[841,1476,1477],{"class":846},"        async",[841,1479,999],{"class":846},[841,1481,1002],{"class":854},[841,1483,1006],{"class":1005},[841,1485,911],{"class":846},[841,1487,1011],{"class":854},[841,1489,1014],{"class":1005},[841,1491,911],{"class":846},[841,1493,1019],{"class":854},[841,1495,1022],{"class":1005},[841,1497,911],{"class":846},[841,1499,1500],{"class":861},"\"http:\u002F\u002Ft\"",[841,1502,1030],{"class":854},[841,1504,890],{"class":846},[841,1506,893],{"class":854},[841,1508,1510,1512,1514,1516],{"class":716,"line":1509},31,[841,1511,1266],{"class":846},[841,1513,914],{"class":854},[841,1515,1402],{"class":861},[841,1517,878],{"class":854},[841,1519,1521],{"class":716,"line":1520},32,[841,1522,954],{"emptyLinePlaceholder":953},[841,1524,1526,1528,1530,1532],{"class":716,"line":1525},33,[841,1527,1223],{"class":854},[841,1529,911],{"class":846},[841,1531,1424],{"class":846},[841,1533,1427],{"class":854},[841,1535,1537,1539,1541,1544],{"class":716,"line":1536},34,[841,1538,926],{"class":846},[841,1540,1341],{"class":854},[841,1542,1543],{"class":846},">=",[841,1545,1546],{"class":868}," 15\n",[832,1548,1551],{"className":1549,"code":1550,"language":686,"meta":837},[1120],"$ GET \u002Fpytest\u002Fblocking\n200 OK\n\nticks during a 200ms TestClient call  = 0\n.ticks during a 200ms AsyncClient call = at least 15: True\n.\n2 passed in 0.00s\n",[603,1552,1550],{"__ignoreMap":837},[590,1554,1555,1558,1559,1561],{},[593,1556,1557],{},"Zero."," During a 200 ms request there was time for roughly twenty ticks, and not one occurred. The loop was completely frozen; the ticker task did not advance a single step. Under ",[603,1560,619],{}," the same 200 ms request left the loop free and at least fifteen ticks landed.",[590,1563,1564,1565,1567],{},"This is not an abstract concern. If your test has a running background task, a polling helper, an async context manager that refreshes something on a timer, or a fixture holding an open connection with a heartbeat, ",[603,1566,605],{}," inside an async test stops all of it. And the symptom is not an error — it is a test that hangs, or one whose timing assertions make no sense.",[590,1569,1570,1571,1574,1575,1578,1579,1583],{},"It is worth naming the symmetry: this is the ",[608,1572,1573],{},"same"," failure as calling a blocking function inside an ",[603,1576,1577],{},"async def"," endpoint, described in ",[655,1580,1582],{"href":1581},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002F","Fixing Blocking Calls in Async Routes",". The test suite obeys the rules the application obeys.",[770,1585,1587],{"id":1586},"where-testclient-wins-lifespan-for-free","Where TestClient Wins: Lifespan for Free",[590,1589,1590,1591,1593,1594,1596,1597,1600],{},"The trade runs the other way on startup. ",[603,1592,605],{}," used as a context manager runs the full lifespan; ",[603,1595,623],{}," never does, because it only implements the ",[603,1598,1599],{},"http"," scope.",[832,1602,1605],{"className":1603,"code":1604,"language":686,"meta":837},[1120],"$ GET \u002Fpytest\u002Flifespan\n200 OK\n\nTestClient inside `with`        -> lifespan_ran True\n.TestClient without `with`      -> lifespan_ran False\n.AsyncClient + ASGITransport    -> lifespan_ran False\n.AsyncClient + lifespan_context -> lifespan_ran True\n.\n4 passed in 0.00s\n",[603,1606,1604],{"__ignoreMap":837},[590,1608,1609,1610,800,1613,1616,1617,1619,1620,1623,1624,650],{},"Four real results against an app whose lifespan flips a flag. The second line is the trap that costs the most debugging time: ",[603,1611,1612],{},"TestClient(app)",[608,1614,1615],{},"without"," a ",[603,1618,638],{}," block does not raise, does not warn, and silently skips startup. Every request works until one touches ",[603,1621,1622],{},"app.state"," and gets an ",[603,1625,1626],{},"AttributeError",[590,1628,1629,1630,1632],{},"Under ",[603,1631,619],{},", run the lifespan explicitly when you need it:",[832,1634,1636],{"className":834,"code":1635,"language":836,"meta":837,"style":837},"async def test_asgitransport_with_lifespanmanager_style_startup():\n    \"\"\"Drive the lifespan yourself when you need startup state under AsyncClient.\"\"\"\n    async with app.router.lifespan_context(app):\n        async with AsyncClient(transport=T(app=app), base_url=\"http:\u002F\u002Ftest\") as client:\n            assert (await client.get(\"\u002Flifespan\")).json() == {\"lifespan_ran\": True}\n",[603,1637,1638,1649,1654,1663,1696],{"__ignoreMap":837},[841,1639,1640,1642,1644,1647],{"class":716,"line":843},[841,1641,965],{"class":846},[841,1643,968],{"class":846},[841,1645,1646],{"class":850}," test_asgitransport_with_lifespanmanager_style_startup",[841,1648,855],{"class":854},[841,1650,1651],{"class":716,"line":858},[841,1652,1653],{"class":861},"    \"\"\"Drive the lifespan yourself when you need startup state under AsyncClient.\"\"\"\n",[841,1655,1656,1658,1660],{"class":716,"line":865},[841,1657,996],{"class":846},[841,1659,999],{"class":846},[841,1661,1662],{"class":854}," app.router.lifespan_context(app):\n",[841,1664,1665,1667,1669,1671,1673,1675,1678,1680,1682,1684,1686,1688,1690,1692,1694],{"class":716,"line":881},[841,1666,1477],{"class":846},[841,1668,999],{"class":846},[841,1670,1002],{"class":854},[841,1672,1006],{"class":1005},[841,1674,911],{"class":846},[841,1676,1677],{"class":854},"T(",[841,1679,1014],{"class":1005},[841,1681,911],{"class":846},[841,1683,1019],{"class":854},[841,1685,1022],{"class":1005},[841,1687,911],{"class":846},[841,1689,1027],{"class":861},[841,1691,1030],{"class":854},[841,1693,890],{"class":846},[841,1695,893],{"class":854},[841,1697,1698,1701,1703,1705,1707,1710,1713,1715,1718,1721,1724,1727],{"class":716,"line":896},[841,1699,1700],{"class":846},"            assert",[841,1702,1051],{"class":854},[841,1704,1054],{"class":846},[841,1706,914],{"class":854},[841,1708,1709],{"class":861},"\"\u002Flifespan\"",[841,1711,1712],{"class":854},")).json() ",[841,1714,1082],{"class":846},[841,1716,1717],{"class":854}," {",[841,1719,1720],{"class":861},"\"lifespan_ran\"",[841,1722,1723],{"class":854},": ",[841,1725,1726],{"class":868},"True",[841,1728,1729],{"class":854},"}\n",[590,1731,1732,1733,1736,1737,1740,1741,650],{},"That is the fourth line of the transcript, and it passed. ",[603,1734,1735],{},"app.router.lifespan_context(app)"," is the same context Starlette itself enters on startup, so the third-party ",[603,1738,1739],{},"asgi-lifespan"," package is optional — useful mainly for its extra timeout and error handling. Put this in a session-scoped fixture if startup is expensive, but be aware that shared startup state then leaks between tests, so most suites are better off with a function-scoped lifespan and cheap startup. Lifespan design is covered in ",[655,1742,1744],{"href":1743},"\u002Fcore-architecture-routing-patterns\u002Fapplication-factory-patterns\u002Flifespan-events-vs-startup-shutdown\u002F","Lifespan Events vs Startup Shutdown",[770,1746,1748],{"id":1747},"concurrency-only-one-client-can-express-it","Concurrency: Only One Client Can Express It",[590,1750,1751,1752,1754],{},"A blocking call cannot overlap with another blocking call on the same thread. That makes some tests simply unwritable with ",[603,1753,605],{},":",[832,1756,1758],{"className":834,"code":1757,"language":836,"meta":837,"style":837},"def test_testclient_requests_serialise():\n    with TestClient(app) as client:\n        started = time.perf_counter()\n        for _ in range(3):\n            client.get(\"\u002Fslow\")\n        elapsed = time.perf_counter() - started\n    assert elapsed >= 0.6\n\n\nasync def test_asyncclient_requests_overlap():\n    async with AsyncClient(transport=ASGITransport(app=app), base_url=\"http:\u002F\u002Ft\") as client:\n        started = time.perf_counter()\n        await asyncio.gather(*[client.get(\"\u002Fslow\") for _ in range(3)])\n        elapsed = time.perf_counter() - started\n    assert elapsed \u003C 0.4\n",[603,1759,1760,1769,1779,1789,1812,1820,1835,1847,1851,1855,1866,1898,1906,1940,1952],{"__ignoreMap":837},[841,1761,1762,1764,1767],{"class":716,"line":843},[841,1763,847],{"class":846},[841,1765,1766],{"class":850}," test_testclient_requests_serialise",[841,1768,855],{"class":854},[841,1770,1771,1773,1775,1777],{"class":716,"line":858},[841,1772,884],{"class":846},[841,1774,887],{"class":854},[841,1776,890],{"class":846},[841,1778,893],{"class":854},[841,1780,1781,1784,1786],{"class":716,"line":865},[841,1782,1783],{"class":854},"        started ",[841,1785,911],{"class":846},[841,1787,1788],{"class":854}," time.perf_counter()\n",[841,1790,1791,1794,1797,1800,1803,1806,1809],{"class":716,"line":881},[841,1792,1793],{"class":846},"        for",[841,1795,1796],{"class":854}," _ ",[841,1798,1799],{"class":846},"in",[841,1801,1802],{"class":868}," range",[841,1804,1805],{"class":854},"(",[841,1807,1808],{"class":868},"3",[841,1810,1811],{"class":854},"):\n",[841,1813,1814,1816,1818],{"class":716,"line":896},[841,1815,1399],{"class":854},[841,1817,1402],{"class":861},[841,1819,878],{"class":854},[841,1821,1822,1825,1827,1830,1832],{"class":716,"line":923},[841,1823,1824],{"class":854},"        elapsed ",[841,1826,911],{"class":846},[841,1828,1829],{"class":854}," time.perf_counter() ",[841,1831,1344],{"class":846},[841,1833,1834],{"class":854}," started\n",[841,1836,1837,1839,1842,1844],{"class":716,"line":950},[841,1838,926],{"class":846},[841,1840,1841],{"class":854}," elapsed ",[841,1843,1543],{"class":846},[841,1845,1846],{"class":868}," 0.6\n",[841,1848,1849],{"class":716,"line":957},[841,1850,954],{"emptyLinePlaceholder":953},[841,1852,1853],{"class":716,"line":962},[841,1854,954],{"emptyLinePlaceholder":953},[841,1856,1857,1859,1861,1864],{"class":716,"line":976},[841,1858,965],{"class":846},[841,1860,968],{"class":846},[841,1862,1863],{"class":850}," test_asyncclient_requests_overlap",[841,1865,855],{"class":854},[841,1867,1868,1870,1872,1874,1876,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896],{"class":716,"line":982},[841,1869,996],{"class":846},[841,1871,999],{"class":846},[841,1873,1002],{"class":854},[841,1875,1006],{"class":1005},[841,1877,911],{"class":846},[841,1879,1011],{"class":854},[841,1881,1014],{"class":1005},[841,1883,911],{"class":846},[841,1885,1019],{"class":854},[841,1887,1022],{"class":1005},[841,1889,911],{"class":846},[841,1891,1500],{"class":861},[841,1893,1030],{"class":854},[841,1895,890],{"class":846},[841,1897,893],{"class":854},[841,1899,1900,1902,1904],{"class":716,"line":993},[841,1901,1783],{"class":854},[841,1903,911],{"class":846},[841,1905,1788],{"class":854},[841,1907,1908,1911,1914,1917,1920,1922,1924,1927,1929,1931,1933,1935,1937],{"class":716,"line":1037},[841,1909,1910],{"class":846},"        await",[841,1912,1913],{"class":854}," asyncio.gather(",[841,1915,1916],{"class":846},"*",[841,1918,1919],{"class":854},"[client.get(",[841,1921,1402],{"class":861},[841,1923,1030],{"class":854},[841,1925,1926],{"class":846},"for",[841,1928,1796],{"class":854},[841,1930,1799],{"class":846},[841,1932,1802],{"class":868},[841,1934,1805],{"class":854},[841,1936,1808],{"class":868},[841,1938,1939],{"class":854},")])\n",[841,1941,1942,1944,1946,1948,1950],{"class":716,"line":1064},[841,1943,1824],{"class":854},[841,1945,911],{"class":846},[841,1947,1829],{"class":854},[841,1949,1344],{"class":846},[841,1951,1834],{"class":854},[841,1953,1954,1956,1958,1961],{"class":716,"line":1091},[841,1955,926],{"class":846},[841,1957,1841],{"class":854},[841,1959,1960],{"class":846},"\u003C",[841,1962,1963],{"class":868}," 0.4\n",[832,1965,1968],{"className":1966,"code":1967,"language":686,"meta":837},[1120],"$ GET \u002Fpytest\u002Fconcurrency\n200 OK\n\n3 x 200ms via TestClient  = 0.6s\n.3 x 200ms via AsyncClient = 0.2s\n.\n2 passed in 0.00s\n",[603,1969,1967],{"__ignoreMap":837},[590,1971,1972,1973,1975,1976,1979,1980,1983],{},"0.6 s versus 0.2 s — the sum versus the maximum. Note that the ",[603,1974,605],{}," result says nothing about the ",[608,1977,1978],{},"app's"," ability to handle concurrency; the app is identical in both runs. The serialisation happens entirely in the client, because each ",[603,1981,1982],{},"client.get"," blocks until it returns.",[590,1985,1986,1987,1991,1992,1996,1997,1999],{},"This matters more than it first appears, because the timing assertion is the standard way to prove an endpoint is not blocking the loop — the technique used throughout ",[655,1988,1990],{"href":1989},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002F","Concurrent Requests with asyncio.gather"," and ",[655,1993,1995],{"href":1994},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002F","Running Sync Code in a Threadpool",". Written with ",[603,1998,605],{},", that test can never pass, and you may well conclude your app has a blocking bug it does not have.",[770,2001,2003],{"id":2002},"why-async-fixtures-want-asyncclient","Why Async Fixtures Want AsyncClient",[590,2005,2006],{},"Put the pieces together and the fixture rule follows.",[590,2008,2009,2010,2013,2014,2017,2018,2021,2022,2025],{},"An async fixture runs on the test's event loop and typically hands back something ",[608,2011,2012],{},"created"," on that loop: an ",[603,2015,2016],{},"AsyncSession",", an ",[603,2019,2020],{},"httpx.AsyncClient"," pointed at a fake upstream, an ",[603,2023,2024],{},"asyncio.Queue",". Two consequences:",[590,2027,2028,2031],{},[593,2029,2030],{},"Loop-bound resources may not survive the trip."," Some async libraries tolerate being driven from another loop, and some do not — it depends on whether the object registered anything with the loop it was created on. Rather than auditing every dependency for this property, keep the app on the same loop as the fixtures and the question never arises.",[590,2033,2034,2037,2038,2040,2041,2044],{},[593,2035,2036],{},"A blocked loop cannot service the fixture."," This one is not library-dependent at all. If your fixture yields something that needs the loop to make progress during the request — a connection with a keepalive, a fake server implemented as a task, a queue being fed by a producer coroutine — then ",[603,2039,605],{}," deadlocks it, because the loop is frozen for the duration of the call. The zero-ticks measurement above ",[608,2042,2043],{},"is"," this failure in miniature.",[590,2046,2047,2048,2050,2051,2053,2054,2056,2057,650],{},"So the composition rule is simply: async fixtures and ",[603,2049,619],{},", or synchronous fixtures and ",[603,2052,605],{},". Mixing async fixtures with ",[603,2055,605],{}," works in easy cases and fails in exactly the cases you added the fixture for. The database instance of this is worked through in ",[655,2058,2060],{"href":2059},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures\u002F","Testing with Async Database Fixtures",[770,2062,2064],{"id":2063},"verification","Verification",[590,2066,2067],{},"Two habits will catch the mistakes above before they cost you a debugging session.",[590,2069,2070],{},"Assert startup actually ran, once, somewhere in the suite:",[832,2072,2074],{"className":834,"code":2073,"language":836,"meta":837,"style":837},"async def test_startup_state_exists():\n    async with app.router.lifespan_context(app):\n        transport = ASGITransport(app=app)\n        async with AsyncClient(transport=transport, base_url=\"http:\u002F\u002Ft\") as c:\n            assert (await c.get(\"\u002Fhealth\")).status_code == 200\n    # If this fails with AttributeError on app.state, the lifespan was skipped.\n",[603,2075,2076,2087,2095,2112,2140,2162],{"__ignoreMap":837},[841,2077,2078,2080,2082,2085],{"class":716,"line":843},[841,2079,965],{"class":846},[841,2081,968],{"class":846},[841,2083,2084],{"class":850}," test_startup_state_exists",[841,2086,855],{"class":854},[841,2088,2089,2091,2093],{"class":716,"line":858},[841,2090,996],{"class":846},[841,2092,999],{"class":846},[841,2094,1662],{"class":854},[841,2096,2097,2100,2102,2105,2107,2109],{"class":716,"line":865},[841,2098,2099],{"class":854},"        transport ",[841,2101,911],{"class":846},[841,2103,2104],{"class":854}," ASGITransport(",[841,2106,1014],{"class":1005},[841,2108,911],{"class":846},[841,2110,2111],{"class":854},"app)\n",[841,2113,2114,2116,2118,2120,2122,2124,2127,2129,2131,2133,2135,2137],{"class":716,"line":881},[841,2115,1477],{"class":846},[841,2117,999],{"class":846},[841,2119,1002],{"class":854},[841,2121,1006],{"class":1005},[841,2123,911],{"class":846},[841,2125,2126],{"class":854},"transport, ",[841,2128,1022],{"class":1005},[841,2130,911],{"class":846},[841,2132,1500],{"class":861},[841,2134,1030],{"class":854},[841,2136,890],{"class":846},[841,2138,2139],{"class":854}," c:\n",[841,2141,2142,2144,2146,2148,2151,2154,2157,2159],{"class":716,"line":896},[841,2143,1700],{"class":846},[841,2145,1051],{"class":854},[841,2147,1054],{"class":846},[841,2149,2150],{"class":854}," c.get(",[841,2152,2153],{"class":861},"\"\u002Fhealth\"",[841,2155,2156],{"class":854},")).status_code ",[841,2158,1082],{"class":846},[841,2160,2161],{"class":868}," 200\n",[841,2163,2164],{"class":716,"line":923},[841,2165,2166],{"class":1408},"    # If this fails with AttributeError on app.state, the lifespan was skipped.\n",[590,2168,2169,2170,2173,2174,2176],{},"And when a timing assertion behaves oddly, check which client you used before you suspect the app. A quick way to confirm the layout is to have a debug endpoint report its thread — if it says ",[603,2171,2172],{},"asyncio-portal-…",", you are on ",[603,2175,605],{}," and any concurrency expectation is invalid.",[770,2178,2180],{"id":2179},"a-deprecation-you-will-meet-on-the-way","A deprecation you will meet on the way",[590,2182,2183,2184,2186],{},"Importing ",[603,2185,605],{}," on Starlette 1.3.1 with httpx 0.28.1 emits this before your tests produce a\nsingle line of output:",[832,2188,2191],{"className":2189,"code":2190,"language":686,"meta":837},[1120],"StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated;\ninstall `httpx2` instead.\n",[603,2192,2190],{"__ignoreMap":837},[590,2194,2195,2196,2198,2199,2201,2202,2205,2206,2209,2210,2213,2214,2216,2217,2219,2220,2222],{},"It is a warning about the transport underneath ",[603,2197,605],{},", not about ",[603,2200,605],{}," itself, and\nnothing in this guide stops working because of it. Two things follow from it, though. If your suite\nruns with ",[603,2203,2204],{},"-W error"," — a good default — this warning alone will fail collection, so you either move\nto ",[603,2207,2208],{},"httpx2"," or add a targeted ",[603,2211,2212],{},"filterwarnings"," entry rather than silencing the category wholesale.\nAnd it is a reminder of the structural point this page keeps returning to: ",[603,2215,605],{}," is a\ncompatibility layer with its own dependency surface, while ",[603,2218,619],{}," with ",[603,2221,623],{}," is\njust httpx talking to your app. The layer with fewer moving parts is the one with fewer\ndeprecations to absorb.",[770,2224,2226],{"id":2225},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2228,2229,2234,2235,2238,2239,2242,2243,2246,2247,2249,2250,2252],{},[593,2230,2231,2233],{},[603,2232,605],{}," is still the right tool for WebSockets."," It ships ",[603,2236,2237],{},"websocket_connect",", which returns a synchronous session with ",[603,2240,2241],{},"send_json","\u002F",[603,2244,2245],{},"receive_json",". ",[603,2248,623],{}," handles the ",[603,2251,1599],{}," scope only, so testing a WebSocket route through it is not possible.",[590,2254,2255,2260,2261,2263],{},[593,2256,2257,2258,650],{},"A fully synchronous suite does not need ",[603,2259,619],{}," If your app has no async fixtures, no concurrency assertions and no loop-bound resources, ",[603,2262,605],{}," is less ceremony and one fewer plugin. Do not convert a working suite for its own sake.",[590,2265,2266,800,2273,2275,2276,2279],{},[593,2267,2268,2270,2271,650],{},[603,2269,619],{}," needs ",[603,2272,1022],{},[603,2274,623],{}," does not invent a host, so relative URLs fail without one. ",[603,2277,2278],{},"base_url=\"http:\u002F\u002Ftest\""," is the convention; the value is arbitrary but must be present.",[590,2281,2282,2285],{},[593,2283,2284],{},"Neither client tests the protocol layer."," In-process means no real HTTP parsing, no proxy, no TLS, no server timeouts. Keep a small smoke suite against a deployed instance for those.",[590,2287,2288,2291,2292,2294,2295,2297,2298,2300],{},[593,2289,2290],{},"Mixing both in one file is fine."," They are independent, and a suite that uses ",[603,2293,605],{}," for a WebSocket test and ",[603,2296,619],{}," for everything else is perfectly coherent. What is not fine is one async test that reaches for ",[603,2299,605],{}," because it was quicker to type.",[770,2302,2304],{"id":2303},"faq","FAQ",[590,2306,2307,2310,2311,2313],{},[593,2308,2309],{},"What is a blocking portal and why does TestClient need one?","\nA blocking portal is an AnyIO object that runs an event loop on a dedicated thread and lets synchronous code submit coroutines to it and wait for the result. ",[603,2312,605],{}," needs one because your ASGI app is asynchronous while its own API is synchronous, so something has to own a loop on your behalf.",[590,2315,2316,2319],{},[593,2317,2318],{},"Can I call TestClient from inside an async test?","\nIt works, but it freezes the loop your test is running on for the whole request, because the call is a blocking wait on another thread. Any task, timer or fixture depending on that loop makes no progress until the request completes.",[590,2321,2322,2325],{},[593,2323,2324],{},"Do context variables set in my test reach the endpoint under TestClient?","\nYes. AnyIO copies the calling context into the portal, so a contextvar set in the test is visible inside the endpoint even though it runs on a different loop and thread. Objects bound to a specific event loop are the things that do not transfer.",[590,2327,2328,2331,2332,2334,2335,2337],{},[593,2329,2330],{},"Why does my app miss its startup state under AsyncClient?","\nBecause ",[603,2333,623],{}," only implements the ",[603,2336,1599],{}," scope and never sends lifespan messages. Enter the app's lifespan context around the client, or use a lifespan manager, whenever the endpoint depends on something created at startup.",[590,2339,2340,2343,2344,2219,2346,2349],{},[593,2341,2342],{},"Can I make concurrent requests with TestClient?","\nNot from one client in one thread. Each call blocks until it returns, so a loop of requests takes the sum of their durations. Use ",[603,2345,619],{},[603,2347,2348],{},"asyncio.gather"," when a test needs requests to overlap.",[590,2351,2352,2355,2219,2357,2359,2360,2362],{},[593,2353,2354],{},"Which client should be the default in a new project?",[603,2356,619],{},[603,2358,623],{},", because it composes with async fixtures and can express concurrency. Keep ",[603,2361,605],{}," for WebSocket tests, for suites that are entirely synchronous, and for the convenience of its lifespan-running context manager.",[770,2364,2366],{"id":2365},"related","Related",[597,2368,2369,2377,2387,2399,2407],{},[600,2370,2371,800,2374,2376],{},[593,2372,2373],{},"Up to the topic:",[655,2375,363],{"href":657}," puts this choice inside a full strategy.",[600,2378,2379,800,2382,2386],{},[593,2380,2381],{},"Running async tests at all:",[655,2383,2385],{"href":2384},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftesting-async-endpoints-with-pytest-asyncio\u002F","Testing Async Endpoints with pytest-asyncio"," covers modes, markers and loop scopes.",[600,2388,2389,800,2392,2396,2397,650],{},[593,2390,2391],{},"Replacing upstreams:",[655,2393,2395],{"href":2394},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Fmocking-external-services-in-tests\u002F","Mocking External Services in Tests"," pairs naturally with ",[603,2398,619],{},[600,2400,2401,800,2404,2406],{},[593,2402,2403],{},"Async fixtures in practice:",[655,2405,2060],{"href":2059}," is the canonical case for sharing one loop.",[600,2408,2409,800,2412,2414],{},[593,2410,2411],{},"The same failure in the app:",[655,2413,1582],{"href":1581}," — a blocking call freezes a loop wherever it happens.",[2416,2417,2418],"style",{},"html pre.shiki code .sTJeM, html code.shiki .sTJeM{--shiki-default:#A0111F}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 .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}",{"title":837,"searchDepth":858,"depth":858,"links":2420},[2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431],{"id":772,"depth":858,"text":773},{"id":782,"depth":858,"text":783},{"id":1172,"depth":858,"text":1173},{"id":1586,"depth":858,"text":1587},{"id":1747,"depth":858,"text":1748},{"id":2002,"depth":858,"text":2003},{"id":2063,"depth":858,"text":2064},{"id":2179,"depth":858,"text":2180},{"id":2225,"depth":858,"text":2226},{"id":2303,"depth":858,"text":2304},{"id":2365,"depth":858,"text":2366},"2026-07-20","The real difference: TestClient drives your app from a portal thread while AsyncClient runs it on your loop. Measured evidence on lifespan and blocking.","md",[2436,2438,2440,2442,2444,2446],{"q":2309,"a":2437},"A blocking portal is an AnyIO object that runs an event loop on a dedicated thread and lets synchronous code submit coroutines to it and wait for the result. TestClient needs one because your ASGI app is asynchronous while its own API is synchronous, so something has to own a loop on your behalf.",{"q":2318,"a":2439},"It works, but it freezes the loop your test is running on for the whole request, because the call is a blocking wait on another thread. Any task, timer or fixture depending on that loop makes no progress until the request completes.",{"q":2324,"a":2441},"Yes. AnyIO copies the calling context into the portal, so a contextvar set in the test is visible inside the endpoint even though it runs on a different loop and thread. Objects bound to a specific event loop are the things that do not transfer.",{"q":2330,"a":2443},"Because ASGITransport only implements the http scope and never sends lifespan messages. Enter the app's lifespan context around the client, or use a lifespan manager, whenever the endpoint depends on something created at startup.",{"q":2342,"a":2445},"Not from one client in one thread. Each call blocks until it returns, so a loop of requests takes the sum of their durations. Use AsyncClient with asyncio.gather when a test needs requests to overlap.",{"q":2354,"a":2447},"AsyncClient with ASGITransport, because it composes with async fixtures and can express concurrency. Keep TestClient for WebSocket tests, for suites that are entirely synchronous, and for the convenience of its lifespan-running context manager.",null,{"slug":2450,"breadcrumb":2451},"testclient-vs-httpx-asyncclient",[2452,2454,2457,2458],{"label":2453,"path":2242},"Home",{"label":2455,"path":2456},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":363,"path":657},{"label":2459,"path":2460},"TestClient vs httpx AsyncClient","\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002Ftestclient-vs-httpx-asyncclient\u002F",{"title":375,"description":2433},"article","H35CzsAuu9t3O8tzQ-vpt10u6YG7LphFuez1wKlLdAU",[2448,2448],1784588203038]