Ability to register a callback on "After-Startup" event #14967
-
First Check
Commit to Help
Example Codefrom contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# app is not yet able to serve http requests
yield
app = FastAPI(lifespan=lifespan)DescriptionHello, First of all, thank you for maintaining this amazing project : ) I tried to find information on how to register a callback on post-startup event (moment when FastAPI is ready to handle HTTP requests), but in most threads, documentation and AI suggestions it all comes to usage of _lifespan feature. However _lifespan happens before application is ready to serve and even running there some async code without awaiting (or something similar) does not guarantee that execution will happen after HTTP serving is started. I was thinking about usage of some scheduling library, but I can predict that starting/stopping process of scheduled jobs will be possible only during _lifespan events, what leads to the same situation. Is there any way to register a callback, something like a background task which happens once application is serving, in a easy (or at least a proper for FastAPI) way? Operating SystemLinux Operating System DetailsDocker / Python-3.11-bookworm FastAPI Version0.111.1 Pydantic Version2.10.6 Python Version3.11 Additional ContextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments
-
|
Hey! Ran into this exact same limitation a while ago. The tricky part is that ASGI itself doesn't actually have a native "server is actively listening" event, which is why lifespan blocks until you yield. If you just need to kick off a background process right around startup without blocking the server from handling requests, the easiest way is to just drop an asyncio.create_task right before the yield. Since you don't await it, FastAPI will continue starting up: import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
async def my_background_job():
# Optional: give the server a second to bind to the port
await asyncio.sleep(1)
print("Running post-startup!")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Fire and forget the task
task = asyncio.create_task(my_background_job())
yield
# Clean up gracefully on shutdown
task.cancel()
app = FastAPI(lifespan=lifespan)If you strictly need to guarantee the socket is bound and the server is accepting HTTP traffic before your callback runs, you have to step outside FastAPI and handle it at the server level (e.g., Uvicorn). You can run Uvicorn programmatically and poll server.started: import asyncio
import uvicorn
from fastapi import FastAPI
app = FastAPI()
async def main():
config = uvicorn.Config("main:app", port=8000)
server = uvicorn.Server(config)
# Start server without blocking the main thread
server_task = asyncio.create_task(server.serve())
# Wait until Uvicorn explicitly flags that it's ready
while not server.started:
await asyncio.sleep(0.1)
print("Server is 100% ready and accepting requests now!")
# ---> Do your post-startup stuff here <---
await server_task
if __name__ == "__main__":
asyncio.run(main())Hope this helps! |
Beta Was this translation helpful? Give feedback.
-
|
@shreyansh96 Hi, and thank you for your response, I think server usage could be the best option for us then. Once tested will return back here, write on outcomes and close the thread 👍 |
Beta Was this translation helpful? Give feedback.
-
|
I don’t think FastAPI currently has a separate “after the server is already accepting HTTP requests” hook. The @asynccontextmanager
async def lifespan(app: FastAPI):
# startup phase
yield
# shutdown phaseThe important distinction is that FastAPI controls the application lifecycle, but the moment when the socket is bound and the server starts accepting requests is controlled by the ASGI server, such as Uvicorn or Hypercorn. So if you need to run something after the app is “ready”, the usual options are: @asynccontextmanager
async def lifespan(app: FastAPI):
# initialize resources here
async def post_startup_task():
# optional small delay if you need the server to be reachable externally
# await asyncio.sleep(1)
...
task = asyncio.create_task(post_startup_task())
try:
yield
finally:
task.cancel()But this still does not strictly mean “after the HTTP server has started accepting connections”; it only means “scheduled during app startup and allowed to run after control returns to the event loop”. If you need a strict guarantee that the service is reachable over HTTP, I would usually handle that outside the FastAPI app, for example:
So I think |
Beta Was this translation helpful? Give feedback.
-
|
Thanks to everyone, I think after trying different options, indeed, I has to accept that even having small timeout in @asynccontextmanager it's not 100% guaranteed that code will run after application is listening for HTTP requests. For short term solution this is what I decided to use: But for long term it feels like external coordination + docker/k8s healthchecks is the best to trigger that workload. Why - because in our case application starts and calls sidecar "hey, I'm here, feel free to reach me out with requests" and in reality it does it faster than it can actually handle incoming requests via HTTP pipeline. In our case it works still due re-try mechanism, but again, long term the best would be for sidecar to simply observe application availability and health. There was also an idea of self-pinging healthcheck endpoint in a loop by the application, but for me it feels bit unnatural comparing to external coordination, plus in our architecture it is more practical since we also have sidecars for applications. |
Beta Was this translation helpful? Give feedback.
Hey! Ran into this exact same limitation a while ago. The tricky part is that ASGI itself doesn't actually have a native "server is actively listening" event, which is why lifespan blocks until you yield.
If you just need to kick off a background process right around startup without blocking the server from handling requests, the easiest way is to just drop an asyncio.create_task right before the yield. Since you don't await it, FastAPI will continue starting up: