Gunicorn & Flask

A quick setup demo and introduction to multithreading

·

3 min read

This blog will demo running Flask with Gunicorn and how to enable multiple workers.

First, let's start with this simple Flask app called app.py: which just has two endpoints:

  1. /healthcheck endpoint which returns "ok".
  2. /sleep endpoint which sleeps for 10 seconds and then returns "ok".
from flask import Flask, jsonify
import sys
import time
app = Flask(__name__)

@app.route("/healthcheck")
def healthcheck():
    return jsonify({"status": "ok"})

@app.route("/sleep")
def sleep():
    time.sleep(10)
    return jsonify({"status": "ok"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, port=8888)

I run this app via the command line by running:

$ python3 app.py

And to hit the /healthcheck endpoint, I just run $ curl localhost:8888/healthcheck and receive the output

{
  "status": "ok"
}

Great! If you look at your logs you will see this:

127.0.0.1 - - [21/May/2022 18:41:09] "GET /healthcheck HTTP/1.1" 200 -

Now we can add Gunicorn and run it with Flask by adding the following Gunicorn config file, which I am calling gunicorn_config.py, to our repo.

import multiprocessing

bind = "127.0.0.1:8000"
workers = 1
worker_class = "sync"

errorlog = "-"
loglevel = "info"
accesslog = "-"

The file basically says I'll be running my application on port 8000, with 1 worker running synchronously plus some commands to display logs. To run this I just run $ gunicorn --config gunicorn_config.py app:app telling gunicorn the path to my config file and the app file to run. When I run this, I first get the following log lines:

[2022-05-21 18:55:13 -0500] [41471] [INFO] Starting gunicorn 20.1.0
[2022-05-21 18:55:13 -0500] [41471] [INFO] Listening at: http://127.0.0.1:8000 (41471)
[2022-05-21 18:55:13 -0500] [41471] [INFO] Using worker: sync
[2022-05-21 18:55:13 -0500] [41472] [INFO] Booting worker with pid: 41472

In the logs, there is the single worker with process id of 41472. Next, I am going to run two curls, near simultaneously

  1. curl localhost:8000/sleep
  2. (in a new terminal window) curl localhost:8000/healthcheck.

The logs show:

127.0.0.1 - - [21/May/2022:18:57:49 -0500] "GET /sleep HTTP/1.1" 200 16 "-" "curl/7.77.0"
127.0.0.1 - - [21/May/2022:18:57:49 -0500] "GET /healthcheck HTTP/1.1" 200 16 "-" "curl/7.77.0"
`

Because we only had one worker, we had to wait 10 seconds for /sleep to complete before the /healthcheck endpoint could run.

But if I increase the number of workers to 2 and run the curls in the exact same order, I get:

[2022-05-21 19:00:02 -0500] [41588] [INFO] Starting gunicorn 20.1.0
[2022-05-21 19:00:02 -0500] [41588] [INFO] Listening at: http://127.0.0.1:8000 (41588)
[2022-05-21 19:00:02 -0500] [41588] [INFO] Using worker: sync
[2022-05-21 19:00:02 -0500] [41589] [INFO] Booting worker with pid: 41589
[2022-05-21 19:00:02 -0500] [41590] [INFO] Booting worker with pid: 41590
127.0.0.1 - - [21/May/2022:19:00:06 -0500] "GET /healthcheck HTTP/1.1" 200 16 "-" "curl/7.77.0"
127.0.0.1 - - [21/May/2022:19:00:15 -0500] "GET /sleep HTTP/1.1" 200 16 "-" "curl/7.77.0"
`

Firstly, we have two workers with the process ids 41590 and 41589 . Secondly, /healthcheck returned first and then the /sleep returned , even though that was not the order I ran them in. Also you can see that/sleep returned nearly 10 seconds after our /healthcheck did!

Now you can set up Gunicorn with Flask with multiple workers!