8000 More predictable timeout-notify with slow sync calls by ollanta · Pull Request #2625 · Kludex/uvicorn · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@ollanta
Copy link
@ollanta ollanta commented May 5, 2025

Summary

The timeout-notify mechanism is less predictable than necessary when slow synchronous calls are run on the event loop thread. This can lead to uvicorn workers being killed by (for example) gunicorn while they're still responsive.

The uvicorn servers main_loop triggers on_tick and sleeps asynchronously for 0.1 seconds. Every 10 ticks on_tick checks if it's time to run cfg.callback_notify. The comments in the code imply that the check is supposed to run once every second, but if there are synchronous calls blocking the asynchronous sleeps from triggering in a timely manner, the actual cadence can be a lot slower.

Of course, you're not supposed to block the event loop like that, but checking the need to notify on each tick (every 0.1+ seconds) improves the situation at a negligible cost.

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

@ollanta ollanta changed the title More predictable timeout More predictable timeout with slow sync calls May 5, 2025
@ollanta ollanta changed the title More predictable timeout with slow sync calls More predictable timeout-notify with slow sync calls May 5, 2025
@Kludex
Copy link
Owner
Kludex commented Oct 12, 2025

Can you reproduce a real example with this actually being a problem? Will this amount of calls be a problem, performance-wise?

@ollanta
Copy link
Author
ollanta commented Oct 31, 2025

Sorry for the delayed response! I can give you an example, how real it is is a matter of definition.

pyproject.toml

[project]
name = "uvicorn-timeout-example"
version = "0.1.0"
dependencies = [
    "fastapi",
    "gunicorn",
    "uvicorn[standard]"
]

gunicorn.conf.py

bind = "0.0.0.0:8000"
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 9
wsgi_app = "app:app"

app.py

import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/slow_sync")
async def slow_sync():
    time.sleep(1)
    return "slept sync"

@app.get("/fast_async")
async def fast_async():
    return "slept async"

demo.sh

#!/bin/bash
uv run gunicorn &
sleep 1
while true
do
    curl 0.0.0.0:8000/slow_sync; echo
    curl 0.0.0.0:8000/fast_async; echo
done

Running demo.sh should reliably cause a worker timeout after 9 seconds, even though the worker is responding throughout. The timeout can be longer, as long as (roughly) 10 x sync response time > timeout. The fast_async is just there for show, it's of no other substance to the example.

Performance-wise, the effect should be negligible, it'll be 9 extra calls to time.time() each second. It could be improved, at a slight cost in code complexity, by moving the counting to separate tasks outside the main_loop. I figured it wasn't really worth it :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0