Back to Blog
Python

Python Gunicorn Production Configuration

python gunicorn production configuration: Learn how to configure Gunicorn for production Python applications: worker types, timeouts, logging, and graceful shutdown.

gunicornproductionwsgideploymentconfiguration
A diagram showing Gunicorn workers handling requests behind a reverse proxy.

Configuring Gunicorn correctly is one of the most important steps before moving a Python web application to production. A default Gunicorn setup works for development, but it rarely matches the traffic, latency, and reliability requirements of a live service. This article covers the key decisions in a python gunicorn production configuration: worker model, worker count, worker class, timeouts, logging, and graceful shutdown.

Understanding Gunicorn's Worker Model

Gunicorn is a pre-fork WSGI server. The master process reads the configuration, binds to a socket, and then forks multiple worker processes. Each worker handles requests independently, and the master is responsible for managing worker lifecycles, restarting workers that die, and handling signals.

The worker model directly affects concurrency and resource usage. A sync worker handles one request at a time. If your application performs blocking I/O—such as database calls, external HTTP requests, or file reads—a sync worker will occupy the process for the entire request duration. For many applications, this is acceptable when combined with enough workers. But if your application has long-running or streaming responses, or if you need to handle many concurrent connections with limited memory, you may need a different worker class.

Gunicorn supports several worker types: sync, gthread, gevent, and tornado. The sync worker is the default and the simplest. The gthread worker uses threads within each process, allowing a single process to handle multiple requests concurrently. The gevent and tornado workers use event loops and greenlets or async I/O, respectively, which can scale to thousands of concurrent connections with fewer processes.

Choosing the Number of Workers

The number of workers determines how many requests your application can handle simultaneously. There is no single correct number, but a common starting point is 2 * CPU cores + 1. This formula is a heuristic, not a guarantee. The actual optimal number depends on your application's CPU usage, memory footprint, and the nature of the work each request performs.

For a CPU-bound application, adding more workers beyond the number of cores can cause context-switching overhead. For an I/O-bound application, more workers may help because workers spend much of their time waiting on external resources. However, each worker consumes memory, so you must balance concurrency against available RAM.

A practical approach is to start with the formula, measure CPU and memory usage under realistic load, and then adjust. Gunicorn also supports the --max-requests and --max-requests-jitter options to recycle workers and avoid memory leaks. Setting these to reasonable values can improve stability in long-running deployments.

gunicorn --workers 4 --max-requests 1000 --max-requests-jitter 100 myapp:app

This command starts four workers and restarts each worker after it has handled between 1000 and 1100 requests. The jitter prevents all workers from restarting at the same time, which could cause a temporary drop in capacity.

Selecting a Worker Class

The default worker class is sync. It is the most reliable and the easiest to reason about, but it is not always the best fit. The choice of worker class should be driven by your application's I/O pattern and the concurrency requirements of your deployment.

If your application is built on a synchronous web framework like Flask or Django and does not use async features, the sync worker is a safe default. It uses one process per request and is predictable under moderate load. When you need to handle more concurrent connections without increasing the number of processes, the gthread worker is a straightforward upgrade. It uses a pool of threads within each worker process, so a single process can serve multiple requests at once.

gunicorn --worker-class gthread --threads 4 --workers 3 myapp:app

This configuration creates three processes, each with four threads, giving you a total of twelve concurrent request slots. The gthread worker is a good choice when your application uses blocking I/O but you want to avoid the memory overhead of many separate processes.

For applications that use asynchronous frameworks like FastAPI with uvicorn or Sanic, you may not need Gunicorn at all, but Gunicorn can still manage worker processes. In that case, you would use a worker class that is compatible with the async framework, such as uvicorn.workers.UvicornWorker. For standard WSGI applications that need high concurrency with low memory, gevent can be effective, but it requires your code to be gevent-friendly. Monkey-patching standard library modules can have subtle side effects, so test thoroughly before adopting it.

The table below summarizes the common worker classes and their typical use cases.

Worker ClassConcurrency ModelBest Fit
syncOne request per processSimple synchronous apps, low concurrency
gthreadThreads per processBlocking I/O, moderate concurrency
geventGreenlets, event loopHigh concurrency, I/O-bound synchronous code
uvicornAsync event loopASGI frameworks like FastAPI

Setting Timeouts and Keep-Alive

Gunicorn has several timeout-related settings that protect against hung workers and slow clients. The timeout option is the maximum number of seconds a worker can take to respond to a request before it is killed and restarted. The default is 30 seconds. If your application performs long-running tasks, you must raise this value, but doing so also means a stuck worker will occupy resources longer. A better approach is to move long tasks to a background job queue and keep the request path fast.

The graceful_timeout setting controls how long a worker is given to finish its current request after a restart or shutdown signal. The default is 30 seconds. If a worker does not exit within this period, it is forcibly terminated. Setting this too low can drop in-flight requests; setting it too high can delay restarts.

keepalive controls how long a worker waits for a new request on a persistent connection. The default is 2 seconds. If you are behind a reverse proxy like Nginx, the proxy may keep connections open to Gunicorn, so you can set this to a low value to free up workers sooner. For direct client connections, a higher value may reduce connection setup overhead.

# gunicorn.conf.py timeout = 30 graceful_timeout = 30 keepalive = 2

These settings are part of a configuration file, which is a cleaner way to manage production settings than a long command line.

Binding to a Socket and Reverse Proxy

In production, Gunicorn rarely listens directly on a public port. Instead, it binds to a local socket, and a reverse proxy such as Nginx or Caddy handles client connections, TLS termination, and static file serving. This separation improves security and allows the proxy to buffer slow clients.

You can bind Gunicorn to a TCP port on localhost or to a Unix domain socket. Unix sockets are faster because they avoid network stack overhead, and they are not accessible from outside the machine. The bind option accepts either format.

# gunicorn.conf.py bind = "unix:/run/gunicorn.sock"

Or on the command line:

gunicorn --bind unix:/run/gunicorn.sock myapp:app

When using a Unix socket, ensure the socket file is writable by the user that the reverse proxy runs as. This often requires setting the socket's permissions and the directory's ownership. A common pattern is to create a dedicated system user for Gunicorn and add the proxy user to the same group.

If you bind to a TCP port, use 127.0.0.1:8000 instead of 0.0.0.0:8000 to avoid exposing the port externally. The reverse proxy then forwards requests to that address.

Logging and Error Handling

Production debugging depends on good logs. Gunicorn separates access logs and error logs. The accesslog option writes one line per request, including the client IP, request line, status code, and response size. The errorlog captures Gunicorn's own messages, worker errors, and any output from your application that goes to stderr.

By default, access logs are disabled. In production, you should enable them, but be mindful of the volume. Logging every request can generate a large amount of data, so you may want to aggregate logs in a central system. The loglevel option controls the verbosity of the error log; info is a reasonable default, while debug is useful during troubleshooting.

# gunicorn.conf.py accesslog = "/var/log/gunicorn/access.log" errorlog = "/var/log/gunicorn/error.log" loglevel = "info"

Gunicorn also has a capture_output option. When set to True, it redirects stdout and stderr from your application to the error log. This is helpful for catching stray print() statements or unhandled exceptions that would otherwise go to the console and be lost.

Graceful Shutdown and Reload

When you deploy a new version, you want to stop old workers without dropping in-flight requests. Gunicorn's TERM signal triggers a graceful shutdown: the master stops accepting new connections, tells each worker to finish its current request, and then terminates the worker. The graceful_timeout sets the maximum time a worker can take to finish before being killed.

For zero-downtime deployments, you typically run multiple workers and use a rolling restart. You can send a HUP signal to the master to reload the configuration and gracefully restart workers. This is useful when you change code or configuration without fully stopping the server.

kill -HUP $(cat /run/gunicorn.pid)

If you are using a process manager like systemd, you can define the restart behavior and send the appropriate signals. A common systemd unit file for Gunicorn includes ExecStart with the full command and ExecReload with the HUP signal.

Using Environment Variables and a Config File

Hard-coding configuration values in a command line is fragile. A Python configuration file gives you the full power of the Python language, including the ability to read environment variables. This is especially important for secrets and environment-specific settings.

# gunicorn.conf.py import os bind = os.getenv("GUNICORN_BIND", "unix:/run/gunicorn.sock") workers = int(os.getenv("GUNICORN_WORKERS", "4")) worker_class = os.getenv("GUNICORN_WORKER_CLASS", "sync") timeout = int(os.getenv("GUNICORN_TIMEOUT", "30"))

Then start Gunicorn with -c gunicorn.conf.py. This approach keeps the configuration in version control and makes it easy to adjust settings per environment without editing files on the server.

When you use environment variables, remember that Gunicorn itself does not automatically load a .env file. You need to load it yourself, either in the config file or through your process manager. For example, systemd can set environment variables via EnvironmentFile=. Docker Compose can pass them via the environment key.

A well-structured production configuration also considers the worker_tmp_dir setting. Gunicorn uses a temporary directory for worker heartbeats. On systems with a small /tmp partition, you can point this to a dedicated location to avoid filling up the disk.

worker_tmp_dir = "/dev/shm"

Using /dev/shm puts the heartbeat files in memory, which is faster and avoids disk writes. This is a small but useful detail for high-traffic deployments.

Finally, always verify that your Gunicorn configuration matches the rest of your infrastructure. Check that the reverse proxy forwards the correct headers, especially X-Forwarded-For and X-Forwarded-Proto, so your application sees the real client IP and scheme. Gunicorn does not parse these headers by default; you need to configure your proxy and possibly set forwarded-allow-ips to trust the proxy's IP address.

forwarded_allow_ips = "127.0.0.1"

This setting ensures that Gunicorn only trusts forwarded headers from the reverse proxy, preventing clients from spoofing their IP addresses. Without it, a client could send a fake X-Forwarded-For header and appear to come from a different address.

A production-ready Gunicorn configuration is not a single set of values; it is a set of deliberate choices based on your application's behavior and your infrastructure. Start with the defaults, measure under load, and adjust the worker count, worker class, and timeouts to match your real traffic patterns. The settings described here give you a solid foundation for a reliable and maintainable deployment.

python gunicorn production configuration: Practical Usage an | RYUSLOG DEV