[{"data":1,"prerenderedAt":2491},["ShallowReactive",2],{"nav":3,"page-\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002F":580,"surround-\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002F":2490},[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":249,"body":582,"dateModified":2458,"datePublished":2458,"description":2459,"extension":2460,"faq":2461,"howto":2474,"meta":2475,"navigation":1019,"path":250,"seo":2487,"stem":251,"type":2488,"__hash__":2489},"content\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002Findex.md",{"type":583,"value":584,"toc":2446},"minimark",[585,589,596,646,655,775,780,783,786,793,820,823,937,954,976,980,991,1482,1489,1496,1519,1522,1526,1529,1681,1687,1701,1707,1711,1714,1721,1886,1892,1899,1909,2056,2062,2074,2078,2081,2187,2190,2234,2241,2245,2259,2274,2280,2286,2296,2300,2312,2328,2349,2366,2379,2388,2392,2442],[586,587,249],"h1",{"id":588},"transaction-management-and-rollback-in-fastapi",[590,591,592],"p",{},[593,594,595],"strong",{},"Key takeaways:",[597,598,599,608,623,633,639],"ul",{},[600,601,602,603,607],"li",{},"The transaction boundary belongs to the ",[604,605,606],"code",{},"yield"," dependency, not to the endpoint body.",[600,609,610,611,614,615,618,619,622],{},"Commit in an ",[604,612,613],{},"else"," clause and roll back in an ",[604,616,617],{},"except"," clause, so a raised ",[604,620,621],{},"HTTPException"," cannot commit half a write.",[600,624,625,628,629,632],{},[604,626,627],{},"flush()"," sends SQL and populates primary keys; only ",[604,630,631],{},"commit()"," makes it durable.",[600,634,635,636,638],{},"A ",[604,637,631],{}," inside the endpoint converts every later failure into a partial write — shown below with real numbers.",[600,640,641,642,645],{},"Use ",[604,643,644],{},"begin_nested()"," (a SAVEPOINT) when part of the work must survive a failure of the rest.",[590,647,648,649,654],{},"This page continues ",[650,651,653],"a",{"href":652},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002F","Async Database Sessions",", which covers where the engine and session factory live. Here we are concerned with one narrower question: which lines of code are allowed to end a transaction.",[656,657,658,768],"figure",{},[659,660,668,669,668,673,668,677,668,684,668,691,668,697,668,700,668,704,668,708,668,713,668,717,668,722,668,727,668,733,668,738,668,741,668,744,668,746,668,749,668,751,668,753,668,755,668,759,668,763],"svg",{"viewBox":661,"role":662,"ariaLabelledBy":663,"xmlns":666,"style":667},"0 0 720 300","img",[664,665],"tx-title","tx-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;max-width:720px;margin:2rem 0","\n  ",[670,671,672],"title",{"id":664},"Transaction boundary owned by the dependency versus the endpoint",[674,675,676],"desc",{"id":665},"When the yield dependency owns the boundary, a failure anywhere in the endpoint rolls the whole unit back. When the endpoint commits mid-way, the first write is already durable and the failure leaves the database inconsistent.",[678,679,683],"text",{"x":680,"y":681,"style":682},"20","30","text-anchor:start;fill:#00796B;font:600 14px sans-serif","Dependency owns the boundary",[685,686],"rect",{"x":680,"y":687,"width":688,"height":687,"rx":689,"style":690},"42","130","6","fill:none;stroke:#00796B;stroke-width:1.8",[678,692,696],{"x":693,"y":694,"style":695},"85","68","text-anchor:middle;fill:#00796B;font:12px sans-serif","debit alice",[685,698],{"x":699,"y":687,"width":688,"height":687,"rx":689,"style":690},"162",[678,701,703],{"x":702,"y":694,"style":695},"227","credit bob",[685,705],{"x":706,"y":687,"width":688,"height":687,"rx":689,"style":707},"304","fill:none;stroke:currentColor;stroke-width:1.4",[678,709,712],{"x":710,"y":694,"style":711},"369","text-anchor:middle;fill:currentColor;font:12px sans-serif","check fails",[685,714],{"x":715,"y":687,"width":716,"height":687,"rx":689,"style":690},"446","150",[678,718,721],{"x":719,"y":694,"style":720},"521","text-anchor:middle;fill:#00796B;font:600 12px sans-serif","rollback: all",[678,723,726],{"x":680,"y":724,"style":725},"112","text-anchor:start;fill:currentColor;font:12px monospace","alice 100  bob 0  (unchanged)",[728,729],"line",{"x1":680,"y1":730,"x2":731,"y2":730,"style":732},"134","700","stroke:currentColor;stroke-width:1;stroke-dasharray:4 4",[678,734,737],{"x":680,"y":735,"style":736},"168","text-anchor:start;fill:currentColor;font:600 14px sans-serif","Endpoint commits mid-way",[685,739],{"x":680,"y":740,"width":688,"height":687,"rx":689,"style":707},"180",[678,742,696],{"x":693,"y":743,"style":711},"206",[685,745],{"x":699,"y":740,"width":688,"height":687,"rx":689,"style":707},[678,747,631],{"x":702,"y":743,"style":748},"text-anchor:middle;fill:currentColor;font:600 12px monospace",[685,750],{"x":706,"y":740,"width":688,"height":687,"rx":689,"style":707},[678,752,712],{"x":710,"y":743,"style":711},[685,754],{"x":715,"y":740,"width":716,"height":687,"rx":689,"style":707},[678,756,758],{"x":719,"y":743,"style":757},"text-anchor:middle;fill:currentColor;font:600 12px sans-serif","nothing to undo",[678,760,762],{"x":680,"y":761,"style":725},"250","alice -50  bob 0  (money destroyed)",[678,764,767],{"x":680,"y":765,"style":766},"282","text-anchor:start;fill:currentColor;font:12px sans-serif","Both rows below are measured output from the same seeded database.",[769,770,771,772,774],"figcaption",{},"The only difference between the two rows is where ",[604,773,631],{}," is written.",[776,777,779],"h2",{"id":778},"the-problem-this-solves","The Problem This Solves",[590,781,782],{},"A handler makes two or three related writes. Something between them fails — a business rule rejects the operation, a constraint fires, an upstream call times out. What is left in the database afterwards depends entirely on where the commit was written, and in most codebases that answer varies from handler to handler because each one was written by a different person on a different day.",[590,784,785],{},"The goal is a rule with no exceptions: one request, one transaction, and exactly one piece of code allowed to end it.",[776,787,789,790,792],{"id":788},"why-it-happens-yield-dependencies-wrap-the-path-operation","Why It Happens: ",[604,791,606],{}," Dependencies Wrap the Path Operation",[590,794,795,796,798,799,803,804,806,807,809,810,813,814,816,817,819],{},"FastAPI's dependencies with ",[604,797,606],{}," are not just a setup\u002Fteardown convenience — they are a context that ",[800,801,802],"em",{},"encloses"," the path operation. The code before ",[604,805,606],{}," runs first, the endpoint body runs, then the code after ",[604,808,606],{}," runs. Crucially, when the endpoint raises, the exception propagates ",[800,811,812],{},"through"," the generator, so an ",[604,815,617],{}," clause around the ",[604,818,606],{}," sees it.",[590,821,822],{},"That is precisely the shape of a transaction:",[824,825,830],"pre",{"className":826,"code":827,"language":828,"meta":829,"style":829},"language-python shiki shiki-themes github-light-high-contrast","async def get_session() -> AsyncIterator[AsyncSession]:\n    \"\"\"The transaction boundary. The endpoint never commits; this dependency does, once.\"\"\"\n    async with Session() as session:\n        try:\n            yield session\n        except Exception:\n            await session.rollback()\n            raise\n        else:\n            await session.commit()\n","python","",[604,831,832,851,858,876,885,894,906,915,921,929],{"__ignoreMap":829},[833,834,836,840,843,847],"span",{"class":728,"line":835},1,[833,837,839],{"class":838},"sTJeM","async",[833,841,842],{"class":838}," def",[833,844,846],{"class":845},"s3dhs"," get_session",[833,848,850],{"class":849},"sigWx","() -> AsyncIterator[AsyncSession]:\n",[833,852,854],{"class":728,"line":853},2,[833,855,857],{"class":856},"sYEJz","    \"\"\"The transaction boundary. The endpoint never commits; this dependency does, once.\"\"\"\n",[833,859,861,864,867,870,873],{"class":728,"line":860},3,[833,862,863],{"class":838},"    async",[833,865,866],{"class":838}," with",[833,868,869],{"class":849}," Session() ",[833,871,872],{"class":838},"as",[833,874,875],{"class":849}," session:\n",[833,877,879,882],{"class":728,"line":878},4,[833,880,881],{"class":838},"        try",[833,883,884],{"class":849},":\n",[833,886,888,891],{"class":728,"line":887},5,[833,889,890],{"class":838},"            yield",[833,892,893],{"class":849}," session\n",[833,895,897,900,904],{"class":728,"line":896},6,[833,898,899],{"class":838},"        except",[833,901,903],{"class":902},"sacAq"," Exception",[833,905,884],{"class":849},[833,907,909,912],{"class":728,"line":908},7,[833,910,911],{"class":838},"            await",[833,913,914],{"class":849}," session.rollback()\n",[833,916,918],{"class":728,"line":917},8,[833,919,920],{"class":838},"            raise\n",[833,922,924,927],{"class":728,"line":923},9,[833,925,926],{"class":838},"        else",[833,928,884],{"class":849},[833,930,932,934],{"class":728,"line":931},10,[833,933,911],{"class":838},[833,935,936],{"class":849}," session.commit()\n",[590,938,939,940,942,943,945,946,949,950,953],{},"The ",[604,941,613],{}," clause is doing real work. ",[604,944,631],{}," runs ",[800,947,948],{},"only"," when the endpoint returned without raising. Writing the commit after the ",[604,951,952],{},"try"," block instead — unconditionally — would commit whatever was pending even on the failure path, which is the single most common way this pattern is broken in practice.",[590,955,956,957,960,961,964,965,967,968,971,972,975],{},"The other half of the mechanism is the distinction between ",[604,958,959],{},"flush"," and ",[604,962,963],{},"commit",". ",[604,966,627],{}," emits the pending ",[604,969,970],{},"INSERT","\u002F",[604,973,974],{},"UPDATE"," statements so that constraints fire and autogenerated primary keys become available, but it remains inside the open transaction. Nothing is durable, and a rollback erases it. Endpoints should flush freely and never commit.",[776,977,979],{"id":978},"the-fix-one-transaction-per-request","The Fix: One Transaction per Request",[590,981,982,983,986,987,990],{},"The example runs against in-memory SQLite through aiosqlite, seeded with ",[604,984,985],{},"alice"," holding 100 and ",[604,988,989],{},"bob"," holding 0. Every scenario below transfers 150 — an overdraft that must fail — and we look at what survives.",[824,992,994],{"className":826,"code":993,"language":828,"meta":829,"style":829},"\"\"\"Where the transaction boundary lives.\"\"\"\nfrom typing import Annotated, AsyncIterator\n\nfrom fastapi import Depends, FastAPI, HTTPException\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy.pool import StaticPool\n\nengine = create_async_engine(\n    \"sqlite+aiosqlite:\u002F\u002F\",\n    poolclass=StaticPool,\n    connect_args={\"check_same_thread\": False},\n)\nSession = async_sessionmaker(engine, expire_on_commit=False)\n\nSessionDep = Annotated[AsyncSession, Depends(get_session)]\n\napp = FastAPI()\n\n\nasync def _debit_credit(session: AsyncSession, amount: int) -> Account:\n    alice = await session.get(Account, \"alice\")\n    bob = await session.get(Account, \"bob\")\n    alice.balance -= amount\n    bob.balance += amount\n    session.add(Ledger(note=f\"transfer {amount}\"))\n    await session.flush()  # SQL is sent, but nothing is committed yet\n    return alice\n\n\n@app.post(\"\u002Ftransfer\u002Fdependency-owned\")\nasync def transfer_dependency_owned(session: SessionDep, amount: int = 150) -> dict:\n    \"\"\"Correct: writes are flushed, the check fails, the dependency rolls the whole unit back.\"\"\"\n    alice = await _debit_credit(session, amount)\n    if alice.balance \u003C 0:\n        raise HTTPException(status_code=409, detail=f\"overdraft: alice would be {alice.balance}\")\n    return {\"transferred\": amount}\n",[604,995,996,1001,1015,1021,1033,1045,1057,1069,1081,1085,1096,1105,1117,1140,1146,1166,1171,1182,1187,1198,1203,1208,1227,1246,1263,1275,1286,1317,1330,1339,1344,1349,1363,1392,1398,1410,1427,1468],{"__ignoreMap":829},[833,997,998],{"class":728,"line":835},[833,999,1000],{"class":856},"\"\"\"Where the transaction boundary lives.\"\"\"\n",[833,1002,1003,1006,1009,1012],{"class":728,"line":853},[833,1004,1005],{"class":838},"from",[833,1007,1008],{"class":849}," typing ",[833,1010,1011],{"class":838},"import",[833,1013,1014],{"class":849}," Annotated, AsyncIterator\n",[833,1016,1017],{"class":728,"line":860},[833,1018,1020],{"emptyLinePlaceholder":1019},true,"\n",[833,1022,1023,1025,1028,1030],{"class":728,"line":878},[833,1024,1005],{"class":838},[833,1026,1027],{"class":849}," fastapi ",[833,1029,1011],{"class":838},[833,1031,1032],{"class":849}," Depends, FastAPI, HTTPException\n",[833,1034,1035,1037,1040,1042],{"class":728,"line":887},[833,1036,1005],{"class":838},[833,1038,1039],{"class":849}," sqlalchemy ",[833,1041,1011],{"class":838},[833,1043,1044],{"class":849}," select\n",[833,1046,1047,1049,1052,1054],{"class":728,"line":896},[833,1048,1005],{"class":838},[833,1050,1051],{"class":849}," sqlalchemy.ext.asyncio ",[833,1053,1011],{"class":838},[833,1055,1056],{"class":849}," AsyncSession, async_sessionmaker, create_async_engine\n",[833,1058,1059,1061,1064,1066],{"class":728,"line":908},[833,1060,1005],{"class":838},[833,1062,1063],{"class":849}," sqlalchemy.orm ",[833,1065,1011],{"class":838},[833,1067,1068],{"class":849}," DeclarativeBase, Mapped, mapped_column\n",[833,1070,1071,1073,1076,1078],{"class":728,"line":917},[833,1072,1005],{"class":838},[833,1074,1075],{"class":849}," sqlalchemy.pool ",[833,1077,1011],{"class":838},[833,1079,1080],{"class":849}," StaticPool\n",[833,1082,1083],{"class":728,"line":923},[833,1084,1020],{"emptyLinePlaceholder":1019},[833,1086,1087,1090,1093],{"class":728,"line":931},[833,1088,1089],{"class":849},"engine ",[833,1091,1092],{"class":838},"=",[833,1094,1095],{"class":849}," create_async_engine(\n",[833,1097,1099,1102],{"class":728,"line":1098},11,[833,1100,1101],{"class":856},"    \"sqlite+aiosqlite:\u002F\u002F\"",[833,1103,1104],{"class":849},",\n",[833,1106,1108,1112,1114],{"class":728,"line":1107},12,[833,1109,1111],{"class":1110},"sV4o_","    poolclass",[833,1113,1092],{"class":838},[833,1115,1116],{"class":849},"StaticPool,\n",[833,1118,1120,1123,1125,1128,1131,1134,1137],{"class":728,"line":1119},13,[833,1121,1122],{"class":1110},"    connect_args",[833,1124,1092],{"class":838},[833,1126,1127],{"class":849},"{",[833,1129,1130],{"class":856},"\"check_same_thread\"",[833,1132,1133],{"class":849},": ",[833,1135,1136],{"class":902},"False",[833,1138,1139],{"class":849},"},\n",[833,1141,1143],{"class":728,"line":1142},14,[833,1144,1145],{"class":849},")\n",[833,1147,1149,1152,1154,1157,1160,1162,1164],{"class":728,"line":1148},15,[833,1150,1151],{"class":849},"Session ",[833,1153,1092],{"class":838},[833,1155,1156],{"class":849}," async_sessionmaker(engine, ",[833,1158,1159],{"class":1110},"expire_on_commit",[833,1161,1092],{"class":838},[833,1163,1136],{"class":902},[833,1165,1145],{"class":849},[833,1167,1169],{"class":728,"line":1168},16,[833,1170,1020],{"emptyLinePlaceholder":1019},[833,1172,1174,1177,1179],{"class":728,"line":1173},17,[833,1175,1176],{"class":849},"SessionDep ",[833,1178,1092],{"class":838},[833,1180,1181],{"class":849}," Annotated[AsyncSession, Depends(get_session)]\n",[833,1183,1185],{"class":728,"line":1184},18,[833,1186,1020],{"emptyLinePlaceholder":1019},[833,1188,1190,1193,1195],{"class":728,"line":1189},19,[833,1191,1192],{"class":849},"app ",[833,1194,1092],{"class":838},[833,1196,1197],{"class":849}," FastAPI()\n",[833,1199,1201],{"class":728,"line":1200},20,[833,1202,1020],{"emptyLinePlaceholder":1019},[833,1204,1206],{"class":728,"line":1205},21,[833,1207,1020],{"emptyLinePlaceholder":1019},[833,1209,1211,1213,1215,1218,1221,1224],{"class":728,"line":1210},22,[833,1212,839],{"class":838},[833,1214,842],{"class":838},[833,1216,1217],{"class":845}," _debit_credit",[833,1219,1220],{"class":849},"(session: AsyncSession, amount: ",[833,1222,1223],{"class":902},"int",[833,1225,1226],{"class":849},") -> Account:\n",[833,1228,1230,1233,1235,1238,1241,1244],{"class":728,"line":1229},23,[833,1231,1232],{"class":849},"    alice ",[833,1234,1092],{"class":838},[833,1236,1237],{"class":838}," await",[833,1239,1240],{"class":849}," session.get(Account, ",[833,1242,1243],{"class":856},"\"alice\"",[833,1245,1145],{"class":849},[833,1247,1249,1252,1254,1256,1258,1261],{"class":728,"line":1248},24,[833,1250,1251],{"class":849},"    bob ",[833,1253,1092],{"class":838},[833,1255,1237],{"class":838},[833,1257,1240],{"class":849},[833,1259,1260],{"class":856},"\"bob\"",[833,1262,1145],{"class":849},[833,1264,1266,1269,1272],{"class":728,"line":1265},25,[833,1267,1268],{"class":849},"    alice.balance ",[833,1270,1271],{"class":838},"-=",[833,1273,1274],{"class":849}," amount\n",[833,1276,1278,1281,1284],{"class":728,"line":1277},26,[833,1279,1280],{"class":849},"    bob.balance ",[833,1282,1283],{"class":838},"+=",[833,1285,1274],{"class":849},[833,1287,1289,1292,1295,1297,1300,1303,1305,1308,1311,1314],{"class":728,"line":1288},27,[833,1290,1291],{"class":849},"    session.add(Ledger(",[833,1293,1294],{"class":1110},"note",[833,1296,1092],{"class":838},[833,1298,1299],{"class":838},"f",[833,1301,1302],{"class":856},"\"transfer ",[833,1304,1127],{"class":838},[833,1306,1307],{"class":849},"amount",[833,1309,1310],{"class":838},"}",[833,1312,1313],{"class":856},"\"",[833,1315,1316],{"class":849},"))\n",[833,1318,1320,1323,1326],{"class":728,"line":1319},28,[833,1321,1322],{"class":838},"    await",[833,1324,1325],{"class":849}," session.flush()  ",[833,1327,1329],{"class":1328},"sFeEa","# SQL is sent, but nothing is committed yet\n",[833,1331,1333,1336],{"class":728,"line":1332},29,[833,1334,1335],{"class":838},"    return",[833,1337,1338],{"class":849}," alice\n",[833,1340,1342],{"class":728,"line":1341},30,[833,1343,1020],{"emptyLinePlaceholder":1019},[833,1345,1347],{"class":728,"line":1346},31,[833,1348,1020],{"emptyLinePlaceholder":1019},[833,1350,1352,1355,1358,1361],{"class":728,"line":1351},32,[833,1353,1354],{"class":845},"@app.post",[833,1356,1357],{"class":849},"(",[833,1359,1360],{"class":856},"\"\u002Ftransfer\u002Fdependency-owned\"",[833,1362,1145],{"class":849},[833,1364,1366,1368,1370,1373,1376,1378,1381,1384,1387,1390],{"class":728,"line":1365},33,[833,1367,839],{"class":838},[833,1369,842],{"class":838},[833,1371,1372],{"class":845}," transfer_dependency_owned",[833,1374,1375],{"class":849},"(session: SessionDep, amount: ",[833,1377,1223],{"class":902},[833,1379,1380],{"class":838}," =",[833,1382,1383],{"class":902}," 150",[833,1385,1386],{"class":849},") -> ",[833,1388,1389],{"class":902},"dict",[833,1391,884],{"class":849},[833,1393,1395],{"class":728,"line":1394},34,[833,1396,1397],{"class":856},"    \"\"\"Correct: writes are flushed, the check fails, the dependency rolls the whole unit back.\"\"\"\n",[833,1399,1401,1403,1405,1407],{"class":728,"line":1400},35,[833,1402,1232],{"class":849},[833,1404,1092],{"class":838},[833,1406,1237],{"class":838},[833,1408,1409],{"class":849}," _debit_credit(session, amount)\n",[833,1411,1413,1416,1419,1422,1425],{"class":728,"line":1412},36,[833,1414,1415],{"class":838},"    if",[833,1417,1418],{"class":849}," alice.balance ",[833,1420,1421],{"class":838},"\u003C",[833,1423,1424],{"class":902}," 0",[833,1426,884],{"class":849},[833,1428,1430,1433,1436,1439,1441,1444,1447,1450,1452,1454,1457,1459,1462,1464,1466],{"class":728,"line":1429},37,[833,1431,1432],{"class":838},"        raise",[833,1434,1435],{"class":849}," HTTPException(",[833,1437,1438],{"class":1110},"status_code",[833,1440,1092],{"class":838},[833,1442,1443],{"class":902},"409",[833,1445,1446],{"class":849},", ",[833,1448,1449],{"class":1110},"detail",[833,1451,1092],{"class":838},[833,1453,1299],{"class":838},[833,1455,1456],{"class":856},"\"overdraft: alice would be ",[833,1458,1127],{"class":838},[833,1460,1461],{"class":849},"alice.balance",[833,1463,1310],{"class":838},[833,1465,1313],{"class":856},[833,1467,1145],{"class":849},[833,1469,1471,1473,1476,1479],{"class":728,"line":1470},38,[833,1472,1335],{"class":838},[833,1474,1475],{"class":849}," {",[833,1477,1478],{"class":856},"\"transferred\"",[833,1480,1481],{"class":849},": amount}\n",[590,1483,1484,1485,1488],{},"The real transcript, with ",[604,1486,1487],{},"\u002Fstate"," reading the table back after each attempt:",[824,1490,1494],{"className":1491,"code":1493,"language":678,"meta":829},[1492],"language-text","$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": 100,\n    \"bob\": 0\n  },\n  \"ledger\": []\n}\n\n$ POST \u002Ftransfer\u002Fdependency-owned?amount=150\n409 Conflict\n{\n  \"detail\": \"overdraft: alice would be -50\"\n}\n\n$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": 100,\n    \"bob\": 0\n  },\n  \"ledger\": []\n}\n\n$ POST \u002Ftransfer\u002Fdependency-owned?amount=40\n200 OK\n{\n  \"transferred\": 40\n}\n\n$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": 60,\n    \"bob\": 40\n  },\n  \"ledger\": [\n    \"transfer 40\"\n  ]\n}\n",[604,1495,1493],{"__ignoreMap":829},[590,1497,1498,1499,1502,1503,1505,1506,1509,1510,1512,1513,1515,1516,1518],{},"Note what happened in the failing case. Both balance updates ",[800,1500,1501],{},"and"," the ledger insert were flushed to the database — the SQL genuinely executed, which is why ",[604,1504,1461],{}," was ",[604,1507,1508],{},"-50"," when the check read it. The ",[604,1511,621],{}," then propagated out of the endpoint, through the generator's ",[604,1514,617],{}," clause, and the rollback erased all three statements. The follow-up ",[604,1517,1487],{}," shows 100 and 0, exactly as before. The 409 response was still produced normally, because raising and rolling back are independent concerns.",[590,1520,1521],{},"The successful transfer of 40 confirms the same boundary commits when it should: both balances moved and the ledger row is there.",[776,1523,1525],{"id":1524},"the-failure-a-commit-in-the-middle","The Failure: a Commit in the Middle",[590,1527,1528],{},"Now the same operation with the commit moved into the handler — a change that looks harmless and is not:",[824,1530,1532],{"className":826,"code":1531,"language":828,"meta":829,"style":829},"@app.post(\"\u002Ftransfer\u002Fcommits-in-endpoint\")\nasync def transfer_commits_in_endpoint(session: SessionDep, amount: int = 150) -> dict:\n    \"\"\"Wrong: the endpoint commits the debit, so the failure leaves money destroyed.\"\"\"\n    alice = await session.get(Account, \"alice\")\n    alice.balance -= amount\n    await session.commit()  # \u003C-- the partial write is now durable\n    bob = await session.get(Account, \"bob\")\n    if alice.balance \u003C 0:\n        raise HTTPException(status_code=409, detail=f\"overdraft: alice would be {alice.balance}\")\n    bob.balance += amount\n    return {\"transferred\": amount}\n",[604,1533,1534,1545,1568,1573,1587,1595,1605,1619,1631,1663,1671],{"__ignoreMap":829},[833,1535,1536,1538,1540,1543],{"class":728,"line":835},[833,1537,1354],{"class":845},[833,1539,1357],{"class":849},[833,1541,1542],{"class":856},"\"\u002Ftransfer\u002Fcommits-in-endpoint\"",[833,1544,1145],{"class":849},[833,1546,1547,1549,1551,1554,1556,1558,1560,1562,1564,1566],{"class":728,"line":853},[833,1548,839],{"class":838},[833,1550,842],{"class":838},[833,1552,1553],{"class":845}," transfer_commits_in_endpoint",[833,1555,1375],{"class":849},[833,1557,1223],{"class":902},[833,1559,1380],{"class":838},[833,1561,1383],{"class":902},[833,1563,1386],{"class":849},[833,1565,1389],{"class":902},[833,1567,884],{"class":849},[833,1569,1570],{"class":728,"line":860},[833,1571,1572],{"class":856},"    \"\"\"Wrong: the endpoint commits the debit, so the failure leaves money destroyed.\"\"\"\n",[833,1574,1575,1577,1579,1581,1583,1585],{"class":728,"line":878},[833,1576,1232],{"class":849},[833,1578,1092],{"class":838},[833,1580,1237],{"class":838},[833,1582,1240],{"class":849},[833,1584,1243],{"class":856},[833,1586,1145],{"class":849},[833,1588,1589,1591,1593],{"class":728,"line":887},[833,1590,1268],{"class":849},[833,1592,1271],{"class":838},[833,1594,1274],{"class":849},[833,1596,1597,1599,1602],{"class":728,"line":896},[833,1598,1322],{"class":838},[833,1600,1601],{"class":849}," session.commit()  ",[833,1603,1604],{"class":1328},"# \u003C-- the partial write is now durable\n",[833,1606,1607,1609,1611,1613,1615,1617],{"class":728,"line":908},[833,1608,1251],{"class":849},[833,1610,1092],{"class":838},[833,1612,1237],{"class":838},[833,1614,1240],{"class":849},[833,1616,1260],{"class":856},[833,1618,1145],{"class":849},[833,1620,1621,1623,1625,1627,1629],{"class":728,"line":917},[833,1622,1415],{"class":838},[833,1624,1418],{"class":849},[833,1626,1421],{"class":838},[833,1628,1424],{"class":902},[833,1630,884],{"class":849},[833,1632,1633,1635,1637,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659,1661],{"class":728,"line":923},[833,1634,1432],{"class":838},[833,1636,1435],{"class":849},[833,1638,1438],{"class":1110},[833,1640,1092],{"class":838},[833,1642,1443],{"class":902},[833,1644,1446],{"class":849},[833,1646,1449],{"class":1110},[833,1648,1092],{"class":838},[833,1650,1299],{"class":838},[833,1652,1456],{"class":856},[833,1654,1127],{"class":838},[833,1656,1461],{"class":849},[833,1658,1310],{"class":838},[833,1660,1313],{"class":856},[833,1662,1145],{"class":849},[833,1664,1665,1667,1669],{"class":728,"line":931},[833,1666,1280],{"class":849},[833,1668,1283],{"class":838},[833,1670,1274],{"class":849},[833,1672,1673,1675,1677,1679],{"class":728,"line":1098},[833,1674,1335],{"class":838},[833,1676,1475],{"class":849},[833,1678,1478],{"class":856},[833,1680,1481],{"class":849},[824,1682,1685],{"className":1683,"code":1684,"language":678,"meta":829},[1492],"$ POST \u002Freset\n200 OK\n{\n  \"status\": \"seeded\"\n}\n\n$ POST \u002Ftransfer\u002Fcommits-in-endpoint?amount=150\n409 Conflict\n{\n  \"detail\": \"overdraft: alice would be -50\"\n}\n\n$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": -50,\n    \"bob\": 0\n  },\n  \"ledger\": []\n}\n",[604,1686,1684],{"__ignoreMap":829},[590,1688,1689,1690,1692,1693,960,1695,1697,1698,1700],{},"The API returned the same 409. The client sees an identical failure and will reasonably assume nothing happened. But ",[604,1691,985],{}," is now at ",[593,1694,1508],{},[604,1696,989],{}," is unchanged: 150 units have simply ceased to exist. The dependency's rollback still ran — it just had nothing left to undo, because ",[604,1699,631],{}," had already ended the transaction that contained the debit.",[590,1702,1703,1704,1706],{},"This is why \"search the codebase for ",[604,1705,631],{},"\" is a productive code review. Every occurrence outside the session dependency is a candidate partial write, and the damage is silent — no error, no log line, just a row that is wrong forever.",[776,1708,1710],{"id":1709},"explicit-blocks-and-savepoints","Explicit Blocks and Savepoints",[590,1712,1713],{},"Two variants are worth having in your vocabulary.",[590,1715,1716,1717,1720],{},"When work happens outside a request — a startup job, a task worker, a script — there is no dependency to own the boundary, so use ",[604,1718,1719],{},"session.begin()"," as a context manager. It commits on clean exit and rolls back on any exception:",[824,1722,1724],{"className":826,"code":1723,"language":828,"meta":829,"style":829},"@app.post(\"\u002Ftransfer\u002Fexplicit-begin\")\nasync def transfer_explicit_begin(amount: int = 150) -> dict:\n    \"\"\"`async with session.begin()` rolls back on any exception leaving the block.\"\"\"\n    try:\n        async with Session() as session, session.begin():\n            alice = await _debit_credit(session, amount)\n            if alice.balance \u003C 0:\n                raise ValueError(f\"overdraft: alice would be {alice.balance}\")\n    except ValueError as exc:\n        return {\"rolled_back\": True, \"reason\": str(exc)}\n    return {\"transferred\": amount}\n",[604,1725,1726,1737,1761,1766,1773,1787,1798,1811,1835,1848,1876],{"__ignoreMap":829},[833,1727,1728,1730,1732,1735],{"class":728,"line":835},[833,1729,1354],{"class":845},[833,1731,1357],{"class":849},[833,1733,1734],{"class":856},"\"\u002Ftransfer\u002Fexplicit-begin\"",[833,1736,1145],{"class":849},[833,1738,1739,1741,1743,1746,1749,1751,1753,1755,1757,1759],{"class":728,"line":853},[833,1740,839],{"class":838},[833,1742,842],{"class":838},[833,1744,1745],{"class":845}," transfer_explicit_begin",[833,1747,1748],{"class":849},"(amount: ",[833,1750,1223],{"class":902},[833,1752,1380],{"class":838},[833,1754,1383],{"class":902},[833,1756,1386],{"class":849},[833,1758,1389],{"class":902},[833,1760,884],{"class":849},[833,1762,1763],{"class":728,"line":860},[833,1764,1765],{"class":856},"    \"\"\"`async with session.begin()` rolls back on any exception leaving the block.\"\"\"\n",[833,1767,1768,1771],{"class":728,"line":878},[833,1769,1770],{"class":838},"    try",[833,1772,884],{"class":849},[833,1774,1775,1778,1780,1782,1784],{"class":728,"line":887},[833,1776,1777],{"class":838},"        async",[833,1779,866],{"class":838},[833,1781,869],{"class":849},[833,1783,872],{"class":838},[833,1785,1786],{"class":849}," session, session.begin():\n",[833,1788,1789,1792,1794,1796],{"class":728,"line":896},[833,1790,1791],{"class":849},"            alice ",[833,1793,1092],{"class":838},[833,1795,1237],{"class":838},[833,1797,1409],{"class":849},[833,1799,1800,1803,1805,1807,1809],{"class":728,"line":908},[833,1801,1802],{"class":838},"            if",[833,1804,1418],{"class":849},[833,1806,1421],{"class":838},[833,1808,1424],{"class":902},[833,1810,884],{"class":849},[833,1812,1813,1816,1819,1821,1823,1825,1827,1829,1831,1833],{"class":728,"line":917},[833,1814,1815],{"class":838},"                raise",[833,1817,1818],{"class":902}," ValueError",[833,1820,1357],{"class":849},[833,1822,1299],{"class":838},[833,1824,1456],{"class":856},[833,1826,1127],{"class":838},[833,1828,1461],{"class":849},[833,1830,1310],{"class":838},[833,1832,1313],{"class":856},[833,1834,1145],{"class":849},[833,1836,1837,1840,1842,1845],{"class":728,"line":923},[833,1838,1839],{"class":838},"    except",[833,1841,1818],{"class":902},[833,1843,1844],{"class":838}," as",[833,1846,1847],{"class":849}," exc:\n",[833,1849,1850,1853,1855,1858,1860,1863,1865,1868,1870,1873],{"class":728,"line":931},[833,1851,1852],{"class":838},"        return",[833,1854,1475],{"class":849},[833,1856,1857],{"class":856},"\"rolled_back\"",[833,1859,1133],{"class":849},[833,1861,1862],{"class":902},"True",[833,1864,1446],{"class":849},[833,1866,1867],{"class":856},"\"reason\"",[833,1869,1133],{"class":849},[833,1871,1872],{"class":902},"str",[833,1874,1875],{"class":849},"(exc)}\n",[833,1877,1878,1880,1882,1884],{"class":728,"line":1098},[833,1879,1335],{"class":838},[833,1881,1475],{"class":849},[833,1883,1478],{"class":856},[833,1885,1481],{"class":849},[824,1887,1890],{"className":1888,"code":1889,"language":678,"meta":829},[1492],"$ POST \u002Ftransfer\u002Fexplicit-begin?amount=150\n200 OK\n{\n  \"rolled_back\": true,\n  \"reason\": \"overdraft: alice would be -50\"\n}\n\n$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": 100,\n    \"bob\": 0\n  },\n  \"ledger\": []\n}\n",[604,1891,1889],{"__ignoreMap":829},[590,1893,1894,1895,1898],{},"Note the 200 status: the exception never left the endpoint, yet the balances are untouched. The rollback is driven by the ",[604,1896,1897],{},"begin()"," block exiting with an exception, entirely independently of what the HTTP layer decides to return.",[590,1900,1901,1902,1904,1905,1908],{},"The second variant answers a real requirement — \"we must record that the attempt happened, even though the attempt failed\". A blanket rollback would erase the audit row along with everything else. ",[604,1903,644],{}," issues a ",[604,1906,1907],{},"SAVEPOINT"," so you can undo a sub-range:",[824,1910,1912],{"className":826,"code":1911,"language":828,"meta":829,"style":829},"@app.post(\"\u002Ftransfer\u002Fsavepoint\")\nasync def transfer_savepoint(session: SessionDep, amount: int = 150) -> dict:\n    \"\"\"A nested savepoint undoes only the risky part; the audit row still commits.\"\"\"\n    session.add(Ledger(note=\"attempt logged\"))\n    rolled_back = False\n    try:\n        async with session.begin_nested():\n            alice = await _debit_credit(session, amount)\n            if alice.balance \u003C 0:\n                raise ValueError(\"overdraft\")\n    except ValueError:\n        rolled_back = True\n    return {\"savepoint_rolled_back\": rolled_back}\n",[604,1913,1914,1925,1948,1953,1966,1976,1982,1991,2001,2013,2026,2034,2044],{"__ignoreMap":829},[833,1915,1916,1918,1920,1923],{"class":728,"line":835},[833,1917,1354],{"class":845},[833,1919,1357],{"class":849},[833,1921,1922],{"class":856},"\"\u002Ftransfer\u002Fsavepoint\"",[833,1924,1145],{"class":849},[833,1926,1927,1929,1931,1934,1936,1938,1940,1942,1944,1946],{"class":728,"line":853},[833,1928,839],{"class":838},[833,1930,842],{"class":838},[833,1932,1933],{"class":845}," transfer_savepoint",[833,1935,1375],{"class":849},[833,1937,1223],{"class":902},[833,1939,1380],{"class":838},[833,1941,1383],{"class":902},[833,1943,1386],{"class":849},[833,1945,1389],{"class":902},[833,1947,884],{"class":849},[833,1949,1950],{"class":728,"line":860},[833,1951,1952],{"class":856},"    \"\"\"A nested savepoint undoes only the risky part; the audit row still commits.\"\"\"\n",[833,1954,1955,1957,1959,1961,1964],{"class":728,"line":878},[833,1956,1291],{"class":849},[833,1958,1294],{"class":1110},[833,1960,1092],{"class":838},[833,1962,1963],{"class":856},"\"attempt logged\"",[833,1965,1316],{"class":849},[833,1967,1968,1971,1973],{"class":728,"line":887},[833,1969,1970],{"class":849},"    rolled_back ",[833,1972,1092],{"class":838},[833,1974,1975],{"class":902}," False\n",[833,1977,1978,1980],{"class":728,"line":896},[833,1979,1770],{"class":838},[833,1981,884],{"class":849},[833,1983,1984,1986,1988],{"class":728,"line":908},[833,1985,1777],{"class":838},[833,1987,866],{"class":838},[833,1989,1990],{"class":849}," session.begin_nested():\n",[833,1992,1993,1995,1997,1999],{"class":728,"line":917},[833,1994,1791],{"class":849},[833,1996,1092],{"class":838},[833,1998,1237],{"class":838},[833,2000,1409],{"class":849},[833,2002,2003,2005,2007,2009,2011],{"class":728,"line":923},[833,2004,1802],{"class":838},[833,2006,1418],{"class":849},[833,2008,1421],{"class":838},[833,2010,1424],{"class":902},[833,2012,884],{"class":849},[833,2014,2015,2017,2019,2021,2024],{"class":728,"line":931},[833,2016,1815],{"class":838},[833,2018,1818],{"class":902},[833,2020,1357],{"class":849},[833,2022,2023],{"class":856},"\"overdraft\"",[833,2025,1145],{"class":849},[833,2027,2028,2030,2032],{"class":728,"line":1098},[833,2029,1839],{"class":838},[833,2031,1818],{"class":902},[833,2033,884],{"class":849},[833,2035,2036,2039,2041],{"class":728,"line":1107},[833,2037,2038],{"class":849},"        rolled_back ",[833,2040,1092],{"class":838},[833,2042,2043],{"class":902}," True\n",[833,2045,2046,2048,2050,2053],{"class":728,"line":1119},[833,2047,1335],{"class":838},[833,2049,1475],{"class":849},[833,2051,2052],{"class":856},"\"savepoint_rolled_back\"",[833,2054,2055],{"class":849},": rolled_back}\n",[824,2057,2060],{"className":2058,"code":2059,"language":678,"meta":829},[1492],"$ POST \u002Ftransfer\u002Fsavepoint?amount=150\n200 OK\n{\n  \"savepoint_rolled_back\": true\n}\n\n$ GET \u002Fstate\n200 OK\n{\n  \"accounts\": {\n    \"alice\": 100,\n    \"bob\": 0\n  },\n  \"ledger\": [\n    \"attempt logged\"\n  ]\n}\n",[604,2061,2059],{"__ignoreMap":829},[590,2063,2064,2065,2068,2069,2073],{},"Balances unchanged, ",[604,2066,2067],{},"attempt logged"," persisted. The savepoint rolled back the transfer; the outer transaction continued and the dependency committed it on the way out. This is also the mechanism behind per-test rollback fixtures, described in ",[650,2070,2072],{"href":2071},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftesting-with-async-database-fixtures\u002F","Testing with Async Database Fixtures",".",[776,2075,2077],{"id":2076},"verification","Verification",[590,2079,2080],{},"The assertion that matters is not \"the endpoint returned 409\" but \"the database is unchanged\". Test both:",[824,2082,2084],{"className":826,"code":2083,"language":828,"meta":829,"style":829},"async def test_failed_transfer_changes_nothing(client):\n    before = (await client.get(\"\u002Fstate\")).json()\n    failed = await client.post(\"\u002Ftransfer\u002Fdependency-owned\", params={\"amount\": 150})\n    assert failed.status_code == 409\n    assert (await client.get(\"\u002Fstate\")).json() == before\n",[604,2085,2086,2098,2120,2153,2167],{"__ignoreMap":829},[833,2087,2088,2090,2092,2095],{"class":728,"line":835},[833,2089,839],{"class":838},[833,2091,842],{"class":838},[833,2093,2094],{"class":845}," test_failed_transfer_changes_nothing",[833,2096,2097],{"class":849},"(client):\n",[833,2099,2100,2103,2105,2108,2111,2114,2117],{"class":728,"line":853},[833,2101,2102],{"class":849},"    before ",[833,2104,1092],{"class":838},[833,2106,2107],{"class":849}," (",[833,2109,2110],{"class":838},"await",[833,2112,2113],{"class":849}," client.get(",[833,2115,2116],{"class":856},"\"\u002Fstate\"",[833,2118,2119],{"class":849},")).json()\n",[833,2121,2122,2125,2127,2129,2132,2134,2136,2139,2141,2143,2146,2148,2150],{"class":728,"line":860},[833,2123,2124],{"class":849},"    failed ",[833,2126,1092],{"class":838},[833,2128,1237],{"class":838},[833,2130,2131],{"class":849}," client.post(",[833,2133,1360],{"class":856},[833,2135,1446],{"class":849},[833,2137,2138],{"class":1110},"params",[833,2140,1092],{"class":838},[833,2142,1127],{"class":849},[833,2144,2145],{"class":856},"\"amount\"",[833,2147,1133],{"class":849},[833,2149,716],{"class":902},[833,2151,2152],{"class":849},"})\n",[833,2154,2155,2158,2161,2164],{"class":728,"line":878},[833,2156,2157],{"class":838},"    assert",[833,2159,2160],{"class":849}," failed.status_code ",[833,2162,2163],{"class":838},"==",[833,2165,2166],{"class":902}," 409\n",[833,2168,2169,2171,2173,2175,2177,2179,2182,2184],{"class":728,"line":887},[833,2170,2157],{"class":838},[833,2172,2107],{"class":849},[833,2174,2110],{"class":838},[833,2176,2113],{"class":849},[833,2178,2116],{"class":856},[833,2180,2181],{"class":849},")).json() ",[833,2183,2163],{"class":838},[833,2185,2186],{"class":849}," before\n",[590,2188,2189],{},"Two more checks are worth adding permanently. Grep for stray boundaries in CI, since this is a rule a linter can hold for you:",[824,2191,2195],{"className":2192,"code":2193,"language":2194,"meta":829,"style":829},"language-bash shiki shiki-themes github-light-high-contrast","# Any commit outside the session dependency is a partial-write risk.\ngrep -rn \"\\.commit()\" app\u002F --include=\"*.py\" | grep -v \"app\u002Fdb\u002Fsession.py\"\n","bash",[604,2196,2197,2202],{"__ignoreMap":829},[833,2198,2199],{"class":728,"line":835},[833,2200,2201],{"class":1328},"# Any commit outside the session dependency is a partial-write risk.\n",[833,2203,2204,2207,2210,2213,2216,2219,2222,2225,2228,2231],{"class":728,"line":853},[833,2205,2206],{"class":1110},"grep",[833,2208,2209],{"class":902}," -rn",[833,2211,2212],{"class":856}," \"\\.commit()\"",[833,2214,2215],{"class":856}," app\u002F",[833,2217,2218],{"class":902}," --include=",[833,2220,2221],{"class":856},"\"*.py\"",[833,2223,2224],{"class":838}," |",[833,2226,2227],{"class":1110}," grep",[833,2229,2230],{"class":902}," -v",[833,2232,2233],{"class":856}," \"app\u002Fdb\u002Fsession.py\"\n",[590,2235,2236,2237,2240],{},"And in production, alert on ",[604,2238,2239],{},"ROLLBACK"," rate rather than only on 5xx. A handler that quietly rolls back on every request because a constraint always fires will look healthy in your HTTP metrics while writing nothing at all.",[776,2242,2244],{"id":2243},"trade-offs-and-when-not-to","Trade-offs and When Not To",[590,2246,2247,2250,2251,2254,2255,2073],{},[593,2248,2249],{},"Long transactions hold connections."," One transaction per request is correct, but a request that fans out to a slow upstream ",[800,2252,2253],{},"while holding an open transaction"," keeps a pooled connection and its locks for the whole call. Do the external I\u002FO before you open the write path, or you will meet the exhaustion described in ",[650,2256,2258],{"href":2257},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ffixing-asyncpg-pool-exhaustion\u002F","Fixing asyncpg Pool Exhaustion",[590,2260,2261,2264,2265,2269,2270,2073],{},[593,2262,2263],{},"Rollback does not undo side effects."," Only the database is transactional. Emails sent, payments captured and cache entries written during a rolled-back request stay done. Defer those to a background task queued only on success — see ",[650,2266,2268],{"href":2267},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002F","Background Task Processing"," and, for the idempotency this implies, ",[650,2271,2273],{"href":2272},"\u002Fasync-background-tasks-observability\u002Fbackground-task-processing\u002Fretry-and-idempotency-for-tasks\u002F","Retry and Idempotency for Tasks",[590,2275,2276,2279],{},[593,2277,2278],{},"Savepoints have a cost."," They are cheap but not free, and heavy nesting inside a loop generates a lot of round trips. Reach for one when a specific sub-operation must be recoverable, not as a default wrapper.",[590,2281,2282,2285],{},[593,2283,2284],{},"Batch jobs may want many transactions."," \"One transaction per request\" is a rule for requests. A job importing a million rows should commit in batches so a failure at row 999,999 does not discard everything, and that job should not be running inside a web handler anyway.",[590,2287,2288,2291,2292,2295],{},[593,2289,2290],{},"SQLite is not Postgres."," The transcripts above are real, but SQLite's isolation and locking differ from Postgres and MySQL. The boundary ",[800,2293,2294],{},"placement"," transfers exactly; concurrency behaviour under load must be tested against your real engine.",[776,2297,2299],{"id":2298},"faq","FAQ",[590,2301,2302,2305,2306,2308,2309,2311],{},[593,2303,2304],{},"Should the endpoint or the dependency call commit?","\nThe dependency. A ",[604,2307,606],{}," dependency wraps the whole path operation, so committing after the ",[604,2310,606],{}," means one transaction per request that commits only if the endpoint returned normally. An endpoint that commits mid-way makes every later failure a partial write.",[590,2313,2314,2317,2319,2320,960,2322,2324,2325,2327],{},[593,2315,2316],{},"What is the difference between flush and commit?",[604,2318,959],{}," sends the pending ",[604,2321,970],{},[604,2323,974],{}," statements to the database so constraints and generated primary keys take effect, but it stays inside the open transaction and is undone by a rollback. ",[604,2326,963],{}," ends the transaction and makes everything durable.",[590,2329,2330,2333,2334,2336,2337,2339,2340,2342,2343,2345,2346,2348],{},[593,2331,2332],{},"Does raising HTTPException roll back my transaction?","\nOnly if something rolls it back. ",[604,2335,621],{}," is an ordinary exception, so a ",[604,2338,606],{}," dependency that commits in an ",[604,2341,613],{}," clause or rolls back in an ",[604,2344,617],{}," clause will handle it correctly, while a dependency that commits unconditionally after the ",[604,2347,606],{}," will commit the partial work.",[590,2350,2351,2354,2355,2358,2359,2362,2363,2365],{},[593,2352,2353],{},"Do I need to call rollback explicitly if I use the session as a context manager?","\nNot for correctness, because ",[604,2356,2357],{},"AsyncSession.close"," discards any uncommitted transaction when the ",[604,2360,2361],{},"async with"," block exits. An explicit rollback in an ",[604,2364,617],{}," clause is still worth writing because it releases the database locks immediately and makes the intent obvious to readers.",[590,2367,2368,2371,2372,2375,2376,2378],{},[593,2369,2370],{},"How do I keep an audit row when the main work fails?","\nPut the risky work inside ",[604,2373,2374],{},"session.begin_nested",", which issues a ",[604,2377,1907],{},". Rolling back to the savepoint undoes only that block, leaving earlier statements in the outer transaction intact so they commit with the request.",[590,2380,2381,2384,2385,2387],{},[593,2382,2383],{},"Why did my rollback not undo anything?","\nAlmost always because a commit already happened earlier in the request, so the rollback only covers the statements issued after it. Search the handler and its helpers for stray ",[604,2386,963],{}," calls; the transaction boundary must exist in exactly one place.",[776,2389,2391],{"id":2390},"related","Related",[597,2393,2394,2403,2413,2421,2429],{},[600,2395,2396,2399,2400,2402],{},[593,2397,2398],{},"Up to the topic:"," ",[650,2401,653],{"href":652}," covers engine, pool and session lifetime.",[600,2404,2405,2399,2408,2412],{},[593,2406,2407],{},"The session pattern itself:",[650,2409,2411],{"href":2410},"\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Fasync-sqlalchemy-session-per-request\u002F","Async SQLAlchemy Session per Request"," is where the dependency shown here comes from.",[600,2414,2415,2399,2418,2420],{},[593,2416,2417],{},"Proving rollback in tests:",[650,2419,2072],{"href":2071}," reuses savepoints for per-test isolation.",[600,2422,2423,2399,2426,2428],{},[593,2424,2425],{},"When transactions run too long:",[650,2427,2258],{"href":2257}," diagnoses the connection starvation that follows.",[600,2430,2431,2399,2434,2438,2439,2441],{},[593,2432,2433],{},"Dependency mechanics:",[650,2435,2437],{"href":2436},"\u002Fcore-architecture-routing-patterns\u002Fdependency-injection-strategies\u002F","Dependency Injection Strategies"," explains the ",[604,2440,606],{}," lifecycle this pattern relies on.",[2443,2444,2445],"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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sV4o_, html code.shiki .sV4o_{--shiki-default:#702C00}html pre.shiki code .sFeEa, html code.shiki .sFeEa{--shiki-default:#66707B}",{"title":829,"searchDepth":853,"depth":853,"links":2447},[2448,2449,2451,2452,2453,2454,2455,2456,2457],{"id":778,"depth":853,"text":779},{"id":788,"depth":853,"text":2450},"Why It Happens: yield Dependencies Wrap the Path Operation",{"id":978,"depth":853,"text":979},{"id":1524,"depth":853,"text":1525},{"id":1709,"depth":853,"text":1710},{"id":2076,"depth":853,"text":2077},{"id":2243,"depth":853,"text":2244},{"id":2298,"depth":853,"text":2299},{"id":2390,"depth":853,"text":2391},"2026-07-20","Own the transaction in a yield dependency: where commit and rollback belong, what a partial write looks like when an endpoint raises, and when to savepoint.","md",[2462,2464,2466,2468,2470,2472],{"q":2304,"a":2463},"The dependency. A yield dependency wraps the whole path operation, so committing after the yield means one transaction per request that commits only if the endpoint returned normally. An endpoint that commits mid-way makes every later failure a partial write.",{"q":2316,"a":2465},"flush sends the pending INSERT and UPDATE statements to the database so constraints and generated primary keys take effect, but it stays inside the open transaction and is undone by a rollback. commit ends the transaction and makes everything durable.",{"q":2332,"a":2467},"Only if something rolls it back. HTTPException is an ordinary exception, so a yield dependency that commits in an else clause or rolls back in an except clause will handle it correctly, while a dependency that commits unconditionally after the yield will commit the partial work.",{"q":2353,"a":2469},"Not for correctness, because AsyncSession.close discards any uncommitted transaction when the async with block exits. An explicit rollback in an except clause is still worth writing because it releases the database locks immediately and makes the intent obvious to readers.",{"q":2370,"a":2471},"Put the risky work inside session.begin_nested, which issues a SAVEPOINT. Rolling back to the savepoint undoes only that block, leaving earlier statements in the outer transaction intact so they commit with the request.",{"q":2383,"a":2473},"Almost always because a commit already happened earlier in the request, so the rollback only covers the statements issued after it. Search the handler and its helpers for stray commit calls; the transaction boundary must exist in exactly one place.",null,{"slug":2476,"breadcrumb":2477},"transaction-management-and-rollback",[2478,2480,2483,2484],{"label":2479,"path":971},"Home",{"label":2481,"path":2482},"Async, Background Tasks & Observability","\u002Fasync-background-tasks-observability\u002F",{"label":653,"path":652},{"label":2485,"path":2486},"Transaction Management and Rollback","\u002Fasync-background-tasks-observability\u002Fasync-database-sessions\u002Ftransaction-management-and-rollback\u002F",{"title":249,"description":2459},"article","usBJk1iGW2Lif6wUmkYVKGXEhiSVAi4Pm-IZZGb1aGM",[2474,2474],1784588203038]