📝 Fix StreamingResponse async generator example to actually stream#14993
📝 Fix StreamingResponse async generator example to actually stream#14993Vishnuuuu24 wants to merge 1 commit intofastapi:masterfrom
Conversation
📝 Docs previewLast commit 355f2cf at: https://18fad690.fastapitiangolo.pages.dev Modified Pages |
There was a problem hiding this comment.
Pull request overview
Updates the FastAPI StreamingResponse documentation and its referenced example so the async generator yields control to the event loop, making the example behave like real incremental streaming.
Changes:
- Added
await asyncio.sleep(0)(andimport asyncio) to the async generator in theStreamingResponsetutorial example. - Updated the docs snippet highlighting and added a tip explaining the need for an
awaitwhen using async generators for streaming.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| docs_src/custom_response/tutorial007_py310.py | Makes the documented async generator include an await so streaming can yield control between chunks. |
| docs/en/docs/advanced/custom-response.md | Updates the StreamingResponse section snippet highlighting and adds a tip about async generator streaming behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -6,6 +8,7 @@ | |||
|
|
|||
| async def fake_video_streamer(): | |||
| for i in range(10): | |||
There was a problem hiding this comment.
The loop variable i is unused; consider renaming it to _ to avoid implying the value is used and to keep the example focused.
| for i in range(10): | |
| for _ in range(10): |
| When using an **async generator**, make sure it contains at least one `await` expression (e.g. `await asyncio.sleep(0)`) to yield control back to the event loop between iterations. Without an `await`, the generator will run synchronously and the response will be buffered entirely before being sent to the client, instead of being streamed incrementally. | ||
|
|
||
| If your generator wraps synchronous/blocking work and has no natural `await` points, consider using a **normal generator** with a `def` function instead. |
There was a problem hiding this comment.
The tip text is quite absolute (e.g. "Without an await, the generator will run synchronously and the response will be buffered entirely"). Even without an await inside the async generator, each yield still suspends execution; the real issue is typically blocking/CPU-bound work between yields or producing all chunks back-to-back so the transport coalesces them. Consider rephrasing to clarify that an await (or other pause/offloading) is needed to avoid blocking the event loop / to observe incremental delivery, rather than stating that async generators inherently buffer without await.
| When using an **async generator**, make sure it contains at least one `await` expression (e.g. `await asyncio.sleep(0)`) to yield control back to the event loop between iterations. Without an `await`, the generator will run synchronously and the response will be buffered entirely before being sent to the client, instead of being streamed incrementally. | |
| If your generator wraps synchronous/blocking work and has no natural `await` points, consider using a **normal generator** with a `def` function instead. | |
| When using an **async generator**, make sure it includes at least one `await` (or other non-blocking pause/offloading) between iterations, for example `await asyncio.sleep(0)` or `await some_async_io()`. This yields control back to the event loop so it can send each chunk as it is produced, instead of running all the work in a tight loop where the client only sees the response once most of the work has completed. | |
| If your async generator wraps mostly synchronous or CPU-bound work and has no natural `await` points, it may still behave as if the response is buffered because the event loop cannot interleave other tasks. In that case, consider using a **normal generator** with a `def` function and/or offloading the blocking work appropriately, while yielding smaller chunks. |
5b906af to
543fd2b
Compare
Add `await asyncio.sleep(0)` to the async generator in the StreamingResponse documentation example so it properly yields control back to the event loop between iterations, enabling true incremental streaming and proper task cancellation. Without an await point the async generator holds the event loop for the entire iteration, buffering the full response before sending it to the client. Also add a note admonition explaining why the await is needed and suggesting sync generators as an alternative when there are no natural await points. Closes fastapi#14680
543fd2b to
355f2cf
Compare
|
This is a duplicate of #14681 Let's close this and I encourage you review existing PR @Vishnuuuu24, thank you! |
Summary
Fixes #14680.
The
StreamingResponsedocumentation example uses an async generator (fake_video_streamer) without anyawaitexpression. This causes two problems:awaitpoint, the async generator holds the event loop for the entire iteration, so the response is buffered entirely and sent all at once instead of being streamed chunk by chunk.await. Without one, the generator cannot be interrupted and may keep running after cancellation is requested.Changes
1. Code example (
docs_src/custom_response/tutorial007_py310.py)Added
import asyncioandawait asyncio.sleep(0)inside the async generator loop. This is the minimal fix to yield control back to the event loop between iterations.2. Documentation (
docs/en/docs/advanced/custom-response.md)asyncioimport andawaitline./// noteadmonition after the code example explaining why theawaitis necessary (event loop yielding, streaming, cancellation) and suggesting sync generators as an alternative.Only the English docs are updated — translations are handled separately per project convention.
Validation
tests/test_tutorial/test_custom_response/test_tutorial007.py) passes.ruff checkandruff formatpass cleanly.