[{"data":1,"prerenderedAt":2765},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002F":580,"surround-\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002F":2764},[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":219,"body":582,"dateModified":2730,"datePublished":2730,"description":2731,"extension":2732,"faq":2733,"howto":2746,"meta":2747,"navigation":927,"path":220,"seo":2761,"stem":221,"type":2762,"__hash__":2763},"content\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002Findex.md",{"type":583,"value":584,"toc":2719},"minimark",[585,589,596,645,654,797,802,813,822,826,833,856,863,867,878,1687,1690,1697,1700,1749,1752,1756,1765,1917,1923,1958,1962,1968,2098,2104,2107,2110,2227,2235,2239,2242,2245,2397,2400,2442,2456,2460,2474,2480,2560,2571,2586,2590,2605,2620,2626,2639,2645,2663,2667,2715],[586,587,219],"h1",{"id":588},"running-sync-code-in-a-threadpool-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,613,628,635,638],"ul",{},[600,601,602,603,607,608,612],"li",{},"A plain ",[604,605,606],"code",{},"def"," endpoint is ",[609,610,611],"em",{},"already"," dispatched to a threadpool by Starlette. You do not need to wrap anything inside it.",[600,614,615,616,619,620,623,624,627],{},"Inside ",[604,617,618],{},"async def",", use ",[604,621,622],{},"run_in_threadpool"," or ",[604,625,626],{},"anyio.to_thread.run_sync"," — they are the same mechanism and share one pool.",[600,629,630,631,634],{},"The pool is governed by an AnyIO capacity limiter whose default ",[604,632,633],{},"total_tokens"," is 40 per event loop.",[600,636,637],{},"Resize the limiter from inside the loop (a lifespan handler), never at import time.",[600,639,640,641,644],{},"Threads fix ",[609,642,643],{},"responsiveness",", not throughput. CPU-bound work still needs a process pool.",[590,646,647,648,653],{},"This is the mechanical companion to ",[649,650,652],"a",{"href":651},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002F","Async Correctness and Concurrency",". That page explains why a blocked loop stalls a whole worker; this one explains exactly where the sync work goes instead, and shows the wall-clock evidence.",[655,656,657,790],"figure",{},[658,659,667,668,667,672,667,676,667,683,667,692,667,698,667,705,667,710,667,715,667,720,667,724,667,728,667,731,667,735,667,739,667,742,667,746,667,749,667,753,667,757,667,760,667,765,667,768,667,772,667,778,667,782,667,786],"svg",{"viewBox":660,"role":661,"ariaLabelledBy":662,"xmlns":665,"style":666},"0 0 720 300","img",[663,664],"tp-title","tp-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[669,670,671],"title",{"id":663},"How FastAPI dispatches async and sync endpoints",[673,674,675],"desc",{"id":664},"An async def endpoint runs directly on the event loop thread. A plain def endpoint and any run_in_threadpool call both pass through the AnyIO capacity limiter, which allows forty concurrent worker threads by default.",[677,678,682],"text",{"x":679,"y":680,"style":681},"360","28","text-anchor:middle;fill:currentColor;font:600 15px sans-serif","Where the body of an endpoint actually runs",[684,685],"rect",{"x":686,"y":687,"width":688,"height":689,"rx":690,"style":691},"20","50","200","48","8","fill:none;stroke:currentColor;stroke-width:1.4",[677,693,697],{"x":694,"y":695,"style":696},"120","79","text-anchor:middle;fill:currentColor;font:13px sans-serif","async def endpoint",[699,700],"line",{"x1":701,"y1":702,"x2":703,"y2":702,"style":704},"220","74","262","stroke:currentColor;stroke-width:1.4",[706,707],"polygon",{"points":708,"style":709},"262,70 272,74 262,78","fill:currentColor",[684,711],{"x":712,"y":687,"width":713,"height":689,"rx":690,"style":714},"272","198","fill:none;stroke:#00796B;stroke-width:1.8",[677,716,719],{"x":717,"y":695,"style":718},"371","text-anchor:middle;fill:#00796B;font:600 13px sans-serif","stays on the loop",[684,721],{"x":686,"y":722,"width":688,"height":723,"rx":690,"style":691},"122","46",[677,725,727],{"x":694,"y":726,"style":696},"150","plain def endpoint",[684,729],{"x":686,"y":730,"width":688,"height":723,"rx":690,"style":691},"192",[677,732,734],{"x":694,"y":701,"style":733},"text-anchor:middle;fill:currentColor;font:12px monospace","run_in_threadpool()",[699,736],{"x1":701,"y1":737,"x2":703,"y2":738,"style":704},"145","164",[706,740],{"points":741,"style":709},"262,159 272,166 260,168",[699,743],{"x1":701,"y1":744,"x2":703,"y2":745,"style":704},"215","196",[706,747],{"points":748,"style":709},"262,192 272,194 262,202",[684,750],{"x":712,"y":751,"width":713,"height":752,"rx":690,"style":714},"140","80",[677,754,756],{"x":717,"y":755,"style":718},"170","capacity limiter",[677,758,759],{"x":717,"y":730,"style":733},"total_tokens = 40",[699,761],{"x1":762,"y1":763,"x2":764,"y2":763,"style":704},"470","180","512",[706,766],{"points":767,"style":709},"512,176 522,180 512,184",[684,769],{"x":770,"y":751,"width":771,"height":752,"rx":690,"style":691},"522","178",[677,773,777],{"x":774,"y":775,"style":776},"611","166","text-anchor:middle;fill:currentColor;font:12px sans-serif","worker thread 1",[677,779,781],{"x":774,"y":780,"style":776},"186","worker thread 2",[677,783,785],{"x":774,"y":784,"style":776},"206","... up to 40",[677,787,789],{"x":679,"y":788,"style":776},"270","Both sync paths share one pool, so they compete for the same 40 tokens.",[791,792,793,794,796],"figcaption",{},"Only ",[604,795,618],{}," bodies execute on the loop thread. Everything sync funnels through a single, bounded pool.",[798,799,801],"h2",{"id":800},"the-problem-this-solves","The Problem This Solves",[590,803,804,805,808,809,812],{},"You have a function you cannot make async — a vendor SDK that only ships a sync client, a legacy DB-API driver, an image resize, a PDF render. It has to be called from a FastAPI request. The question is not ",[609,806,807],{},"whether"," to run it in a thread but ",[609,810,811],{},"which"," of the three available dispatch routes to use, and what the ceiling on that route is when a hundred requests arrive at once.",[590,814,815,816,818,819,821],{},"Most of the confusion here comes from a single fact that is easy to miss: FastAPI already threads sync endpoints for you. Developers who do not know this either wrap things twice, or convert a perfectly healthy ",[604,817,606],{}," endpoint to ",[604,820,618],{}," for consistency and accidentally move blocking work onto the loop.",[798,823,825],{"id":824},"why-it-happens-starlettes-dispatch-decision","Why It Happens: Starlette's Dispatch Decision",[590,827,828,829,832],{},"When you register a path operation, FastAPI inspects the callable with ",[604,830,831],{},"asyncio.iscoroutinefunction",". If it is a coroutine function, the endpoint is awaited directly on the event loop. If it is not, Starlette wraps it so that the whole body is submitted to a worker thread and awaited from the loop. That decision is made once, at route-registration time, and it applies to the entire function body — not just to the blocking line inside it.",[590,834,835,837,838,841,842,844,845,848,849,852,853,855],{},[604,836,622],{}," is the same machinery exposed as a function. In current Starlette it is a thin wrapper: it binds keyword arguments with ",[604,839,840],{},"functools.partial"," and hands the callable to ",[604,843,626],{},". There is no separate Starlette-owned pool. Both routes end up in AnyIO's worker-thread machinery, and both are gated by the same object: the ",[609,846,847],{},"default thread limiter",", an ",[604,850,851],{},"anyio.CapacityLimiter"," stored per event loop, whose ",[604,854,633],{}," defaults to 40.",[590,857,858,859,862],{},"\"40 threads\" is worth reading precisely. It is not a pre-spawned pool of 40 OS threads; it is a cap on how many ",[604,860,861],{},"run_sync"," calls may be in flight simultaneously. AnyIO creates threads lazily, idles them for a short while, and reuses them. The forty-first concurrent call does not fail — it waits for a token, which is exactly the queuing behaviour that turns \"we offloaded it\" into \"we moved the bottleneck\".",[798,864,866],{"id":865},"the-fix-pick-the-right-route-and-measure-it","The Fix: Pick the Right Route and Measure It",[590,868,869,870,873,874,877],{},"The example below makes the dispatch behaviour observable. Each endpoint does the same 200 ms ",[604,871,872],{},"time.sleep",", and each ",[604,875,876],{},"\u002Fprobe\u002F*"," endpoint fires five concurrent in-process requests at one of them and returns the measured wall clock alongside the number of distinct OS threads that served them.",[879,880,885],"pre",{"className":881,"code":882,"language":883,"meta":884,"style":884},"language-python shiki shiki-themes github-light-high-contrast","\"\"\"Measure how Starlette's threadpool actually runs sync work.\"\"\"\nimport asyncio\nimport threading\nimport time\n\nimport anyio\nimport anyio.to_thread\nimport httpx\nfrom fastapi import FastAPI\nfrom starlette.concurrency import run_in_threadpool\n\napp = FastAPI()\n\nSLEEP = 0.20\n\n\ndef blocking_work(tag: str) -> str:\n    \"\"\"A synchronous call that holds its thread for SLEEP seconds.\"\"\"\n    time.sleep(SLEEP)\n    return f\"{tag}@{threading.current_thread().name}#{threading.get_ident()}\"\n\n\n@app.get(\"\u002Fsync-endpoint\")\ndef sync_endpoint() -> dict[str, str]:\n    \"\"\"A PLAIN def endpoint. Starlette dispatches it to the threadpool for us.\"\"\"\n    return {\"thread\": blocking_work(\"sync-endpoint\")}\n\n\n@app.get(\"\u002Fasync-endpoint-blocking\")\nasync def async_endpoint_blocking() -> dict[str, str]:\n    \"\"\"The bug: sync work called directly from the loop thread.\"\"\"\n    return {\"thread\": blocking_work(\"async-blocking\")}\n\n\n@app.get(\"\u002Fasync-endpoint-offloaded\")\nasync def async_endpoint_offloaded() -> dict[str, str]:\n    \"\"\"The fix: the same sync work handed to the threadpool.\"\"\"\n    return {\"thread\": await run_in_threadpool(blocking_work, \"offloaded\")}\n\n\nasync def fan_out(path: str, n: int) -> dict:\n    \"\"\"Fire n concurrent in-process requests at our own app and time the whole batch.\"\"\"\n    transport = httpx.ASGITransport(app=app)\n    async with httpx.AsyncClient(transport=transport, base_url=\"http:\u002F\u002Fprobe\") as client:\n        started = time.perf_counter()\n        responses = await asyncio.gather(*[client.get(path) for _ in range(n)])\n        elapsed = time.perf_counter() - started\n    threads = {r.json()[\"thread\"].split(\"#\")[1] for r in responses}\n    return {\n        \"path\": path,\n        \"requests\": n,\n        \"elapsed_s\": round(elapsed, 1),\n        \"serial_would_be_s\": round(n * SLEEP, 1),\n        \"distinct_threads\": len(threads),\n    }\n\n\n@app.get(\"\u002Flimiter\")\nasync def limiter() -> dict[str, float]:\n    \"\"\"Report the live capacity of the threadpool this worker will use.\"\"\"\n    lim = anyio.to_thread.current_default_thread_limiter()\n    return {\"total_tokens\": lim.total_tokens, \"borrowed_tokens\": lim.borrowed_tokens}\n","python","",[604,886,887,895,906,914,922,929,937,945,953,967,980,985,997,1002,1015,1020,1025,1048,1054,1065,1109,1114,1119,1133,1154,1160,1180,1185,1190,1202,1224,1230,1246,1251,1256,1268,1288,1294,1317,1322,1327,1355,1361,1381,1418,1429,1464,1481,1519,1527,1536,1545,1564,1588,1602,1608,1613,1618,1630,1651,1657,1668],{"__ignoreMap":884},[888,889,891],"span",{"class":699,"line":890},1,[888,892,894],{"class":893},"sYEJz","\"\"\"Measure how Starlette's threadpool actually runs sync work.\"\"\"\n",[888,896,898,902],{"class":699,"line":897},2,[888,899,901],{"class":900},"sTJeM","import",[888,903,905],{"class":904},"sigWx"," asyncio\n",[888,907,909,911],{"class":699,"line":908},3,[888,910,901],{"class":900},[888,912,913],{"class":904}," threading\n",[888,915,917,919],{"class":699,"line":916},4,[888,918,901],{"class":900},[888,920,921],{"class":904}," time\n",[888,923,925],{"class":699,"line":924},5,[888,926,928],{"emptyLinePlaceholder":927},true,"\n",[888,930,932,934],{"class":699,"line":931},6,[888,933,901],{"class":900},[888,935,936],{"class":904}," anyio\n",[888,938,940,942],{"class":699,"line":939},7,[888,941,901],{"class":900},[888,943,944],{"class":904}," anyio.to_thread\n",[888,946,948,950],{"class":699,"line":947},8,[888,949,901],{"class":900},[888,951,952],{"class":904}," httpx\n",[888,954,956,959,962,964],{"class":699,"line":955},9,[888,957,958],{"class":900},"from",[888,960,961],{"class":904}," fastapi ",[888,963,901],{"class":900},[888,965,966],{"class":904}," FastAPI\n",[888,968,970,972,975,977],{"class":699,"line":969},10,[888,971,958],{"class":900},[888,973,974],{"class":904}," starlette.concurrency ",[888,976,901],{"class":900},[888,978,979],{"class":904}," run_in_threadpool\n",[888,981,983],{"class":699,"line":982},11,[888,984,928],{"emptyLinePlaceholder":927},[888,986,988,991,994],{"class":699,"line":987},12,[888,989,990],{"class":904},"app ",[888,992,993],{"class":900},"=",[888,995,996],{"class":904}," FastAPI()\n",[888,998,1000],{"class":699,"line":999},13,[888,1001,928],{"emptyLinePlaceholder":927},[888,1003,1005,1009,1012],{"class":699,"line":1004},14,[888,1006,1008],{"class":1007},"sacAq","SLEEP",[888,1010,1011],{"class":900}," =",[888,1013,1014],{"class":1007}," 0.20\n",[888,1016,1018],{"class":699,"line":1017},15,[888,1019,928],{"emptyLinePlaceholder":927},[888,1021,1023],{"class":699,"line":1022},16,[888,1024,928],{"emptyLinePlaceholder":927},[888,1026,1028,1030,1034,1037,1040,1043,1045],{"class":699,"line":1027},17,[888,1029,606],{"class":900},[888,1031,1033],{"class":1032},"s3dhs"," blocking_work",[888,1035,1036],{"class":904},"(tag: ",[888,1038,1039],{"class":1007},"str",[888,1041,1042],{"class":904},") -> ",[888,1044,1039],{"class":1007},[888,1046,1047],{"class":904},":\n",[888,1049,1051],{"class":699,"line":1050},18,[888,1052,1053],{"class":893},"    \"\"\"A synchronous call that holds its thread for SLEEP seconds.\"\"\"\n",[888,1055,1057,1060,1062],{"class":699,"line":1056},19,[888,1058,1059],{"class":904},"    time.sleep(",[888,1061,1008],{"class":1007},[888,1063,1064],{"class":904},")\n",[888,1066,1068,1071,1074,1077,1080,1083,1086,1089,1091,1094,1096,1099,1101,1104,1106],{"class":699,"line":1067},20,[888,1069,1070],{"class":900},"    return",[888,1072,1073],{"class":900}," f",[888,1075,1076],{"class":893},"\"",[888,1078,1079],{"class":900},"{",[888,1081,1082],{"class":904},"tag",[888,1084,1085],{"class":900},"}",[888,1087,1088],{"class":893},"@",[888,1090,1079],{"class":900},[888,1092,1093],{"class":904},"threading.current_thread().name",[888,1095,1085],{"class":900},[888,1097,1098],{"class":893},"#",[888,1100,1079],{"class":900},[888,1102,1103],{"class":904},"threading.get_ident()",[888,1105,1085],{"class":900},[888,1107,1108],{"class":893},"\"\n",[888,1110,1112],{"class":699,"line":1111},21,[888,1113,928],{"emptyLinePlaceholder":927},[888,1115,1117],{"class":699,"line":1116},22,[888,1118,928],{"emptyLinePlaceholder":927},[888,1120,1122,1125,1128,1131],{"class":699,"line":1121},23,[888,1123,1124],{"class":1032},"@app.get",[888,1126,1127],{"class":904},"(",[888,1129,1130],{"class":893},"\"\u002Fsync-endpoint\"",[888,1132,1064],{"class":904},[888,1134,1136,1138,1141,1144,1146,1149,1151],{"class":699,"line":1135},24,[888,1137,606],{"class":900},[888,1139,1140],{"class":1032}," sync_endpoint",[888,1142,1143],{"class":904},"() -> dict[",[888,1145,1039],{"class":1007},[888,1147,1148],{"class":904},", ",[888,1150,1039],{"class":1007},[888,1152,1153],{"class":904},"]:\n",[888,1155,1157],{"class":699,"line":1156},25,[888,1158,1159],{"class":893},"    \"\"\"A PLAIN def endpoint. Starlette dispatches it to the threadpool for us.\"\"\"\n",[888,1161,1163,1165,1168,1171,1174,1177],{"class":699,"line":1162},26,[888,1164,1070],{"class":900},[888,1166,1167],{"class":904}," {",[888,1169,1170],{"class":893},"\"thread\"",[888,1172,1173],{"class":904},": blocking_work(",[888,1175,1176],{"class":893},"\"sync-endpoint\"",[888,1178,1179],{"class":904},")}\n",[888,1181,1183],{"class":699,"line":1182},27,[888,1184,928],{"emptyLinePlaceholder":927},[888,1186,1188],{"class":699,"line":1187},28,[888,1189,928],{"emptyLinePlaceholder":927},[888,1191,1193,1195,1197,1200],{"class":699,"line":1192},29,[888,1194,1124],{"class":1032},[888,1196,1127],{"class":904},[888,1198,1199],{"class":893},"\"\u002Fasync-endpoint-blocking\"",[888,1201,1064],{"class":904},[888,1203,1205,1208,1211,1214,1216,1218,1220,1222],{"class":699,"line":1204},30,[888,1206,1207],{"class":900},"async",[888,1209,1210],{"class":900}," def",[888,1212,1213],{"class":1032}," async_endpoint_blocking",[888,1215,1143],{"class":904},[888,1217,1039],{"class":1007},[888,1219,1148],{"class":904},[888,1221,1039],{"class":1007},[888,1223,1153],{"class":904},[888,1225,1227],{"class":699,"line":1226},31,[888,1228,1229],{"class":893},"    \"\"\"The bug: sync work called directly from the loop thread.\"\"\"\n",[888,1231,1233,1235,1237,1239,1241,1244],{"class":699,"line":1232},32,[888,1234,1070],{"class":900},[888,1236,1167],{"class":904},[888,1238,1170],{"class":893},[888,1240,1173],{"class":904},[888,1242,1243],{"class":893},"\"async-blocking\"",[888,1245,1179],{"class":904},[888,1247,1249],{"class":699,"line":1248},33,[888,1250,928],{"emptyLinePlaceholder":927},[888,1252,1254],{"class":699,"line":1253},34,[888,1255,928],{"emptyLinePlaceholder":927},[888,1257,1259,1261,1263,1266],{"class":699,"line":1258},35,[888,1260,1124],{"class":1032},[888,1262,1127],{"class":904},[888,1264,1265],{"class":893},"\"\u002Fasync-endpoint-offloaded\"",[888,1267,1064],{"class":904},[888,1269,1271,1273,1275,1278,1280,1282,1284,1286],{"class":699,"line":1270},36,[888,1272,1207],{"class":900},[888,1274,1210],{"class":900},[888,1276,1277],{"class":1032}," async_endpoint_offloaded",[888,1279,1143],{"class":904},[888,1281,1039],{"class":1007},[888,1283,1148],{"class":904},[888,1285,1039],{"class":1007},[888,1287,1153],{"class":904},[888,1289,1291],{"class":699,"line":1290},37,[888,1292,1293],{"class":893},"    \"\"\"The fix: the same sync work handed to the threadpool.\"\"\"\n",[888,1295,1297,1299,1301,1303,1306,1309,1312,1315],{"class":699,"line":1296},38,[888,1298,1070],{"class":900},[888,1300,1167],{"class":904},[888,1302,1170],{"class":893},[888,1304,1305],{"class":904},": ",[888,1307,1308],{"class":900},"await",[888,1310,1311],{"class":904}," run_in_threadpool(blocking_work, ",[888,1313,1314],{"class":893},"\"offloaded\"",[888,1316,1179],{"class":904},[888,1318,1320],{"class":699,"line":1319},39,[888,1321,928],{"emptyLinePlaceholder":927},[888,1323,1325],{"class":699,"line":1324},40,[888,1326,928],{"emptyLinePlaceholder":927},[888,1328,1330,1332,1334,1337,1340,1342,1345,1348,1350,1353],{"class":699,"line":1329},41,[888,1331,1207],{"class":900},[888,1333,1210],{"class":900},[888,1335,1336],{"class":1032}," fan_out",[888,1338,1339],{"class":904},"(path: ",[888,1341,1039],{"class":1007},[888,1343,1344],{"class":904},", n: ",[888,1346,1347],{"class":1007},"int",[888,1349,1042],{"class":904},[888,1351,1352],{"class":1007},"dict",[888,1354,1047],{"class":904},[888,1356,1358],{"class":699,"line":1357},42,[888,1359,1360],{"class":893},"    \"\"\"Fire n concurrent in-process requests at our own app and time the whole batch.\"\"\"\n",[888,1362,1364,1367,1369,1372,1376,1378],{"class":699,"line":1363},43,[888,1365,1366],{"class":904},"    transport ",[888,1368,993],{"class":900},[888,1370,1371],{"class":904}," httpx.ASGITransport(",[888,1373,1375],{"class":1374},"sV4o_","app",[888,1377,993],{"class":900},[888,1379,1380],{"class":904},"app)\n",[888,1382,1384,1387,1390,1393,1396,1398,1401,1404,1406,1409,1412,1415],{"class":699,"line":1383},44,[888,1385,1386],{"class":900},"    async",[888,1388,1389],{"class":900}," with",[888,1391,1392],{"class":904}," httpx.AsyncClient(",[888,1394,1395],{"class":1374},"transport",[888,1397,993],{"class":900},[888,1399,1400],{"class":904},"transport, ",[888,1402,1403],{"class":1374},"base_url",[888,1405,993],{"class":900},[888,1407,1408],{"class":893},"\"http:\u002F\u002Fprobe\"",[888,1410,1411],{"class":904},") ",[888,1413,1414],{"class":900},"as",[888,1416,1417],{"class":904}," client:\n",[888,1419,1421,1424,1426],{"class":699,"line":1420},45,[888,1422,1423],{"class":904},"        started ",[888,1425,993],{"class":900},[888,1427,1428],{"class":904}," time.perf_counter()\n",[888,1430,1432,1435,1437,1440,1443,1446,1449,1452,1455,1458,1461],{"class":699,"line":1431},46,[888,1433,1434],{"class":904},"        responses ",[888,1436,993],{"class":900},[888,1438,1439],{"class":900}," await",[888,1441,1442],{"class":904}," asyncio.gather(",[888,1444,1445],{"class":900},"*",[888,1447,1448],{"class":904},"[client.get(path) ",[888,1450,1451],{"class":900},"for",[888,1453,1454],{"class":904}," _ ",[888,1456,1457],{"class":900},"in",[888,1459,1460],{"class":1007}," range",[888,1462,1463],{"class":904},"(n)])\n",[888,1465,1467,1470,1472,1475,1478],{"class":699,"line":1466},47,[888,1468,1469],{"class":904},"        elapsed ",[888,1471,993],{"class":900},[888,1473,1474],{"class":904}," time.perf_counter() ",[888,1476,1477],{"class":900},"-",[888,1479,1480],{"class":904}," started\n",[888,1482,1484,1487,1489,1492,1494,1497,1500,1503,1506,1509,1511,1514,1516],{"class":699,"line":1483},48,[888,1485,1486],{"class":904},"    threads ",[888,1488,993],{"class":900},[888,1490,1491],{"class":904}," {r.json()[",[888,1493,1170],{"class":893},[888,1495,1496],{"class":904},"].split(",[888,1498,1499],{"class":893},"\"#\"",[888,1501,1502],{"class":904},")[",[888,1504,1505],{"class":1007},"1",[888,1507,1508],{"class":904},"] ",[888,1510,1451],{"class":900},[888,1512,1513],{"class":904}," r ",[888,1515,1457],{"class":900},[888,1517,1518],{"class":904}," responses}\n",[888,1520,1522,1524],{"class":699,"line":1521},49,[888,1523,1070],{"class":900},[888,1525,1526],{"class":904}," {\n",[888,1528,1530,1533],{"class":699,"line":1529},50,[888,1531,1532],{"class":893},"        \"path\"",[888,1534,1535],{"class":904},": path,\n",[888,1537,1539,1542],{"class":699,"line":1538},51,[888,1540,1541],{"class":893},"        \"requests\"",[888,1543,1544],{"class":904},": n,\n",[888,1546,1548,1551,1553,1556,1559,1561],{"class":699,"line":1547},52,[888,1549,1550],{"class":893},"        \"elapsed_s\"",[888,1552,1305],{"class":904},[888,1554,1555],{"class":1007},"round",[888,1557,1558],{"class":904},"(elapsed, ",[888,1560,1505],{"class":1007},[888,1562,1563],{"class":904},"),\n",[888,1565,1567,1570,1572,1574,1577,1579,1582,1584,1586],{"class":699,"line":1566},53,[888,1568,1569],{"class":893},"        \"serial_would_be_s\"",[888,1571,1305],{"class":904},[888,1573,1555],{"class":1007},[888,1575,1576],{"class":904},"(n ",[888,1578,1445],{"class":900},[888,1580,1581],{"class":1007}," SLEEP",[888,1583,1148],{"class":904},[888,1585,1505],{"class":1007},[888,1587,1563],{"class":904},[888,1589,1591,1594,1596,1599],{"class":699,"line":1590},54,[888,1592,1593],{"class":893},"        \"distinct_threads\"",[888,1595,1305],{"class":904},[888,1597,1598],{"class":1007},"len",[888,1600,1601],{"class":904},"(threads),\n",[888,1603,1605],{"class":699,"line":1604},55,[888,1606,1607],{"class":904},"    }\n",[888,1609,1611],{"class":699,"line":1610},56,[888,1612,928],{"emptyLinePlaceholder":927},[888,1614,1616],{"class":699,"line":1615},57,[888,1617,928],{"emptyLinePlaceholder":927},[888,1619,1621,1623,1625,1628],{"class":699,"line":1620},58,[888,1622,1124],{"class":1032},[888,1624,1127],{"class":904},[888,1626,1627],{"class":893},"\"\u002Flimiter\"",[888,1629,1064],{"class":904},[888,1631,1633,1635,1637,1640,1642,1644,1646,1649],{"class":699,"line":1632},59,[888,1634,1207],{"class":900},[888,1636,1210],{"class":900},[888,1638,1639],{"class":1032}," limiter",[888,1641,1143],{"class":904},[888,1643,1039],{"class":1007},[888,1645,1148],{"class":904},[888,1647,1648],{"class":1007},"float",[888,1650,1153],{"class":904},[888,1652,1654],{"class":699,"line":1653},60,[888,1655,1656],{"class":893},"    \"\"\"Report the live capacity of the threadpool this worker will use.\"\"\"\n",[888,1658,1660,1663,1665],{"class":699,"line":1659},61,[888,1661,1662],{"class":904},"    lim ",[888,1664,993],{"class":900},[888,1666,1667],{"class":904}," anyio.to_thread.current_default_thread_limiter()\n",[888,1669,1671,1673,1675,1678,1681,1684],{"class":699,"line":1670},62,[888,1672,1070],{"class":900},[888,1674,1167],{"class":904},[888,1676,1677],{"class":893},"\"total_tokens\"",[888,1679,1680],{"class":904},": lim.total_tokens, ",[888,1682,1683],{"class":893},"\"borrowed_tokens\"",[888,1685,1686],{"class":904},": lim.borrowed_tokens}\n",[590,1688,1689],{},"Running it under the verification harness produces this — every number below is a real measurement taken by the process, not an estimate:",[879,1691,1695],{"className":1692,"code":1694,"language":677,"meta":884},[1693],"language-text","$ GET \u002Flimiter\n200 OK\n{\n  \"total_tokens\": 40.0,\n  \"borrowed_tokens\": 0.0\n}\n\n$ GET \u002Fprobe\u002Fsync-endpoint\n200 OK\n{\n  \"path\": \"\u002Fsync-endpoint\",\n  \"requests\": 5,\n  \"elapsed_s\": 0.2,\n  \"serial_would_be_s\": 1.0,\n  \"distinct_threads\": 5\n}\n\n$ GET \u002Fprobe\u002Fasync-blocking\n200 OK\n{\n  \"path\": \"\u002Fasync-endpoint-blocking\",\n  \"requests\": 5,\n  \"elapsed_s\": 1.0,\n  \"serial_would_be_s\": 1.0,\n  \"distinct_threads\": 1\n}\n\n$ GET \u002Fprobe\u002Foffloaded\n200 OK\n{\n  \"path\": \"\u002Fasync-endpoint-offloaded\",\n  \"requests\": 5,\n  \"elapsed_s\": 0.2,\n  \"serial_would_be_s\": 1.0,\n  \"distinct_threads\": 5\n}\n",[604,1696,1694],{"__ignoreMap":884},[590,1698,1699],{},"Read the three probes together, because the contrast is the whole lesson:",[597,1701,1702,1720,1738],{},[600,1703,1704,1709,1710,1712,1713,1716,1717,1719],{},[593,1705,1706],{},[604,1707,1708],{},"\u002Fsync-endpoint"," — a plain ",[604,1711,606],{}," with no offloading code anywhere in it. Five concurrent requests finished in 0.2 s and were served by ",[593,1714,1715],{},"five distinct threads",". Nobody wrote ",[604,1718,622],{},"; Starlette did the dispatch.",[600,1721,1722,1727,1728,1730,1731,1733,1734,1737],{},[593,1723,1724],{},[604,1725,1726],{},"\u002Fasync-endpoint-blocking"," — the identical ",[604,1729,872],{},", but inside ",[604,1732,618],{},". Five requests took 1.0 s, exactly the serial total, on ",[593,1735,1736],{},"one thread",". That single thread is the event loop, and while it slept nothing else on the worker could run.",[600,1739,1740,1745,1746,1748],{},[593,1741,1742],{},[604,1743,1744],{},"\u002Fasync-endpoint-offloaded"," — the same ",[604,1747,618],{}," with the call wrapped. Back to 0.2 s across five threads.",[590,1750,1751],{},"The middle case is the one that reaches production. It is not slow because threads are fast; it is slow because there is only one loop thread and it was asleep.",[798,1753,1755],{"id":1754},"the-two-helpers-are-one-helper","The Two Helpers Are One Helper",[590,1757,1758,1761,1762,1764],{},[604,1759,1760],{},"starlette.concurrency.run_in_threadpool"," and ",[604,1763,626],{}," are frequently discussed as if they were alternatives with different characteristics. They are not.",[879,1766,1768],{"className":881,"code":1767,"language":883,"meta":884,"style":884},"@app.get(\"\u002Fto-thread-vs-run-in-threadpool\")\nasync def compare_helpers() -> dict[str, str]:\n    \"\"\"Both helpers land on the same anyio worker-thread pool.\"\"\"\n    a = await anyio.to_thread.run_sync(blocking_work, \"anyio.to_thread\")\n    b = await run_in_threadpool(blocking_work, \"run_in_threadpool\")\n    return {\n        \"anyio\": a.split(\"#\")[0],\n        \"starlette\": b.split(\"#\")[0],\n        \"same_thread_reused\": str(a.split(\"#\")[1] == b.split(\"#\")[1]),\n    }\n",[604,1769,1770,1781,1800,1805,1822,1838,1844,1862,1878,1913],{"__ignoreMap":884},[888,1771,1772,1774,1776,1779],{"class":699,"line":890},[888,1773,1124],{"class":1032},[888,1775,1127],{"class":904},[888,1777,1778],{"class":893},"\"\u002Fto-thread-vs-run-in-threadpool\"",[888,1780,1064],{"class":904},[888,1782,1783,1785,1787,1790,1792,1794,1796,1798],{"class":699,"line":897},[888,1784,1207],{"class":900},[888,1786,1210],{"class":900},[888,1788,1789],{"class":1032}," compare_helpers",[888,1791,1143],{"class":904},[888,1793,1039],{"class":1007},[888,1795,1148],{"class":904},[888,1797,1039],{"class":1007},[888,1799,1153],{"class":904},[888,1801,1802],{"class":699,"line":908},[888,1803,1804],{"class":893},"    \"\"\"Both helpers land on the same anyio worker-thread pool.\"\"\"\n",[888,1806,1807,1810,1812,1814,1817,1820],{"class":699,"line":916},[888,1808,1809],{"class":904},"    a ",[888,1811,993],{"class":900},[888,1813,1439],{"class":900},[888,1815,1816],{"class":904}," anyio.to_thread.run_sync(blocking_work, ",[888,1818,1819],{"class":893},"\"anyio.to_thread\"",[888,1821,1064],{"class":904},[888,1823,1824,1827,1829,1831,1833,1836],{"class":699,"line":924},[888,1825,1826],{"class":904},"    b ",[888,1828,993],{"class":900},[888,1830,1439],{"class":900},[888,1832,1311],{"class":904},[888,1834,1835],{"class":893},"\"run_in_threadpool\"",[888,1837,1064],{"class":904},[888,1839,1840,1842],{"class":699,"line":931},[888,1841,1070],{"class":900},[888,1843,1526],{"class":904},[888,1845,1846,1849,1852,1854,1856,1859],{"class":699,"line":939},[888,1847,1848],{"class":893},"        \"anyio\"",[888,1850,1851],{"class":904},": a.split(",[888,1853,1499],{"class":893},[888,1855,1502],{"class":904},[888,1857,1858],{"class":1007},"0",[888,1860,1861],{"class":904},"],\n",[888,1863,1864,1867,1870,1872,1874,1876],{"class":699,"line":947},[888,1865,1866],{"class":893},"        \"starlette\"",[888,1868,1869],{"class":904},": b.split(",[888,1871,1499],{"class":893},[888,1873,1502],{"class":904},[888,1875,1858],{"class":1007},[888,1877,1861],{"class":904},[888,1879,1880,1883,1885,1887,1890,1892,1894,1896,1898,1901,1904,1906,1908,1910],{"class":699,"line":955},[888,1881,1882],{"class":893},"        \"same_thread_reused\"",[888,1884,1305],{"class":904},[888,1886,1039],{"class":1007},[888,1888,1889],{"class":904},"(a.split(",[888,1891,1499],{"class":893},[888,1893,1502],{"class":904},[888,1895,1505],{"class":1007},[888,1897,1508],{"class":904},[888,1899,1900],{"class":900},"==",[888,1902,1903],{"class":904}," b.split(",[888,1905,1499],{"class":893},[888,1907,1502],{"class":904},[888,1909,1505],{"class":1007},[888,1911,1912],{"class":904},"]),\n",[888,1914,1915],{"class":699,"line":969},[888,1916,1607],{"class":904},[879,1918,1921],{"className":1919,"code":1920,"language":677,"meta":884},[1693],"$ GET \u002Fto-thread-vs-run-in-threadpool\n200 OK\n{\n  \"anyio\": \"anyio.to_thread@AnyIO worker thread\",\n  \"starlette\": \"run_in_threadpool@AnyIO worker thread\",\n  \"same_thread_reused\": \"True\"\n}\n",[604,1922,1920],{"__ignoreMap":884},[590,1924,1925,1926,1929,1930,1933,1934,1937,1938,1940,1941,1944,1945,1761,1948,1951,1952,1954,1955,1957],{},"Both calls landed on a thread named ",[604,1927,1928],{},"AnyIO worker thread",", and ",[604,1931,1932],{},"same_thread_reused"," is ",[604,1935,1936],{},"True"," because the second call reused the thread the first one had just released. The practical differences are ergonomic: ",[604,1939,622],{}," accepts keyword arguments, while ",[604,1942,1943],{},"to_thread.run_sync"," takes positional arguments only and exposes ",[604,1946,1947],{},"cancellable",[604,1949,1950],{},"limiter"," parameters. Use ",[604,1953,622],{}," in Starlette-flavoured code; reach for ",[604,1956,626],{}," when you want to pass your own limiter.",[798,1959,1961],{"id":1960},"resizing-the-limiter-and-proving-it-binds","Resizing the Limiter — and Proving It Binds",[590,1963,1964,1965,1967],{},"Because every sync path shares one limiter, its size is a global property of the worker. Raising it lets more blocking calls overlap; lowering it protects a fragile downstream or caps memory. The following endpoint shrinks the limiter to two tokens, fans six requests at the ",[604,1966,606],{}," endpoint, and restores it:",[879,1969,1971],{"className":881,"code":1970,"language":883,"meta":884,"style":884},"@app.get(\"\u002Fprobe\u002Flimiter-shrunk\")\nasync def probe_limiter_shrunk() -> dict:\n    \"\"\"Shrink the limiter to 2 threads, then fan out 6 requests at the def endpoint.\"\"\"\n    lim = anyio.to_thread.current_default_thread_limiter()\n    original = lim.total_tokens\n    lim.total_tokens = 2\n    try:\n        result = await fan_out(\"\u002Fsync-endpoint\", 6)\n    finally:\n        lim.total_tokens = original\n    result[\"total_tokens\"] = 2\n    return result\n",[604,1972,1973,1984,2000,2005,2013,2023,2033,2040,2061,2068,2078,2091],{"__ignoreMap":884},[888,1974,1975,1977,1979,1982],{"class":699,"line":890},[888,1976,1124],{"class":1032},[888,1978,1127],{"class":904},[888,1980,1981],{"class":893},"\"\u002Fprobe\u002Flimiter-shrunk\"",[888,1983,1064],{"class":904},[888,1985,1986,1988,1990,1993,1996,1998],{"class":699,"line":897},[888,1987,1207],{"class":900},[888,1989,1210],{"class":900},[888,1991,1992],{"class":1032}," probe_limiter_shrunk",[888,1994,1995],{"class":904},"() -> ",[888,1997,1352],{"class":1007},[888,1999,1047],{"class":904},[888,2001,2002],{"class":699,"line":908},[888,2003,2004],{"class":893},"    \"\"\"Shrink the limiter to 2 threads, then fan out 6 requests at the def endpoint.\"\"\"\n",[888,2006,2007,2009,2011],{"class":699,"line":916},[888,2008,1662],{"class":904},[888,2010,993],{"class":900},[888,2012,1667],{"class":904},[888,2014,2015,2018,2020],{"class":699,"line":924},[888,2016,2017],{"class":904},"    original ",[888,2019,993],{"class":900},[888,2021,2022],{"class":904}," lim.total_tokens\n",[888,2024,2025,2028,2030],{"class":699,"line":931},[888,2026,2027],{"class":904},"    lim.total_tokens ",[888,2029,993],{"class":900},[888,2031,2032],{"class":1007}," 2\n",[888,2034,2035,2038],{"class":699,"line":939},[888,2036,2037],{"class":900},"    try",[888,2039,1047],{"class":904},[888,2041,2042,2045,2047,2049,2052,2054,2056,2059],{"class":699,"line":947},[888,2043,2044],{"class":904},"        result ",[888,2046,993],{"class":900},[888,2048,1439],{"class":900},[888,2050,2051],{"class":904}," fan_out(",[888,2053,1130],{"class":893},[888,2055,1148],{"class":904},[888,2057,2058],{"class":1007},"6",[888,2060,1064],{"class":904},[888,2062,2063,2066],{"class":699,"line":955},[888,2064,2065],{"class":900},"    finally",[888,2067,1047],{"class":904},[888,2069,2070,2073,2075],{"class":699,"line":969},[888,2071,2072],{"class":904},"        lim.total_tokens ",[888,2074,993],{"class":900},[888,2076,2077],{"class":904}," original\n",[888,2079,2080,2083,2085,2087,2089],{"class":699,"line":982},[888,2081,2082],{"class":904},"    result[",[888,2084,1677],{"class":893},[888,2086,1508],{"class":904},[888,2088,993],{"class":900},[888,2090,2032],{"class":1007},[888,2092,2093,2095],{"class":699,"line":987},[888,2094,1070],{"class":900},[888,2096,2097],{"class":904}," result\n",[879,2099,2102],{"className":2100,"code":2101,"language":677,"meta":884},[1693],"$ GET \u002Fprobe\u002Flimiter-shrunk\n200 OK\n{\n  \"path\": \"\u002Fsync-endpoint\",\n  \"requests\": 6,\n  \"elapsed_s\": 0.6,\n  \"serial_would_be_s\": 1.2,\n  \"distinct_threads\": 2,\n  \"total_tokens\": 2\n}\n",[604,2103,2101],{"__ignoreMap":884},[590,2105,2106],{},"Six 200 ms calls through two tokens took 0.6 s and used exactly two threads — three sequential batches of two. This is precisely what threadpool saturation looks like in production, just in miniature: the work is still off the loop, so the app stays responsive, but the sync endpoint's own latency is now three times its service time.",[590,2108,2109],{},"In a real app, set this once during startup rather than per request:",[879,2111,2113],{"className":881,"code":2112,"language":883,"meta":884,"style":884},"from contextlib import asynccontextmanager\n\nimport anyio.to_thread\nfrom fastapi import FastAPI\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    # Runs inside the loop, so the limiter it fetches is the one requests will use.\n    limiter = anyio.to_thread.current_default_thread_limiter()\n    limiter.total_tokens = 80\n    yield\n\n\napp = FastAPI(lifespan=lifespan)\n",[604,2114,2115,2127,2131,2137,2147,2151,2155,2160,2172,2178,2187,2197,2202,2206,2210],{"__ignoreMap":884},[888,2116,2117,2119,2122,2124],{"class":699,"line":890},[888,2118,958],{"class":900},[888,2120,2121],{"class":904}," contextlib ",[888,2123,901],{"class":900},[888,2125,2126],{"class":904}," asynccontextmanager\n",[888,2128,2129],{"class":699,"line":897},[888,2130,928],{"emptyLinePlaceholder":927},[888,2132,2133,2135],{"class":699,"line":908},[888,2134,901],{"class":900},[888,2136,944],{"class":904},[888,2138,2139,2141,2143,2145],{"class":699,"line":916},[888,2140,958],{"class":900},[888,2142,961],{"class":904},[888,2144,901],{"class":900},[888,2146,966],{"class":904},[888,2148,2149],{"class":699,"line":924},[888,2150,928],{"emptyLinePlaceholder":927},[888,2152,2153],{"class":699,"line":931},[888,2154,928],{"emptyLinePlaceholder":927},[888,2156,2157],{"class":699,"line":939},[888,2158,2159],{"class":1032},"@asynccontextmanager\n",[888,2161,2162,2164,2166,2169],{"class":699,"line":947},[888,2163,1207],{"class":900},[888,2165,1210],{"class":900},[888,2167,2168],{"class":1032}," lifespan",[888,2170,2171],{"class":904},"(app: FastAPI):\n",[888,2173,2174],{"class":699,"line":955},[888,2175,2177],{"class":2176},"sFeEa","    # Runs inside the loop, so the limiter it fetches is the one requests will use.\n",[888,2179,2180,2183,2185],{"class":699,"line":969},[888,2181,2182],{"class":904},"    limiter ",[888,2184,993],{"class":900},[888,2186,1667],{"class":904},[888,2188,2189,2192,2194],{"class":699,"line":982},[888,2190,2191],{"class":904},"    limiter.total_tokens ",[888,2193,993],{"class":900},[888,2195,2196],{"class":1007}," 80\n",[888,2198,2199],{"class":699,"line":987},[888,2200,2201],{"class":900},"    yield\n",[888,2203,2204],{"class":699,"line":999},[888,2205,928],{"emptyLinePlaceholder":927},[888,2207,2208],{"class":699,"line":1004},[888,2209,928],{"emptyLinePlaceholder":927},[888,2211,2212,2214,2216,2219,2222,2224],{"class":699,"line":1017},[888,2213,990],{"class":904},[888,2215,993],{"class":900},[888,2217,2218],{"class":904}," FastAPI(",[888,2220,2221],{"class":1374},"lifespan",[888,2223,993],{"class":900},[888,2225,2226],{"class":904},"lifespan)\n",[590,2228,2229,2230,2234],{},"The limiter is stored in an AnyIO run-scoped variable, which means it does not exist until an event loop is running. Setting it at module import time either raises or configures a limiter no request will ever see — a lifespan handler is the correct place, and it composes with the other startup work described in ",[649,2231,2233],{"href":2232},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002F","Async Database Sessions",".",[798,2236,2238],{"id":2237},"verification","Verification",[590,2240,2241],{},"Two checks are worth keeping permanently.",[590,2243,2244],{},"First, assert that concurrency is real rather than assumed, by timing a fan-out:",[879,2246,2248],{"className":881,"code":2247,"language":883,"meta":884,"style":884},"import asyncio\nimport time\n\nimport httpx\n\n\nasync def test_sync_endpoint_is_actually_threaded():\n    transport = httpx.ASGITransport(app=app)\n    async with httpx.AsyncClient(transport=transport, base_url=\"http:\u002F\u002Ft\") as c:\n        started = time.perf_counter()\n        await asyncio.gather(*[c.get(\"\u002Fsync-endpoint\") for _ in range(5)])\n        # Serial would be 5 x 200ms; anything near 1s means the loop was blocked.\n        assert time.perf_counter() - started \u003C 0.5\n",[604,2249,2250,2256,2262,2266,2272,2276,2280,2292,2306,2334,2342,2374,2379],{"__ignoreMap":884},[888,2251,2252,2254],{"class":699,"line":890},[888,2253,901],{"class":900},[888,2255,905],{"class":904},[888,2257,2258,2260],{"class":699,"line":897},[888,2259,901],{"class":900},[888,2261,921],{"class":904},[888,2263,2264],{"class":699,"line":908},[888,2265,928],{"emptyLinePlaceholder":927},[888,2267,2268,2270],{"class":699,"line":916},[888,2269,901],{"class":900},[888,2271,952],{"class":904},[888,2273,2274],{"class":699,"line":924},[888,2275,928],{"emptyLinePlaceholder":927},[888,2277,2278],{"class":699,"line":931},[888,2279,928],{"emptyLinePlaceholder":927},[888,2281,2282,2284,2286,2289],{"class":699,"line":939},[888,2283,1207],{"class":900},[888,2285,1210],{"class":900},[888,2287,2288],{"class":1032}," test_sync_endpoint_is_actually_threaded",[888,2290,2291],{"class":904},"():\n",[888,2293,2294,2296,2298,2300,2302,2304],{"class":699,"line":947},[888,2295,1366],{"class":904},[888,2297,993],{"class":900},[888,2299,1371],{"class":904},[888,2301,1375],{"class":1374},[888,2303,993],{"class":900},[888,2305,1380],{"class":904},[888,2307,2308,2310,2312,2314,2316,2318,2320,2322,2324,2327,2329,2331],{"class":699,"line":955},[888,2309,1386],{"class":900},[888,2311,1389],{"class":900},[888,2313,1392],{"class":904},[888,2315,1395],{"class":1374},[888,2317,993],{"class":900},[888,2319,1400],{"class":904},[888,2321,1403],{"class":1374},[888,2323,993],{"class":900},[888,2325,2326],{"class":893},"\"http:\u002F\u002Ft\"",[888,2328,1411],{"class":904},[888,2330,1414],{"class":900},[888,2332,2333],{"class":904}," c:\n",[888,2335,2336,2338,2340],{"class":699,"line":969},[888,2337,1423],{"class":904},[888,2339,993],{"class":900},[888,2341,1428],{"class":904},[888,2343,2344,2347,2349,2351,2354,2356,2358,2360,2362,2364,2366,2368,2371],{"class":699,"line":982},[888,2345,2346],{"class":900},"        await",[888,2348,1442],{"class":904},[888,2350,1445],{"class":900},[888,2352,2353],{"class":904},"[c.get(",[888,2355,1130],{"class":893},[888,2357,1411],{"class":904},[888,2359,1451],{"class":900},[888,2361,1454],{"class":904},[888,2363,1457],{"class":900},[888,2365,1460],{"class":1007},[888,2367,1127],{"class":904},[888,2369,2370],{"class":1007},"5",[888,2372,2373],{"class":904},")])\n",[888,2375,2376],{"class":699,"line":987},[888,2377,2378],{"class":2176},"        # Serial would be 5 x 200ms; anything near 1s means the loop was blocked.\n",[888,2380,2381,2384,2386,2388,2391,2394],{"class":699,"line":999},[888,2382,2383],{"class":900},"        assert",[888,2385,1474],{"class":904},[888,2387,1477],{"class":900},[888,2389,2390],{"class":904}," started ",[888,2392,2393],{"class":900},"\u003C",[888,2395,2396],{"class":1007}," 0.5\n",[590,2398,2399],{},"Second, assert the limiter is configured as you intended, since a stray import-time tweak is invisible otherwise:",[879,2401,2403],{"className":881,"code":2402,"language":883,"meta":884,"style":884},"import anyio.to_thread\n\n\nasync def test_limiter_is_sized_for_this_service():\n    assert anyio.to_thread.current_default_thread_limiter().total_tokens == 80\n",[604,2404,2405,2411,2415,2419,2430],{"__ignoreMap":884},[888,2406,2407,2409],{"class":699,"line":890},[888,2408,901],{"class":900},[888,2410,944],{"class":904},[888,2412,2413],{"class":699,"line":897},[888,2414,928],{"emptyLinePlaceholder":927},[888,2416,2417],{"class":699,"line":908},[888,2418,928],{"emptyLinePlaceholder":927},[888,2420,2421,2423,2425,2428],{"class":699,"line":916},[888,2422,1207],{"class":900},[888,2424,1210],{"class":900},[888,2426,2427],{"class":1032}," test_limiter_is_sized_for_this_service",[888,2429,2291],{"class":904},[888,2431,2432,2435,2438,2440],{"class":699,"line":924},[888,2433,2434],{"class":900},"    assert",[888,2436,2437],{"class":904}," anyio.to_thread.current_default_thread_limiter().total_tokens ",[888,2439,1900],{"class":900},[888,2441,2196],{"class":1007},[590,2443,2444,2445,2448,2449,2451,2452,2234],{},"In production, the metric to graph is ",[604,2446,2447],{},"borrowed_tokens"," against ",[604,2450,633],{},". When borrowed sits at the ceiling, sync requests are queuing for a thread and your p99 is being set by the limiter rather than by the downstream. Wiring that into your exporter is covered in ",[649,2453,2455],{"href":2454},"\u002Fasync-background-tasks-observability\u002Fobservability-and-tracing\u002F","Observability and Tracing",[798,2457,2459],{"id":2458},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2461,2462,2465,2466,2469,2470,2234],{},[593,2463,2464],{},"Threads do not create CPU parallelism."," The GIL still serialises pure Python bytecode. Offloading a CPU-bound function protects the loop — other requests keep being served — but the computation itself gets no faster, and forty concurrent CPU-bound threads will thrash. Send that work to a ",[604,2467,2468],{},"ProcessPoolExecutor",", as covered in ",[649,2471,2473],{"href":2472},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffixing-blocking-calls-in-async-routes\u002F","Fixing Blocking Calls in Async Routes",[590,2475,2476,2479],{},[593,2477,2478],{},"A shared limiter means one endpoint can starve another."," Because the pool is global to the worker, a slow sync report endpoint holding thirty tokens leaves ten for everything else. If two workloads have genuinely different capacity needs, give the risky one its own limiter and pass it explicitly:",[879,2481,2483],{"className":881,"code":2482,"language":883,"meta":884,"style":884},"import anyio\n\n_reports = anyio.CapacityLimiter(4)\n\n\nasync def render_report(spec: dict) -> bytes:\n    # Reports can never occupy more than 4 threads, whatever the default limiter allows.\n    return await anyio.to_thread.run_sync(_render_sync, spec, limiter=_reports)\n",[604,2484,2485,2491,2495,2510,2514,2518,2539,2544],{"__ignoreMap":884},[888,2486,2487,2489],{"class":699,"line":890},[888,2488,901],{"class":900},[888,2490,936],{"class":904},[888,2492,2493],{"class":699,"line":897},[888,2494,928],{"emptyLinePlaceholder":927},[888,2496,2497,2500,2502,2505,2508],{"class":699,"line":908},[888,2498,2499],{"class":904},"_reports ",[888,2501,993],{"class":900},[888,2503,2504],{"class":904}," anyio.CapacityLimiter(",[888,2506,2507],{"class":1007},"4",[888,2509,1064],{"class":904},[888,2511,2512],{"class":699,"line":916},[888,2513,928],{"emptyLinePlaceholder":927},[888,2515,2516],{"class":699,"line":924},[888,2517,928],{"emptyLinePlaceholder":927},[888,2519,2520,2522,2524,2527,2530,2532,2534,2537],{"class":699,"line":931},[888,2521,1207],{"class":900},[888,2523,1210],{"class":900},[888,2525,2526],{"class":1032}," render_report",[888,2528,2529],{"class":904},"(spec: ",[888,2531,1352],{"class":1007},[888,2533,1042],{"class":904},[888,2535,2536],{"class":1007},"bytes",[888,2538,1047],{"class":904},[888,2540,2541],{"class":699,"line":939},[888,2542,2543],{"class":2176},"    # Reports can never occupy more than 4 threads, whatever the default limiter allows.\n",[888,2545,2546,2548,2550,2553,2555,2557],{"class":699,"line":947},[888,2547,1070],{"class":900},[888,2549,1439],{"class":900},[888,2551,2552],{"class":904}," anyio.to_thread.run_sync(_render_sync, spec, ",[888,2554,1950],{"class":1374},[888,2556,993],{"class":900},[888,2558,2559],{"class":904},"_reports)\n",[590,2561,2562,2568,2569,2234],{},[593,2563,2564,2565,2567],{},"Raising ",[604,2566,633],{}," is not free."," Each concurrent call is a real OS thread with a real stack, and if the sync work holds a database connection, thread count effectively becomes connection count. Raise the limiter and the pool together, or you will simply relocate the queue to the database, which is the failure described in ",[649,2570,2233],{"href":2232},[590,2572,2573,2576,2577,2580,2581,2585],{},[593,2574,2575],{},"Sometimes the right answer is not a thread at all."," If the blocking work is slow ",[609,2578,2579],{},"and"," the caller does not need the result, it belongs in ",[649,2582,2584],{"href":2583},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002F","Background Task Processing"," rather than in a request-scoped thread.",[798,2587,2589],{"id":2588},"faq","FAQ",[590,2591,2592,2595,2596,2598,2599,2601,2602,2604],{},[593,2593,2594],{},"Do I need run_in_threadpool inside a plain def endpoint?","\nNo. Starlette already runs the entire body of a ",[604,2597,606],{}," endpoint in the threadpool, so wrapping calls inside it adds a second hop for no benefit. Only reach for ",[604,2600,622],{}," when you are inside an ",[604,2603,618],{}," and need to call something blocking.",[590,2606,2607,2610,2611,2613,2614,2616,2617,2619],{},[593,2608,2609],{},"What is the difference between run_in_threadpool and anyio.to_thread.run_sync?","\nThere is almost none. Starlette's ",[604,2612,622],{}," is a thin wrapper that forwards to ",[604,2615,626],{}," after binding keyword arguments with ",[604,2618,840],{},". Both draw from the same pool of AnyIO worker threads and both respect the same capacity limiter.",[590,2621,2622,2625],{},[593,2623,2624],{},"How many threads does FastAPI use for sync code?","\nAnyIO's default capacity limiter allows 40 concurrent worker threads per event loop. That number is a limit on concurrency rather than a fixed pool size, and threads are created on demand and reused.",[590,2627,2628,2631,2632,2634,2635,2638],{},[593,2629,2630],{},"How do I change the threadpool size?","\nSet ",[604,2633,633],{}," on the limiter returned by ",[604,2636,2637],{},"anyio.to_thread.current_default_thread_limiter",", from inside the running event loop. The limiter is scoped to the loop, so do it in a lifespan handler rather than at import time.",[590,2640,2641,2644],{},[593,2642,2643],{},"Does moving CPU-bound work to a thread help?","\nIt keeps the event loop responsive, which protects other requests, but it does not make the CPU work itself faster because the GIL still serialises pure Python bytecode. For genuine CPU parallelism use a process pool.",[590,2646,2647,2650,2651,2653,2654,2656,2657,2659,2660,2662],{},[593,2648,2649],{},"Why did my endpoint get slower after I made it async def?","\nBecause a ",[604,2652,606],{}," endpoint runs in the threadpool where blocking is harmless, while an ",[604,2655,618],{}," endpoint runs directly on the event loop where the same blocking call freezes every other request on that worker. Changing ",[604,2658,606],{}," to ",[604,2661,618],{}," without also making the I\u002FO async converts safe blocking into loop-blocking.",[798,2664,2666],{"id":2665},"related","Related",[597,2668,2669,2678,2688,2696,2706],{},[600,2670,2671,2674,2675,2677],{},[593,2672,2673],{},"Up to the topic:"," ",[649,2676,652],{"href":651}," frames why the loop must stay free.",[600,2679,2680,2674,2683,2687],{},[593,2681,2682],{},"The decision itself:",[649,2684,2686],{"href":2685},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Ffastapi-async-def-vs-def-performance\u002F","FastAPI async def vs def Performance"," covers choosing between the two dispatch routes per endpoint.",[600,2689,2690,2674,2693,2695],{},[593,2691,2692],{},"When it has already gone wrong:",[649,2694,2473],{"href":2472}," is the diagnostic path from symptom to offending line.",[600,2697,2698,2674,2701,2705],{},[593,2699,2700],{},"The other side of concurrency:",[649,2702,2704],{"href":2703},"\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Fconcurrent-requests-with-asyncio-gather\u002F","Concurrent Requests with asyncio.gather"," shows how to overlap async work once the loop is free.",[600,2707,2708,2674,2711,2714],{},[593,2709,2710],{},"Proving it in CI:",[649,2712,363],{"href":2713},"\u002Fasync-background-tasks-observability\u002Ftesting-fastapi-applications\u002F"," explains how to run the timing assertions above inside a real suite.",[2716,2717,2718],"style",{},"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 .sigWx, html code.shiki .sigWx{--shiki-default:#0E1116}html pre.shiki code .sacAq, html code.shiki .sacAq{--shiki-default:#023B95}html pre.shiki code .s3dhs, html code.shiki .s3dhs{--shiki-default:#622CBC}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}",{"title":884,"searchDepth":897,"depth":897,"links":2720},[2721,2722,2723,2724,2725,2726,2727,2728,2729],{"id":800,"depth":897,"text":801},{"id":824,"depth":897,"text":825},{"id":865,"depth":897,"text":866},{"id":1754,"depth":897,"text":1755},{"id":1960,"depth":897,"text":1961},{"id":2237,"depth":897,"text":2238},{"id":2458,"depth":897,"text":2459},{"id":2588,"depth":897,"text":2589},{"id":2665,"depth":897,"text":2666},"2026-07-20","How Starlette dispatches sync work: run_in_threadpool, anyio.to_thread.run_sync, why a plain def endpoint is already threaded, and resizing the limiter.","md",[2734,2736,2738,2740,2742,2744],{"q":2594,"a":2735},"No. Starlette already runs the entire body of a def endpoint in the threadpool, so wrapping calls inside it adds a second hop for no benefit. Only reach for run_in_threadpool when you are inside an async def and need to call something blocking.",{"q":2609,"a":2737},"There is almost none. Starlette's run_in_threadpool is a thin wrapper that forwards to anyio.to_thread.run_sync after binding keyword arguments with functools.partial. Both draw from the same pool of AnyIO worker threads and both respect the same capacity limiter.",{"q":2624,"a":2739},"AnyIO's default capacity limiter allows 40 concurrent worker threads per event loop. That number is a limit on concurrency rather than a fixed pool size, and threads are created on demand and reused.",{"q":2630,"a":2741},"Set total_tokens on the limiter returned by anyio.to_thread.current_default_thread_limiter, from inside the running event loop. The limiter is scoped to the loop, so do it in a lifespan handler rather than at import time.",{"q":2643,"a":2743},"It keeps the event loop responsive, which protects other requests, but it does not make the CPU work itself faster because the GIL still serialises pure Python bytecode. For genuine CPU parallelism use a process pool.",{"q":2649,"a":2745},"Because a def endpoint runs in the threadpool where blocking is harmless, while an async def endpoint runs directly on the event loop where the same blocking call freezes every other request on that worker. Changing def to async def without also making the I\u002FO async converts safe blocking into loop-blocking.",null,{"slug":2748,"breadcrumb":2749},"running-sync-code-in-a-threadpool",[2750,2753,2756,2758],{"label":2751,"path":2752},"Home","\u002F",{"label":2754,"path":2755},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":2757,"path":651},"Async Correctness & Concurrency",{"label":2759,"path":2760},"Running Sync Code in a Threadpool","\u002Fasync-background-tasks-observability\u002Fasync-correctness-concurrency\u002Frunning-sync-code-in-a-threadpool\u002F",{"title":219,"description":2731},"article","MFo9OtuwDxg0Ut2SSdYbYRxwdANQb_aCeYO4OetL_Fk",[2746,2746],1784588203038]