Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[0.118 Regression] Background tasks not executed if added after ayield#14137

Answeredbytiangolo
JP-Ellis asked this question inQuestions
Discussion options

First Check

  • I added a very descriptive title here.
  • I used the GitHub search to find a similar question and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but toPydantic.
  • I already checked if it is not related to FastAPI but toSwagger UI.
  • I already checked if it is not related to FastAPI but toReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

fromcollections.abcimportAsyncGenerator,GeneratorfromtypingimportAnnotated,AnyfromfastapiimportFastAPI,Depends,BackgroundTasksimporttimefromdatetimeimportdatetimeapp=FastAPI()defnow()->str:returndatetime.now().strftime("%H:%M:%S.%f")[:-3]defbackground_task(task_name:str,delay:int=1)->None:print(f"[{now()}|background-task] Started{task_name}")t0=time.monotonic()time.sleep(delay)print(f"[{now()}|background-task] Finished{task_name}. Elapsed:{time.monotonic()-t0:.3f} seconds"    )defsync_dependency(background_tasks:BackgroundTasks)->Generator[dict[str,Any]]:print(f"[{now()}|sync-dependency] Started")t0=time.monotonic()background_tasks.add_task(background_task,"sync:pre")yield {"timestamp":time.time()}# The following background task is never executedbackground_tasks.add_task(background_task,"sync:post")# This statement is executed correctlyprint(f"[{now()}|sync-dependency] Finished. Elapsed:{time.monotonic()-t0:.3f} seconds"    )asyncdefasync_dependency(background_tasks:BackgroundTasks,)->AsyncGenerator[dict[str,Any]]:print(f"[{now()}|async-dependency] Started")t0=time.monotonic()background_tasks.add_task(background_task,"async:pre")yield {"timestamp":time.time()}# The following background task is never executedbackground_tasks.add_task(background_task,"async:post")# This statement is executed correctlyprint(f"[{now()}|async-dependency] Finished. Elapsed:{time.monotonic()-t0:.3f} seconds"    )@app.get("/sync")defsync(data:Annotated[dict[str,Any],Depends(sync_dependency)],background_tasks:BackgroundTasks,)->dict[str,Any]:print(f"[{now()}|GET /sync] Handling request")background_tasks.add_task(background_task,"sync:handler",2)return {"message":"Hello from sync endpoint!","data":data}@app.get("/async")asyncdefasync_endpoint(data:Annotated[dict[str,Any],Depends(async_dependency)],background_tasks:BackgroundTasks,)->dict[str,Any]:print(f"[{now()}|GET /async] Handling request")background_tasks.add_task(background_task,"async:handler",2)return {"message":"Hello from async endpoint!","data":data}if__name__=="__main__":importuvicornuvicorn.run(app,host="localhost",port=8000,log_level="info")

Description

Running the above FastAPI app (e.g., throughuvicorn) and the background tasks which are addedafter theyield statements do not run, despite the code executing at some point since theprint statements are executed.

Here are some logs, while I performGET /sync andGET /async

❯ uv run run.py INFO:     Started server process [25058]INFO:     Waiting for application startup.INFO:     Application startup complete.INFO:     Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)[13:01:52.328|sync-dependency] Started[13:01:52.329|GET /sync] Handling requestINFO:     ::1:49786 - "GET /sync HTTP/1.1" 200 OK[13:01:52.329|background-task] Started sync:pre[13:01:53.330|background-task] Finished sync:pre. Elapsed: 1.001 seconds[13:01:53.331|background-task] Started sync:handler[13:01:55.338|background-task] Finished sync:handler. Elapsed: 2.007 seconds[13:01:55.338|sync-dependency] Finished. Elapsed: 3.010 seconds[13:02:00.122|async-dependency] Started[13:02:00.122|GET /async] Handling requestINFO:     ::1:49787 - "GET /async HTTP/1.1" 200 OK[13:02:00.122|background-task] Started async:pre[13:02:01.127|background-task] Finished async:pre. Elapsed: 1.005 seconds[13:02:01.128|background-task] Started async:handler[13:02:03.131|background-task] Finished async:handler. Elapsed: 2.003 seconds[13:02:03.131|async-dependency] Finished. Elapsed: 3.009 seconds

Observe that thesync:post andasync:post jobs' output are never displayed.

Operating System

macOS, Linux

Operating System Details

No response

FastAPI Version

0.118.0

Pydantic Version

2.11.9

Python Version

Python 3.13.7

Additional Context

The0.118 release changes the behaviour of yielded value; however, I believe it is an error that background tasks dispatched this way silently fail.

Running the same code against0.117 shows the background jobs executing correctly (note the presence ofsync:post andasync:post)

 uv run run.py INFO:     Started server process [26027]INFO:     Waiting for application startup.INFO:     Application startup complete.INFO:     Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)[13:05:48.165|sync-dependency] Started[13:05:48.165|GET /sync] Handling request[13:05:48.166|sync-dependency] Finished. Elapsed: 0.001 secondsINFO:     ::1:49897 - "GET /sync HTTP/1.1" 200 OK[13:05:48.166|background-task] Started sync:pre[13:05:49.171|background-task] Finished sync:pre. Elapsed: 1.005 seconds[13:05:49.171|background-task] Started sync:handler[13:05:51.176|background-task] Finished sync:handler. Elapsed: 2.005 seconds[13:05:51.177|background-task] Started sync:post[13:05:52.182|background-task] Finished sync:post. Elapsed: 1.006 seconds[13:06:01.497|async-dependency] Started[13:06:01.497|GET /async] Handling request[13:06:01.497|async-dependency] Finished. Elapsed: 0.000 secondsINFO:     ::1:49900 - "GET /async HTTP/1.1" 200 OK[13:06:01.497|background-task] Started async:pre[13:06:02.499|background-task] Finished async:pre. Elapsed: 1.002 seconds[13:06:02.499|background-task] Started async:handler[13:06:04.505|background-task] Finished async:handler. Elapsed: 2.005 seconds[13:06:04.509|background-task] Started async:post[13:06:05.514|background-task] Finished async:post. Elapsed: 1.004 seconds
You must be logged in to vote

It seems you were depending on dependencies exiting early, before the response cycle, background tasks come directly from Starlette and are run as part of the response execution, that's why adding background tasks afteryield was working before.

Now that dependencies withyield are (again) by default closed after the response is sent, the code afteryield by default doesn't have a way to add background tasks, as that code is executed after the entire response cycle, which includes the background tasks.

But now, since FastAPI 0.121.0 you can opt-in to early exit, just like before, with the newscope="function":https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#ear…

Replies: 2 comments 2 replies

Comment options

I have created a PR to fix this in#14192, I'm hoping it can be reviewed soonish 🤞

You must be logged in to vote
0 replies
Comment options

It seems you were depending on dependencies exiting early, before the response cycle, background tasks come directly from Starlette and are run as part of the response execution, that's why adding background tasks afteryield was working before.

Now that dependencies withyield are (again) by default closed after the response is sent, the code afteryield by default doesn't have a way to add background tasks, as that code is executed after the entire response cycle, which includes the background tasks.

But now, since FastAPI 0.121.0 you can opt-in to early exit, just like before, with the newscope="function":https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#early-exit-and-scope

Here's your example code with the newDepends(func, scope="function"), showing the same behavior you had before (and expected):

fromcollections.abcimportAsyncGenerator,GeneratorfromtypingimportAnnotated,AnyfromfastapiimportFastAPI,Depends,BackgroundTasksimporttimefromdatetimeimportdatetimeapp=FastAPI()defnow()->str:returndatetime.now().strftime("%H:%M:%S.%f")[:-3]defbackground_task(task_name:str,delay:int=1)->None:print(f"[{now()}|background-task] Started{task_name}")t0=time.monotonic()time.sleep(delay)print(f"[{now()}|background-task] Finished{task_name}. Elapsed:{time.monotonic()-t0:.3f} seconds"    )defsync_dependency(background_tasks:BackgroundTasks)->Generator[dict[str,Any]]:print(f"[{now()}|sync-dependency] Started")t0=time.monotonic()background_tasks.add_task(background_task,"sync:pre")yield {"timestamp":time.time()}# The following background task is never executedbackground_tasks.add_task(background_task,"sync:post")# This statement is executed correctlyprint(f"[{now()}|sync-dependency] Finished. Elapsed:{time.monotonic()-t0:.3f} seconds"    )asyncdefasync_dependency(background_tasks:BackgroundTasks,)->AsyncGenerator[dict[str,Any]]:print(f"[{now()}|async-dependency] Started")t0=time.monotonic()background_tasks.add_task(background_task,"async:pre")yield {"timestamp":time.time()}# The following background task is never executedbackground_tasks.add_task(background_task,"async:post")# This statement is executed correctlyprint(f"[{now()}|async-dependency] Finished. Elapsed:{time.monotonic()-t0:.3f} seconds"    )@app.get("/sync")defsync(data:Annotated[dict[str,Any],Depends(sync_dependency,scope="function")],background_tasks:BackgroundTasks,)->dict[str,Any]:print(f"[{now()}|GET /sync] Handling request")background_tasks.add_task(background_task,"sync:handler",2)return {"message":"Hello from sync endpoint!","data":data}@app.get("/async")asyncdefasync_endpoint(data:Annotated[dict[str,Any],Depends(async_dependency,scope="function")],background_tasks:BackgroundTasks,)->dict[str,Any]:print(f"[{now()}|GET /async] Handling request")background_tasks.add_task(background_task,"async:handler",2)return {"message":"Hello from async endpoint!","data":data}if__name__=="__main__":importuvicornuvicorn.run(app,host="localhost",port=8000,log_level="info")

On the other hand, I think that there's a chance to improve further the idea of background tasks in a way that could go beyond what Starlette can offer by default, but that's something to explore in the future.

You must be logged in to vote
2 replies
@JP-Ellis
Comment options

It seems you were depending on dependencies exiting early, before the response cycle

I’d respectfully disagree with that. The trivial example above intentionally hides some real-world complexities just to make the underlying issue more apparent.

What I’m actually depending on is the guarantee that a function runs in its entirety. According to thedocs (emphasis mine):

Normally theexit code of dependencies with yield is executed after the response is sent to the client.

But if you know that you won't need to use the dependency after returning from the path operation function, you can useDepends(scope="function") to tell FastAPI that it should close the dependency after the path operation function returns, but before the response is sent.

In our actual use case, we use background tasks both for setup/teardown of contexts and for dispatching events to queues once those contexts are finished (the latter depend on context completion and callbacks within the main function). These events are optional and don’t affect what’s returned in the response; they just require that processing is finished. For example:

deftimed(background_tasks:BackgroundTasks)->Generator[None]:background_tasks.add_task(send_to_queue({"event_started_at":now()}))withtimer:yield# Possibly time-consuming computation herebackground_tasks.add_task(send_to_queue({"processing_time":timer.elapsed}))@app.post("/process")defprocess(_timed:Annotated[None,Depends(timed)]):# Expensive computation# In some cases, we may do: `dependency.some_method(...)`returnresponse

I do realise that code afteryield runs after sending the response, so this could be fixed by switching to something like:

deftimed(background_tasks:BackgroundTasks)->Generator[None]:background_tasks.add_task(send_to_queue({"event_started_at":now()}))withtimer:yield# Possibly time-consuming computation heresend_to_queue({"processing_time":timer.elapsed})# No background task here

But my main concern is that FastAPI treats background tasks added beforeyield completely differently from those added afteryield, which feels very counter-intuitive. Even though this comes from Starlette’s design, FastAPI (intentionally or not) makes it easy for users to run into this subtle pitfall.

In my opinion, FastAPI should ideally do one of:

  • Prevent adding tasks when they won’t be executed (i.e.background_tasks.add_task raises an exception/warning),
  • Raise a warning if any background tasks remain unevaluated,
  • Or (possibly best) execute all queued tasks regardless of where they were added.

Personally, I opted to implement this last approach in my PR since it seemed most in line with what users would expect.

@tiangolo
Comment options

Thanks for the feedback, I'll update all the docs around dependencies to make things more clear.

Does the example I gave you usingDepends(func, scope="function") work for your specific use case?

As the title says "0.118 Regression", I would assume it used to work before, with the previous behavior. And thescope="function" is an opt-in to that same old behavior. So if that doesn't really work, I'm a bit confused about what might be happening that used to work but now doesn't, even when using the same old behavior.


I think it might make sense to provide a better system and interface for background tasks, but for now, I prefer not to touch the default behavior of Starlette in a way that could affect many other unknown use cases.

Answer selected bytiangolo
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Labels
questionQuestion or problem
2 participants
@JP-Ellis@tiangolo

[8]ページ先頭

©2009-2025 Movatter.jp