Correct settings for Uvicorn + Gunicorn for production

We need fast async support and also worker restarts after N requests, and so right now we are using Gunicorn with the Uvicorn worker according to their recommendation. When we run Uvicorn with a max request limit, the workers don’t restart and that is an issue.

On the sanic documentation, you recommend when deploying with gunicorn that we use the sanic worker sanic.worker.GunicornWorker https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-gunicorn

But on the uvicorn docs, they recommend using their worker uvicorn.workers.UvicornWorker https://www.uvicorn.org/deployment/

I know that you recommend using the sanic server directly for the best performance — but when we tested in production, it suffers from some issues, mainly that I cannot seem to restart the workers. This is an important requirement for us to deal with unexpected exceptions that appear to lead to memory leaks in some instances.

So I am wondering what config I should use that will meet my requirements:

  • worker supports restarts after N requests
  • most performant running async code

And also, if I use gunicorn, which worker should I in fact be using that will satisfy my requirements?

sanic can be deployed through it’s own server

sanic app:sanic_app

it’s just the same as

app.run()

Since sanic was compliant to asgi, you can deploy sanic through uvicorn:

uvicorn app:sanic_app

But uvicorn server and sanic server neither support restart workers, so sanic support GunicornWorker for gunicorn server, and uvicorn has UvicornWorker.

To meet you requirment, you can use both sanic_worker + sanic_app or uvcorn_worker + sanic_asgi_app through gunicorn server, it’s both asynchronous. and to achieve restart workers after N requests, just set gunicorn server --max-requests.

so the command is like this:

gunicorn app:sanic_app --worker-class 'uvicorn.workers.UvicornWorker' --max-requests N

or

gunicorn app:sanic_app --worker-class 'sanic.worker.GunicornWorker' --max-requests N

Thanks for your reply. So in terms of performance, which is better:

  • uvicorn.workers.UvicornWorker
  • sanic.worker.GunicornWorker

I had assume that I should use the sanic worker but what exactly are the differences in choosing the two? Using the sanic worker with Gunicorn was my original setup, until it was recommended to me that I look into using asgi by using uvicorn.

My code doesn’t have memory leaks but I have had incidents where something was left unchecked and so the max request gives a safety net besides that it seems to work fairly well.

Thank you!

I did a dummy benchmark for both workers.

here is the result:

sanic_worker:

~/Desktop via experiment
avg time is 6.114210337400436ms

~/Desktop via experiment
➜ python bench.py
avg time is 5.590550005435944ms

~/Desktop via experiment
➜ python bench.py
avg time is 6.113555729389191ms

~/Desktop via experiment
➜ python bench.py
avg time is 5.972404107451439ms

~/Desktop via experiment
➜ python bench.py
avg time is 6.191891059279442ms

uvicorn_worker:

~/Desktop via sanic
➜ python bench.py
avg time is 7.635346502065659ms

~/Desktop via sanic
➜ python bench.py
avg time is 7.063340097665787ms

~/Desktop via sanic
➜ python bench.py
avg time is 7.300252094864845ms

~/Desktop via sanic
➜ python bench.py
avg time is 7.094317674636841ms

~/Desktop via sanic
➜ python bench.py
avg time is 6.741353422403336ms

the benchmark script is like this:

from multiprocessing import Pool, Queue
from queue import Empty
from time import time

import httpx

q = Queue()


def bench_for_request(port):
    # sleep(4)
    with httpx.Client() as client:
        n = 0
        for _ in range(100):
            t1 = time()
            res = client.get(f"http://localhost:{port}/index")
            if not res.status_code == 200:
                print("fail")

            delta = time() - t1
            n += delta

        return n


def main(port):
    score = bench_for_request(port)
    q.put(score)


def score():
    score = 0
    pool = Pool(32)

    for _ in range(32):
        pool.apply_async(func=main, args=(8000, ))

    pool.close()
    pool.join()

    while True:
        try:
            a = q.get(block=False)
            score += a
        except Empty:
            break

    print(f"avg time is {score * 1000/ 3200}ms")


if __name__ == "__main__":
    score()

It looks like sanic worker is faster than unvicorn worker.

I maybe @ahopkins can help us answer the question.

Thank you for your reply and benchmarks!

For the record, the sanic cli callable does not currently support this declaration. All parts of the module need to be . seperated. Something that probably needs a PR.

sanic app.sanic_app

I would shy away from the Sanic worker personally. I did some benchmarks of my own a while back with much different results. I think that is because @ZinkLu uses a sync client, and therefore not really doing simultaneous requests.

The Sanic worker is there largely for compat, and backwards support. But, except for a rare edge case, I would not suggest using it. Uvicorn worker is much better option if you need to use gunicorn.

2 Likes

Ok. If that’s the case then maybe someone should update the docs? The docs is saying to use the sanic worker when using gurnicorn, and then we had a discussion a month ago where you suggested uvicorn, but that’s why I posted this question because the docs are not in sync with exactly what I should be using.

https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-gunicorn

1 Like