Gunicorn
Simple and easy-to-use WSGI HTTP server for Python applications. Most documented Python web server. Features easy configuration and management.
Application Server
Gunicorn
Overview
Gunicorn (Green Unicorn) is a Python WSGI HTTP Server for UNIX that serves as a powerful and efficient gateway between web applications and the web server. Known for its simplicity and ease of configuration, Gunicorn is designed specifically for Python web applications that follow the Web Server Gateway Interface (WSGI) standard. It provides robust process management, worker process forking, and excellent performance characteristics that make it the preferred choice for deploying Python web applications in production environments. Gunicorn's pre-fork worker model ensures stability and efficient resource utilization while supporting various worker types for different application requirements.
Details
Gunicorn 2025 edition continues its evolution as the most widely adopted WSGI server in the Python ecosystem, trusted by countless production deployments worldwide. Built specifically for UNIX-like systems, Gunicorn leverages the operating system's process management capabilities to provide exceptional reliability and performance. The server implements a pre-fork worker model where a master process manages multiple worker processes, ensuring application isolation and fault tolerance. Gunicorn supports multiple worker classes including synchronous workers for traditional applications, asynchronous workers for high-concurrency scenarios using gevent or eventlet, and pseudo-threads for CPU-bound tasks. Its configuration flexibility allows fine-tuning for specific deployment scenarios, from simple development setups to complex enterprise environments requiring high availability and scalability.
Key Features
- Pre-fork Worker Model: Master process manages multiple worker processes for stability and isolation
- Multiple Worker Classes: Support for sync, async (gevent/eventlet), and gthread workers
- Zero Downtime Deployments: Graceful worker recycling and seamless code updates
- Extensive Configuration: Python-based configuration files with comprehensive options
- Process Management: Built-in support for worker monitoring, recycling, and health checks
- Integration Ready: Seamless integration with popular web frameworks and reverse proxies
Advantages and Disadvantages
Advantages
- Simple and straightforward configuration with minimal setup requirements
- Excellent stability through process isolation and automatic worker recycling
- High performance with efficient memory usage and request handling
- Comprehensive documentation and active community support
- Seamless integration with popular Python frameworks like Django, Flask, and FastAPI
- Production-proven reliability with extensive deployment options
- Built-in support for various deployment scenarios and monitoring tools
Disadvantages
- UNIX-only deployment limiting Windows development workflows
- Synchronous workers may not be optimal for highly concurrent I/O-bound applications
- Configuration complexity can increase with advanced deployment requirements
- Limited built-in static file serving capabilities requiring additional web server
- Worker process overhead may be significant for lightweight applications
- Requires understanding of WSGI concepts for optimal configuration
Reference Links
Configuration Examples
Installation and Basic Setup
# Install Gunicorn via pip
pip install gunicorn
# Install with async worker support
pip install gunicorn[gevent]
pip install gunicorn[eventlet]
# Install additional dependencies for production
pip install setproctitle # For process naming
pip install psutil # For system monitoring
# Create a basic requirements.txt
echo "gunicorn>=21.0.0" >> requirements.txt
echo "setproctitle" >> requirements.txt
Simple WSGI Application Example
# app.py - Simple WSGI application
def application(environ, start_response):
"""Simple WSGI application for testing"""
# Get request information
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
query = environ.get('QUERY_STRING', '')
# Prepare response
if path == '/':
response_body = b'Hello, World! This is served by Gunicorn.\n'
status = '200 OK'
elif path == '/health':
response_body = b'OK\n'
status = '200 OK'
elif path == '/info':
info = f"Method: {method}\nPath: {path}\nQuery: {query}\n"
response_body = info.encode('utf-8')
status = '200 OK'
else:
response_body = b'Not Found\n'
status = '404 Not Found'
response_headers = [
('Content-Type', 'text/plain'),
('Content-Length', str(len(response_body)))
]
start_response(status, response_headers)
return [response_body]
# For testing with different frameworks
# Flask example
from flask import Flask
flask_app = Flask(__name__)
@flask_app.route('/')
def hello():
return 'Hello from Flask via Gunicorn!'
@flask_app.route('/health')
def health():
return 'OK'
# For deployment, use: flask_app as the WSGI application
Basic Gunicorn Usage
# Basic usage with simple WSGI app
gunicorn app:application
# Specify host and port
gunicorn --bind 0.0.0.0:8000 app:application
# Set number of workers
gunicorn --workers 4 app:application
# Use with Flask application
gunicorn --bind 0.0.0.0:8000 app:flask_app
# Development mode with auto-reload
gunicorn --reload --bind 127.0.0.1:8000 app:application
# Background execution
gunicorn --bind 0.0.0.0:8000 --daemon app:application
# Specify worker class
gunicorn --worker-class gevent --workers 4 app:application
# Combined options
gunicorn --bind 0.0.0.0:8000 --workers 4 --worker-class sync --timeout 30 app:application
Comprehensive Configuration File
# gunicorn.conf.py - Production configuration
import multiprocessing
import os
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 60
max_requests = 1000
max_requests_jitter = 50
# Restart workers after this many requests (with random jitter)
# to prevent memory leaks
preload_app = True
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
# Application
# WSGI module and variable name
wsgi_module = "app:application"
# Environment
raw_env = [
'DJANGO_SETTINGS_MODULE=myproject.settings.production',
'SECRET_KEY=your-secret-key-here',
'DATABASE_URL=postgresql://user:pass@localhost/dbname',
]
# Process naming
proc_name = 'myapp'
default_proc_name = 'myapp'
# Logging
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process ID file
pidfile = '/var/run/gunicorn.pid'
# User and group to run as
user = 'www-data'
group = 'www-data'
# Temporary directory for worker files
tmp_upload_dir = '/tmp'
worker_tmp_dir = '/dev/shm' # Use RAM-backed filesystem
# SSL Configuration (if needed)
# keyfile = '/path/to/ssl/key.pem'
# certfile = '/path/to/ssl/cert.pem'
# ssl_version = ssl.PROTOCOL_TLS
# ciphers = 'TLSv1'
# Worker process management
worker_class = "sync" # Options: sync, gevent, eventlet, tornado, gthread
# For async workers:
if worker_class in ['gevent', 'eventlet']:
workers = multiprocessing.cpu_count() * 2 + 1
worker_connections = 1000
# For thread workers:
if worker_class == 'gthread':
workers = multiprocessing.cpu_count() * 2 + 1
threads = 2
# Graceful timeout for worker shutdown
graceful_timeout = 30
# Keep workers alive during code reload
reload = False
reload_engine = 'auto'
reload_extra_files = []
# Advanced options
enable_stdio_inheritance = False
pythonpath = '/app'
chdir = '/app'
# Server hooks
def on_starting(server):
"""Called just before the master process is initialized."""
server.log.info("Starting Gunicorn server")
def on_reload(server):
"""Called to recycle workers during a reload via SIGHUP."""
server.log.info("Reloading Gunicorn server")
def when_ready(server):
"""Called just after the server is started."""
server.log.info("Gunicorn server is ready. Listening on: %s", server.address)
def pre_fork(server, worker):
"""Called just before a worker is forked."""
server.log.info("Worker %s forked", worker.pid)
def post_fork(server, worker):
"""Called just after a worker has been forked."""
server.log.info("Worker %s ready", worker.pid)
def post_worker_init(worker):
"""Called just after a worker has initialized the application."""
worker.log.info("Worker %s initialized application", worker.pid)
def worker_int(worker):
"""Called just after a worker exited on SIGINT or SIGQUIT."""
worker.log.info("Worker %s received INT/QUIT signal", worker.pid)
def worker_abort(worker):
"""Called when a worker received the SIGABRT signal."""
worker.log.info("Worker %s received ABORT signal", worker.pid)
def pre_exec(server):
"""Called just before a new master process is forked."""
server.log.info("Forking new master process")
def pre_request(worker, req):
"""Called just before a worker processes the request."""
worker.log.debug("%s %s", req.method, req.path)
def post_request(worker, req, environ, resp):
"""Called after a worker processes the request."""
worker.log.debug("Completed %s %s - %s", req.method, req.path, resp.status_code)
def child_exit(server, worker):
"""Called just after a worker has been exited, in the master process."""
server.log.info("Worker %s exited", worker.pid)
def worker_exit(server, worker):
"""Called just after a worker has been exited, in the worker process."""
worker.log.info("Worker %s exiting", worker.pid)
def nworkers_changed(server, new_value, old_value):
"""Called just after num_workers has been changed."""
server.log.info("Changed workers from %s to %s", old_value, new_value)
def on_exit(server):
"""Called just before exiting Gunicorn."""
server.log.info("Shutting down Gunicorn server")
Different Worker Class Configurations
# sync_workers.conf.py - Synchronous workers (default)
workers = 4
worker_class = "sync"
timeout = 30
keepalive = 2
# For CPU-bound applications
max_requests = 1000
max_requests_jitter = 100
# gevent_workers.conf.py - Asynchronous workers with gevent
import multiprocessing
workers = multiprocessing.cpu_count() + 1
worker_class = "gevent"
worker_connections = 1000
# Install: pip install gevent
# Good for I/O-bound applications with many concurrent connections
# eventlet_workers.conf.py - Asynchronous workers with eventlet
import multiprocessing
workers = multiprocessing.cpu_count() + 1
worker_class = "eventlet"
worker_connections = 1000
# Install: pip install eventlet
# Alternative to gevent for async I/O
# thread_workers.conf.py - Thread-based workers
import multiprocessing
workers = multiprocessing.cpu_count()
worker_class = "gthread"
threads = 2
thread_max_requests = 1000
# Good balance between sync and async for mixed workloads
# tornado_workers.conf.py - Tornado workers
workers = 1 # Tornado handles concurrency internally
worker_class = "tornado"
# Install: pip install tornado
# For applications using Tornado framework
Production Deployment Configurations
#!/bin/bash
# start_gunicorn.sh - Production startup script
# Environment variables
export DJANGO_SETTINGS_MODULE=myproject.settings.production
export PYTHONPATH=/app
export GUNICORN_CMD_ARGS="--bind=0.0.0.0:8000 --workers=4"
# Create necessary directories
mkdir -p /var/log/gunicorn
mkdir -p /var/run
# Start Gunicorn with configuration file
exec gunicorn myproject.wsgi:application \
--config /app/gunicorn.conf.py \
--pid /var/run/gunicorn.pid \
--daemon
# Alternative: Using environment variable configuration
GUNICORN_CMD_ARGS="--bind=0.0.0.0:8000 --workers=4 --timeout=30" \
gunicorn myproject.wsgi:application
Systemd Service Configuration
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn instance to serve myproject
Requires=gunicorn.socket
After=network.target
[Service]
Type=notify
NotifyAccess=main
User=www-data
Group=www-data
RuntimeDirectory=gunicorn
WorkingDirectory=/app
Environment="PATH=/app/venv/bin"
ExecStart=/app/venv/bin/gunicorn --config /app/gunicorn.conf.py myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/gunicorn.socket
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/gunicorn.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target
# Commands to manage the service
# sudo systemctl enable --now gunicorn.socket
# sudo systemctl start gunicorn.service
# sudo systemctl reload gunicorn.service
# sudo systemctl status gunicorn.service
Nginx Reverse Proxy Configuration
# /etc/nginx/sites-available/myproject
upstream app_server {
# Unix socket
server unix:/run/gunicorn.sock fail_timeout=0;
# TCP socket alternative
# server 127.0.0.1:8000 fail_timeout=0;
# Multiple servers for load balancing
# server 127.0.0.1:8000 weight=1 fail_timeout=0;
# server 127.0.0.1:8001 weight=1 fail_timeout=0;
}
server {
listen 80;
server_name example.com www.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Basic settings
client_max_body_size 4G;
keepalive_timeout 5;
# Static files
location /static/ {
alias /app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /app/media/;
expires 1y;
add_header Cache-Control "public";
}
# Application
location / {
# Headers for proper client IP detection
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
# Disable buffering for streaming responses
proxy_buffering off;
proxy_redirect off;
# Don't wait for backend if client disconnects
proxy_ignore_client_abort on;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Pass to Gunicorn
proxy_pass http://app_server;
}
# Health check endpoint
location /health/ {
access_log off;
proxy_pass http://app_server;
}
}
Monitoring and Health Checks
# health_check.py - Application health monitoring
import requests
import sys
import time
import subprocess
def check_gunicorn_process():
"""Check if Gunicorn master process is running"""
try:
with open('/var/run/gunicorn.pid', 'r') as f:
pid = int(f.read().strip())
# Check if process exists
subprocess.check_call(['kill', '-0', str(pid)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return True
except (FileNotFoundError, subprocess.CalledProcessError, ValueError):
return False
def check_application_health():
"""Check if application responds correctly"""
try:
response = requests.get('http://localhost:8000/health', timeout=5)
return response.status_code == 200
except requests.RequestException:
return False
def check_worker_count():
"""Check number of active workers"""
try:
result = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
worker_count = len([line for line in result.stdout.split('\n')
if 'gunicorn: worker' in line])
return worker_count
except subprocess.SubprocessError:
return 0
def main():
checks = {
'Process Running': check_gunicorn_process(),
'Application Health': check_application_health(),
'Worker Count': check_worker_count()
}
print("Gunicorn Health Check Results:")
print("-" * 40)
all_healthy = True
for check_name, result in checks.items():
if check_name == 'Worker Count':
status = f"{result} workers"
if result == 0:
all_healthy = False
else:
status = "✓ PASS" if result else "✗ FAIL"
if not result:
all_healthy = False
print(f"{check_name}: {status}")
print("-" * 40)
print(f"Overall Status: {'✓ HEALTHY' if all_healthy else '✗ UNHEALTHY'}")
return 0 if all_healthy else 1
if __name__ == '__main__':
sys.exit(main())
# Usage: python health_check.py
Container Deployment (Docker)
# Dockerfile for Gunicorn application
FROM python:3.11-slim
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PATH="/app/venv/bin:$PATH"
# Create app user
RUN groupadd -r app && useradd -r -g app app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Create and set working directory
WORKDIR /app
# Create virtual environment
RUN python -m venv /app/venv
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Change ownership to app user
RUN chown -R app:app /app
# Switch to app user
USER app
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Default command
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:application"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
- static_volume:/app/static
- media_volume:/app/media
environment:
- DEBUG=False
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_volume:/app/static
- media_volume:/app/media
depends_on:
- web
restart: unless-stopped
volumes:
postgres_data:
static_volume:
media_volume: