# Technical Documentation: benoitc/gunicorn

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/benoitc/gunicorn)

### Overview (`README.md`)

# Gunicorn

<p align="center">
  <strong>Gunicorn is maintained by volunteers. If it powers your production, please consider supporting us:</strong><br>
  <a href="https://github.com/sponsors/benoitc"><img src="https://img.shields.io/badge/GitHub_Sponsors-❤-ea4aaa?style=for-the-badge&logo=github" alt="GitHub Sponsors"></a>
  <a href="https://checkout.revolut.com/pay/74cf6fe1-358e-4880-b800-3f0936ce94be"><img src="https://img.shields.io/badge/Revolut-Donate-191c20?style=for-the-badge" alt="Revolut"></a>
</p>

[](https://pypi.python.org/pypi/gunicorn)
[](https://pypi.python.org/pypi/gunicorn)
[](https://github.com/benoitc/gunicorn/actions/workflows/tox.yml)

Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork
worker model ported from Ruby's [Unicorn](https://bogomips.org/unicorn/) project. The Gunicorn server is broadly
compatible with various web frameworks, simply implemented, light on server
resource usage, and fairly speedy.

**New in v25**: Per-app worker allocation for dirty arbiters, HTTP/2 support (beta)!

## Quick Start

```bash
pip install gunicorn
gunicorn myapp:app --workers 4
```

For ASGI applications (FastAPI, Starlette):

```bash
gunicorn myapp:app --worker-class asgi
```

## Features

- WSGI support for Django, Flask, Pyramid, and any WSGI framework
- **ASGI support** for FastAPI, Starlette, Quart
- **HTTP/2 support** (beta) with multiplexed streams
- **Dirty Arbiters** (beta) for heavy workloads (ML models, long-running tasks)
- uWSGI binary protocol for nginx integration
- Multiple worker types: sync, gthread, gevent, asgi
- Graceful worker process management
- Compatible with Python 3.10+

## Documentation

Full documentation at https://gunicorn.org

- [Quickstart](https://gunicorn.org/quickstart/)
- [Configuration](https://gunicorn.org/configure/)
- [Deployment](https://gunicorn.org/deploy/)
- [Settings Reference](https://gunicorn.org/reference/settings/)

## Community

- Report bugs on [GitHub Issues](https://github.com/benoitc/gunicorn/issues)
- [Chat with us in your browser](https://web.libera.chat/?channels=%23gunicorn) (`#gunicorn` on Libera.Chat, no account needed)
- See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines

## Support

Powering Python apps since 2010. Support continued development.

[](https://gunicorn.org/sponsor/)

### Sponsors

<a href="https://enki-multimedia.eu"><img src="docs/content/assets/enki-multimedia.svg" alt="Enki Multimedia" height="50" /></a>

## License

Gunicorn is released under the MIT License. See the [LICENSE](https://github.com/benoitc/gunicorn/blob/master/LICENSE) file for details.

---

### Overview (`docs/README.md`)

# Generate Documentation

## Requirements

Install the documentation dependencies with:

```bash
pip install -r requirements_dev.txt
```

This provides MkDocs with the Material theme and supporting plugins.

## Build static HTML

```bash
mkdocs build
```

The rendered site is emitted into the `site/` directory.

## Preview locally

```bash
mkdocs serve
```

This serves the documentation at http://127.0.0.1:8000/ with live reload.

---

### Overview (`examples/celery_alternative/README.md`)

# Celery Alternative Example

This example demonstrates how to replace Celery with Gunicorn's **dirty arbiters** for background task processing, using **async ASGI** for non-blocking HTTP handling.

## Why Use This Instead of Celery?

### The Problem with Celery

Celery requires:
- An external message broker (Redis or RabbitMQ)
- Separate worker processes (`celery -A app worker`)
- Stateless workers that reload models/connections on every task
- Polling or WebSockets for progress updates

### What Dirty Arbiters Provide

| Feature | Celery | Dirty Arbiters |
|---------|--------|----------------|
| **External broker** | Required (Redis/RabbitMQ) | None - uses Unix sockets |
| **Deployment** | Multiple processes | Single `gunicorn` command |
| **Worker state** | Stateless | Stateful - keep ML models, DB connections loaded |
| **Progress updates** | Polling or WebSocket | Native streaming |
| **HTTP blocking** | N/A (separate process) | Non-blocking with async ASGI |

### When to Use Dirty Arbiters

**Good fit:**
- Tasks that benefit from keeping state (ML models, DB connection pools, caches)
- Tasks where you want immediate results (not fire-and-forget)
- Real-time progress streaming
- Simpler deployment without external dependencies

**Not ideal for:**
- True fire-and-forget queuing with persistence
- Distributed task execution across multiple machines
- Tasks that must survive server restarts

## How It Works

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Key insight:** The HTTP workers use async I/O, so `await client.execute_async()` doesn't block the event loop. One ASGI worker can handle thousands of concurrent requests while waiting for dirty workers to complete tasks.

## Quick Start

### Local Development

```bash
# Install dependencies
pip install fastapi uvloop httpx pytest pytest-asyncio
pip install -e ../..  # Install gunicorn from source

# Run the application
gunicorn -c gunicorn_conf.py app:app

# In another terminal, test it
curl http://localhost:8000/health
curl -X POST http://localhost:8000/api/email/send \
  -H "Content-Type: application/json" \
  -d '{"to": "test@example.com", "subject": "Hello", "body": "World"}'

# Interactive API docs
open http://localhost:8000/docs
```

### Docker

```bash
# Build and run
docker compose up --build

# Run with tests
docker compose --profile test up --build --abort-on-container-exit
```

## Task Workers

Each worker class maintains state across requests:

### EmailWorker (2 workers)
- Keeps SMTP connection alive
- `send_email(to, subject, body)` - Send single email
- `send_bulk_emails(recipients, subject, body)` - Bulk send with streaming progress

### ImageWorker (2 workers)
- Keeps PIL/image libraries loaded
- `resize(image_data, width, height)` - Resize image
- `process_batch(images, operation)` - Batch process with streaming

### DataWorker (4 workers)
- Maintains DB connection pool and query cache
- `aggregate(data, group_by, agg_field)` - Aggregate data
- `etl_pipeline(source_data, transformations)` - ETL with streaming progress
- `cached_query(query_key, ttl)` - Query with in-memory caching

### ScheduledWorker (1 worker)
- For periodic tasks (call from cron)
- `cleanup_old_files(directory, max_age_days)`
- `generate_daily_report()`

## Streaming Progress Example

Real-time progress without polling:

```python
import httpx
import json

async with httpx.AsyncClient() as client:
    async with client.stream(
        "POST",
        "http://localhost:8000/api/email/send-bulk",
        json={
            "recipients": ["a@x.com", "b@x.com", "c@x.com"],
            "subject": "Newsletter",
            "body": "Hello!",
        },
    ) as response:
        async for line in response.aiter_lines():
            if line.startswith("data: "):
                progress = json.loads(line[6:])
                if progress["type"] == "progress":
                    print(f"Progress: {progress['percent']}%")
                elif progress["type"] == "complete":
                    print(f"Done! Sent: {progress['sent']}")
```

## Celery Migration Guide

### Before (Celery)

```python
# tasks.py
from celery import Celery

app = Celery('tasks', broker='redis://localhost')

@app.task
def send_email(to, subject, body):
    smtp = smtplib.SMTP(...)  # New connection every task!
    smtp.send(...)
    return {"status": "sent"}

@app.task(bind=True)
def send_bulk(self, recipients, subject, body):
    for i, to in enumerate(recipients):
        send_email(to, subject, body)
        self.update_state(state='PROGRESS', meta={'current': i})  # Requires polling!
```

```python
# views.py - Flask
from tasks import send_email

@app.route('/send')
def send_view():
    send_email.delay(to, subject, body)  # Fire and forget
    return {"status": "queued"}  # Can't get result without polling
```

### After (Dirty Arbiters)

```python
# tasks.py
from gunicorn.dirty.app import DirtyApp

class EmailWorker(DirtyApp):
    workers = 2

    def init(self):
        self.smtp = smtplib.SMTP(...)  # Connected once, reused!

    def __call__(self, action, *args, **kwargs):
        return getattr(self, action)(*args, **kwargs)

    def send_email(self, to, subject, body):
        self.smtp.send(...)  # Reuses connection
        return {"status": "sent"}

    def send_bulk(self, recipients, subject, body):
        for i, to in enumerate(recipients):
            self.send_email(to, subject, body)
            yield {"type": "progress", "current": i}  # Native streaming!
```

```python
# views.py - FastAPI (async)
from gunicorn.dirty import get_dirty_client_async

@app.post('/send')
async def send_view(data: EmailRequest):
    client = await get_dirty_client_async()
    # Non-blocking! Other requests handled while waiting
    result = await client.execute_async("tasks:EmailWorker", "send_email", ...)
    return result  # Immediate result, no polling!
```

## Configuration

```python
# gunicorn_conf.py

# ASGI workers for non-blocking HTTP
worker_class = "asgi"
asgi_loop = "uvloop"
workers = 4

# Dirty workers (replace Celery)
dirty_apps = [
    "tasks:EmailWorker",
    "tasks:ImageWorker",
    "tasks:DataWorker",
]
dirty_workers = 9
dirty_timeout = 300
```

## Running Tests

```bash
# Unit tests (no server needed)
pytest tests/test_tasks.py -v

# Integration tests (server must be running)
APP_URL=http://localhost:8000 pytest tests/test_integration.py -v

# All tests via Docker
docker compose --profile test up --build --abort-on-container-exit
```

## API Endpoints

Visit `/docs` for interactive Swagger documentation.

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/email/send` | POST | Send single email |
| `/api/email/send-bulk` | POST | Bulk send (SSE streaming) |
| `/api/image/resize` | POST | Resize image |
| `/api/image/process-batch` | POST | Batch process (SSE streaming) |
| `/api/data/aggregate` | POST | Aggregate data |
| `/api/data/etl` | POST | ETL pipeline (SSE streaming) |
| `/api/data/query` | POST | Cached query |
| `/api/scheduled/*` | POST | Scheduled tasks |
| `/health` | GET | Health check |

---

### Overview (`examples/embedding_service/README.md`)

# Embedding Service Example

A FastAPI-based text embedding service using sentence-transformers, powered by
gunicorn's dirty workers for efficient ML model management.

## Overview

This example demonstrates how to build a production-ready embedding API that:
- Keeps ML models loaded in memory across requests (dirty workers)
- Handles HTTP efficiently with async FastAPI (ASGI workers)
- Provides batch embedding for multiple texts
- Includes Docker-based deployment and testing

## Architecture

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│  HTTP Clients   │────►│  FastAPI (ASGI)  │────►│  DirtyWorker        │
│                 │     │  - /embed        │     │  - sentence-        │
│                 │◄────│  - /health       │◄────│    transformers     │
└─────────────────┘     └──────────────────┘     │  - Model in memory  │
                                                  └─────────────────────┘
```

**Why dirty workers?**
- ML models are expensive to load (several seconds)
- Dirty workers load the model once at startup
- HTTP workers remain lightweight and responsive
- Model stays in memory, serving many requests

## Quick Start

### With Docker (recommended)

```bash
cd examples/embedding_service
docker compose up --build
```

### Local Development

```bash
# Install dependencies
pip install sentence-transformers fastapi pydantic

# Run with gunicorn
gunicorn examples.embedding_service.main:app \
  -c examples/embedding_service/gunicorn_conf.py
```

## API Reference

### POST /embed

Generate embeddings for a list of texts.

**Request:**
```json
{
  "texts": ["Hello world", "Another sentence"]
}
```

**Response:**
```json
{
  "embeddings": [
    [0.123, -0.456, ...],
    [0.789, -0.012, ...]
  ]
}
```

**Example:**
```bash
curl -X POST http://localhost:8000/embed \
  -H "Content-Type: application/json" \
  -d '{"texts": ["Hello world"]}'
```

### GET /health

Health check endpoint.

**Response:**
```json
{"status": "ok"}
```

## Configuration

Edit `gunicorn_conf.py` to adjust:

| Setting | Default | Description |
|---------|---------|-------------|
| `workers` | 2 | Number of HTTP workers |
| `dirty_workers` | 1 | Number of ML model workers |
| `dirty_timeout` | 60 | Max seconds per inference |
| `bind` | 0.0.0.0:8000 | Listen address |

## Model

Uses [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2):
- 384-dimensional embeddings
- Fast inference (~14K sentences/sec on GPU)
- Good quality for semantic search
- ~90MB download

To use a different model, edit `embedding_app.py`:
```python
self.model = SentenceTransformer('your-model-name')
```

## Testing

Run the integration tests:

```bash
# Start the service first
docker compose up -d

# Run tests
pip install requests numpy
python test_embedding.py
```

## Production Considerations

1. **GPU Support**: Add CUDA to the Dockerfile for faster inference
2. **Scaling**: Increase `dirty_workers` for more concurrent embeddings
3. **Caching**: Add Redis caching for repeated texts
4. **Rate Limiting**: Add FastAPI middleware for rate limiting
5. **Monitoring**: Add Prometheus metrics endpoint

---

### Overview (`examples/http2_gevent/README.md`)

# HTTP/2 with Gevent Worker Example

This example demonstrates how to run Gunicorn with HTTP/2 support using the gevent async worker.

## Features

- HTTP/2 protocol with ALPN negotiation
- Gevent-based async worker for high concurrency
- Connection multiplexing (multiple streams per connection)
- Flow control for large transfers
- SSL/TLS encryption (required for HTTP/2)

## Quick Start

### 1. Generate SSL Certificates

HTTP/2 requires TLS. Generate self-signed certificates for testing:

```bash
chmod +x generate_certs.sh
./generate_certs.sh
```

### 2. Start with Docker Compose

```bash
docker compose up -d
```

### 3. Test the Server

Using curl with HTTP/2:

```bash
# Basic request
curl -k --http2 https://localhost:8443/

# Check HTTP version
curl -k --http2 -w "HTTP Version: %{http_version}\n" https://localhost:8443/

# Test echo endpoint
curl -k --http2 -X POST -d "Hello HTTP/2" https://localhost:8443/echo

# Get server info
curl -k --http2 https://localhost:8443/info | jq
```

### 4. Run Tests

```bash
# Install test dependencies
pip install httpx[http2] pytest pytest-asyncio

# Run tests
python test_http2_gevent.py

# Or with pytest for more detail
pytest test_http2_gevent.py -v
```

## Running Locally (Without Docker)

### Prerequisites

```bash
pip install gunicorn[gevent,http2]
```

### Generate Certificates

```bash
./generate_certs.sh
```

### Start Server

```bash
gunicorn --config gunicorn_conf.py app:app
```

Or with command-line options:

```bash
gunicorn app:app \
    --bind 0.0.0.0:8443 \
    --worker-class gevent \
    --workers 4 \
    --worker-connections 1000 \
    --http-protocols h2,h1 \
    --certfile certs/server.crt \
    --keyfile certs/server.key
```

## Configuration Options

### HTTP/2 Settings

| Setting | Default | Description |
|---------|---------|-------------|
| `http_protocols` | `['h1']` | Enable protocols: `['h2', 'h1']` for HTTP/2 |
| `http2_max_concurrent_streams` | 100 | Max streams per connection |
| `http2_initial_window_size` | 65535 | Flow control window size (bytes) |
| `http2_max_frame_size` | 16384 | Max frame size (bytes) |
| `http2_max_header_list_size` | 65536 | Max header list size (bytes) |

### Gevent Worker Settings

| Setting | Default | Description |
|---------|---------|-------------|
| `worker_class` | `sync` | Set to `gevent` for async |
| `workers` | 1 | Number of worker processes |
| `worker_connections` | 1000 | Max clients per worker |

## Endpoints

| Path | Method | Description |
|------|--------|-------------|
| `/` | GET | Hello message |
| `/health` | GET | Health check |
| `/echo` | POST | Echo request body |
| `/info` | GET | Server/request info as JSON |
| `/large` | GET | 1MB response (test streaming) |
| `/stream` | GET | Server-sent events stream |
| `/delay?seconds=N` | GET | Delayed response |
| `/priority` | GET | HTTP/2 priority info |

## Performance Tips

1. **Worker Count**: Use `2 * CPU cores + 1` workers for I/O-bound apps
2. **Connections**: Increase `worker_connections` for high concurrency
3. **Window Size**: Larger `http2_initial_window_size` improves throughput for large transfers
4. **Streams**: Increase `http2_max_concurrent_streams` for many parallel requests

## Troubleshooting

### Certificate Issues

```bash
# Regenerate certificates
rm -rf certs/
./generate_certs.sh
```

### Connection Refused

```bash
# Check if server is running
docker compose ps

# View logs
docker compose logs -f
```

### HTTP/2 Not Negotiated

Ensure:
- SSL/TLS is configured (certfile and keyfile)
- `http_protocols` includes `'h2'`
- Client supports HTTP/2 over TLS (curl with `--http2`, not `--http2-prior-knowledge`)

## License

MIT License - See the main Gunicorn repository for details.

---

### Overview (`examples/streaming_chat/README.md`)

# Streaming Chat Example

A FastAPI-based chat demo that simulates LLM token-by-token streaming, powered
by Gunicorn's dirty workers for efficient long-running operations.

## Overview

This example demonstrates how to build a streaming chat API that:
- Streams tokens word-by-word like ChatGPT (Server-Sent Events)
- Uses dirty workers for the "inference" workload
- Includes a browser-based chat UI for testing
- Requires no ML dependencies (simulated responses)

## Architecture

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│  Browser/curl   │────►│  FastAPI (ASGI)  │────►│  DirtyWorker        │
│  SSE stream     │     │  - /chat (SSE)   │     │  - ChatApp          │
│                 │◄────│  - /chat/sync    │◄────│  - Token generator  │
└─────────────────┘     └──────────────────┘     └─────────────────────┘
                              │
                              ▼
                        text/event-stream
                        data: {"token": "Hello"}
                        data: {"token": " "}
                        data: {"token": "world"}
                        data: [DONE]
```

**Why streaming with dirty workers?**
- Real LLM inference is slow (seconds to minutes)
- Users expect to see responses appear gradually
- Dirty workers keep the "model" loaded between requests
- HTTP workers remain responsive during streaming

## Quick Start

### With Docker (recommended)

```bash
cd examples/streaming_chat
docker compose up --build
```

Then open http://localhost:8000 in your browser.

### Local Development

```bash
# Install dependencies
pip install fastapi pydantic

# Run with gunicorn
gunicorn examples.streaming_chat.main:app \
  -c examples/streaming_chat/gunicorn_conf.py
```

## API Reference

### POST /chat

Stream a chat response using Server-Sent Events.

**Request:**
```json
{
  "prompt": "hello",
  "thinking": false
}
```

**Response:** `text/event-stream`
```
data: {"token": "Hello"}

data: {"token": "!"}

data: {"token": " "}

data: {"token": "I'm"}

...

data: [DONE]
```

**Example with curl:**
```bash
curl -N http://localhost:8000/chat \
  -H "Content-Type: application/json" \
  -d '{"prompt": "hello"}'
```

### POST /chat/sync

Non-streaming version that returns the complete response.

**Request:**
```json
{
  "prompt": "hello"
}
```

**Response:**
```json
{
  "response": "Hello! I'm a simulated AI assistant..."
}
```

### GET /health

Health check endpoint.

**Response:**
```json
{"status": "ok"}
```

### GET /

Browser-based chat UI for testing.

## Configuration

Edit `gunicorn_conf.py` to adjust:

| Setting | Default | Description |
|---------|---------|-------------|
| `workers` | 2 | Number of HTTP workers |
| `dirty_workers` | 1 | Number of dirty workers |
| `dirty_timeout` | 60 | Max seconds per request |
| `bind` | 0.0.0.0:8000 | Listen address |

## Prompts

The simulated chat app responds to these keywords:

| Keyword | Response |
|---------|----------|
| `hello`, `hi`, `hey` | Greeting message |
| `explain` | Explanation of dirty workers |
| `streaming` | How streaming works |
| `code` | Example code snippet |
| (default) | Generic thoughtful response |

## Features Demonstrated

1. **Token streaming** - Word-by-word output via generators
2. **SSE protocol** - Browser-compatible event streaming
3. **Async generators** - Using `stream_async()` from dirty client
4. **Thinking mode** - Multi-phase streaming with visible "thinking"
5. **Browser UI** - Interactive chat with cursor animation

## Testing

Run the integration tests:

```bash
# Start the service first
docker compose up -d

# Run tests
pip install requests
python test_streaming.py
```

## Adapting for Real LLMs

To use a real LLM instead of simulated responses:

```python
# chat_app.py
from gunicorn.dirty.app import DirtyApp

class ChatApp(DirtyApp):
    def init(self):
        from transformers import pipeline
        self.generator = pipeline("text-generation", model="gpt2")

    def generate(self, prompt):
        for output in self.generator(prompt, max_new_tokens=100, do_sample=True):
            # Yield tokens as they're generated
            yield output["generated_text"]

    def close(self):
        del self.generator
```

Or with an API-based LLM:

```python
class ChatApp(DirtyApp):
    def init(self):
        import openai
        self.client = openai.OpenAI()

    async def generate(self, prompt):
        stream = self.client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            stream=True
        )
        for chunk in stream:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
```

## Production Considerations

1. **Real LLM**: Replace `ChatApp` with actual model inference
2. **GPU Support**: Add CUDA to Dockerfile for faster inference
3. **Rate Limiting**: Add FastAPI middleware for rate limiting
4. **Authentication**: Add API key validation
5. **Monitoring**: Add Prometheus metrics endpoint
6. **Timeouts**: Adjust `dirty_timeout` based on max response length

---

### Overview (`tests/docker/README.md`)

# Docker integration tests

End-to-end suites that run gunicorn in containers, some behind nginx. Use these
when a change touches process management, protocol behavior on the wire, or
anything that only shows up with a real client and a real proxy in front.

## Running them

```sh
PYTEST=".venv/bin/python -m pytest" scripts/run_docker_tests.sh
```

One suite at a time:

```sh
PYTEST=".venv/bin/python -m pytest" scripts/run_docker_tests.sh uwsgi http2
```

Requires a running Docker daemon. The first run builds images and is slow;
later runs reuse them.

## Do not run them with a single pytest invocation

`pytest tests/docker` looks like it should work and does not. Every suite ships
its own `docker-compose.yml` and binds fixed host ports, so running them
together means each suite's requests can reach another suite's containers. The
result is failures that have nothing to do with the code: assertion errors from
valid responses served by the wrong application, not connection errors.

`tests/docker` is listed in `norecursedirs` in `pyproject.toml`, so a plain
`pytest tests/` skips these entirely and reports success without running any of
them. That is deliberate, but worth knowing before you conclude the suite is
green.

Use `scripts/run_docker_tests.sh`, which runs each suite on its own and tears
its stack down before starting the next.

## Ports

The suites bind fixed host ports. Anything else already listening on one of them
will silently answer the tests instead of the container. `mkdocs serve` defaults
to 8000 and collides with `asgi_compliance`.

| Suite | Host ports |
| --- | --- |
| `asgi_compliance` | 8000, 8080, 8443, 8444, 8445 |
| `asgi_framework_compat` | 8001 to 8006 |
| `http2` | 8443, 8444 |
| `h2spec` | 8451, 8452, 8453 |
| `stress` | 8460 to 8466, 8474, 8475 |
| `uwsgi` | 8080 |

If a suite fails everywhere at once, check for a port collision before looking
at the code:

```sh
lsof -nP -iTCP:8000 -sTCP:LISTEN
```

## Suites

| Directory | Covers |
| --- | --- |
| `asgi_compliance` | ASGI HTTP, websocket, lifespan and streaming behavior |
| `asgi_framework_compat` | Django, FastAPI, Starlette, Quart, Litestar, BlackSheep |
| `dirty_arbiter` | dirty arbiter lifecycle, parent death, respawn |
| `dirty_ttin_ttou` | scaling dirty workers with TTIN/TTOU |
| `http2` | HTTP/2 over TLS, direct and behind nginx |
| `per_app_allocation` | per-app worker allocation end to end |
| `stress` | k6 load and resilience across workers, direct and behind nginx |
| `uwsgi` | uWSGI binary protocol behind nginx |

`asgi/` and `test_asgi_uwsgi/` are shell-driven demos (`test_asgi.sh`,
`test_uwsgi.sh`), not pytest suites, and the runner skips them.

## CI

`.github/workflows/docker-integration.yml` runs `uwsgi`, `h2spec`, and the
`stress` smoke matrix. The heavier `stress` resilience and fault scenarios run
in `.github/workflows/stress-nightly.yml`. The other suites are not gated, so
run them locally before changing the areas they cover.

## Certificates

`asgi_compliance` and `http2` generate self-signed certificates into their
`certs/` directories, regenerating them once a day. They will show as modified
in `git status` after a run. Do not commit them.

---

### Overview (`tests/docker/asgi_framework_compat/README.md`)

# ASGI Framework Compatibility Test Suite

This test suite validates gunicorn's native ASGI worker (`-k asgi`) against
multiple ASGI frameworks to ensure protocol compliance.

## Frameworks Tested

| Framework | Description |
|-----------|-------------|
| Django + Channels | Django with Channels for WebSocket |
| FastAPI | Modern, fast API framework (Starlette-based) |
| Starlette | Pure ASGI framework |
| Quart | Flask-like async framework |
| Litestar | Modern ASGI framework |
| BlackSheep | High-performance ASGI framework |

## Test Categories

- **HTTP Scope**: ASGI 3.0 HTTP scope compliance
- **HTTP Messages**: Request/response message handling
- **WebSocket**: WebSocket protocol compliance
- **Lifespan**: Startup/shutdown lifecycle
- **Streaming**: Chunked responses and SSE

## Quick Start

```bash
# Build and start all framework containers
docker compose up -d --build

# Run tests
pip install -r requirements.txt
pytest tests/ -v

# Generate compatibility grid
python scripts/generate_grid.py
```

## Testing Event Loop Variants

```bash
# Test with auto-detection (uvloop if available)
ASGI_LOOP=auto docker compose up -d --build
pytest tests/ -v

# Test with asyncio only
ASGI_LOOP=asyncio docker compose up -d --build
pytest tests/ -v

# Test with uvloop explicitly
ASGI_LOOP=uvloop docker compose up -d --build
pytest tests/ -v

# Generate combined report for both loop types
python scripts/generate_grid.py --loop both
```

## Single Framework Testing

```bash
# Test only FastAPI
pytest tests/ -v --framework fastapi

# Test only Django
pytest tests/ -v --framework django
```

## Directory Structure

```
asgi_framework_compat/
├── conftest.py           # Test fixtures
├── docker-compose.yml    # Container orchestration
├── requirements.txt      # Test dependencies
├── frameworks/
│   ├── contract.py       # Endpoint contract
│   ├── django_app/       # Django implementation
│   ├── fastapi_app/      # FastAPI implementation
│   ├── starlette_app/    # Starlette implementation
│   ├── quart_app/        # Quart implementation
│   ├── litestar_app/     # Litestar implementation
│   └── blacksheep_app/   # BlackSheep implementation
├── tests/
│   ├── test_http_scope.py
│   ├── test_http_messages.py
│   ├── test_websocket_scope.py
│   ├── test_lifespan_scope.py
│   └── test_streaming.py
├── scripts/
│   └── generate_grid.py  # Compatibility matrix
└── results/              # Generated reports
```

## Container Management

```bash
# Start containers
docker compose up -d --build

# View logs
docker compose logs -f

# Stop containers
docker compose down

# Rebuild specific framework
docker compose build fastapi
docker compose up -d fastapi
```

## Results

After running `generate_grid.py`, check the `results/` directory for:

- `compatibility_grid_*.md` - Markdown compatibility matrices
- `compatibility_grid_*.json` - JSON data for programmatic access

---

### Overview (`tests/docker/dirty_arbiter/README.md`)

# Docker-Based Dirty Arbiter Integration Tests

This directory contains Docker-based integration tests that verify the dirty
arbiter process lifecycle under realistic conditions.

## Overview

These tests verify:

1. **Parent Death Detection**: Dirty arbiter self-terminates when main arbiter
   dies unexpectedly (SIGKILL)
2. **Orphan Cleanup**: Old dirty arbiter processes are cleaned up on restart
3. **Respawning**: Main arbiter respawns dirty arbiter when it crashes
4. **Graceful Shutdown**: Both arbiters exit cleanly on SIGTERM

## Prerequisites

- Docker
- Python 3.10+
- pytest

## Quick Start

```bash
# Build the Docker image
docker compose build

# Run all tests
pytest test_parent_death.py -v

# Run specific test
pytest test_parent_death.py::TestParentDeath::test_dirty_arbiter_exits_on_parent_sigkill -v
```

## Manual Verification

You can manually verify the behavior:

```bash
# Start the container
docker compose up -d

# Check running processes
docker exec dirty_arbiter-gunicorn-1 ps aux | grep gunicorn

# SIGKILL the master and watch dirty arbiter exit
MASTER_PID=$(docker exec dirty_arbiter-gunicorn-1 pgrep -f "gunicorn: master")
docker exec dirty_arbiter-gunicorn-1 kill -9 $MASTER_PID

# After ~2 seconds, check that all gunicorn processes exited
docker exec dirty_arbiter-gunicorn-1 ps aux | grep gunicorn

# View logs
docker logs dirty_arbiter-gunicorn-1

# Cleanup
docker compose down
```

## Test Scenarios

### Scenario 1: Parent SIGKILL

Tests that the dirty arbiter detects parent death via ppid check:

1. Start gunicorn with dirty workers
2. SIGKILL the main arbiter (bypasses graceful shutdown)
3. Verify dirty arbiter detects ppid change within ~2 seconds
4. Verify no orphan processes remain

### Scenario 2: Orphan Cleanup

Tests the `_cleanup_orphaned_dirty_arbiter()` mechanism:

1. Start gunicorn, note dirty arbiter PID
2. SIGKILL main arbiter (dirty arbiter becomes orphan)
3. Restart gunicorn
4. Verify old dirty arbiter was cleaned up
5. Verify new dirty arbiter spawned

### Scenario 3: Dirty Arbiter Respawn

Tests that main arbiter respawns a dead dirty arbiter:

1. Start gunicorn
2. SIGKILL the dirty arbiter
3. Wait for respawn (~1-2 seconds)
4. Verify new dirty arbiter is running

### Scenario 4: Graceful Shutdown

Tests clean shutdown via SIGTERM:

1. Start gunicorn with dirty workers
2. SIGTERM the main arbiter
3. Verify both arbiters exit cleanly within graceful_timeout
4. Verify clean exit logs

## Files

| File | Description |
|------|-------------|
| `Dockerfile` | Container build configuration |
| `docker-compose.yml` | Container orchestration |
| `app.py` | Simple WSGI app with TestDirtyApp |
| `gunicorn_conf.py` | Gunicorn configuration |
| `test_parent_death.py` | pytest integration tests |
| `README.md` | This file |

## Configuration

The `gunicorn_conf.py` uses:
- 1 sync worker
- 1 dirty worker
- 5 second graceful timeout (for faster tests)
- Debug logging

## Expected Log Messages

When verifying behavior, look for these log messages:

| Message | Meaning |
|---------|---------|
| `Parent changed, shutting down dirty arbiter` | ppid detection triggered |
| `Killing orphaned dirty arbiter` | Orphan cleanup activated |
| `Spawning dirty arbiter` | New dirty arbiter being created |
| `Dirty arbiter exiting` | Clean shutdown |

## Troubleshooting

**Tests time out waiting for container**:
- Check Docker is running
- Check no port conflicts on 8000
- Try `docker compose down` and rebuild

**Dirty arbiter doesn't exit after parent death**:
- Check ppid detection is working (logs should show check)
- The check runs every 1 second, so allow 2-3 seconds

**Container logs not showing expected messages**:
- Verify loglevel is set to "debug" in gunicorn_conf.py
- Check `docker logs <container>` for full output

---

### Overview (`tests/docker/per_app_allocation/README.md`)

# Per-App Worker Allocation E2E Tests

End-to-end Docker-based tests for the per-app worker allocation feature.

## Overview

These tests verify that:
- Apps with worker limits are only loaded on the specified number of workers
- Requests are routed only to workers that have the target app loaded
- Round-robin distribution works correctly within limited worker sets
- Worker crash scenarios maintain correct app allocation
- Class attribute `workers=N` is respected
- Config-based `:N` overrides class attributes

## Configuration

The tests use 4 dirty workers with 3 apps:
- **LightweightApp**: No limit (loads on all 4 workers)
- **HeavyApp**: `workers=2` class attribute (loads on 2 workers)
- **ConfigLimitedApp**: `:1` config (loads on 1 worker)

## Running Tests

```bash
# From this directory
cd tests/docker/per_app_allocation

# Build the Docker image
docker compose build

# Run all tests
pytest test_per_app_e2e.py -v

# Run specific test
pytest test_per_app_e2e.py::TestPerAppAllocation::test_config_limited_app_uses_one_worker -v
```

## Test Categories

### TestPerAppAllocation
- Tests basic functionality of per-app worker allocation
- Verifies round-robin distribution
- Tests app accessibility

### TestPerAppWorkerCrash
- Tests behavior when workers crash
- Verifies app recovery after worker respawn

### TestPerAppLogs
- Verifies logging output contains expected information

## Requirements

- Docker and Docker Compose
- Python 3.8+
- pytest
- requests

## Notes

- Tests run on port 28001 to avoid conflicts with the existing dirty_arbiter tests on 8000 and with macOS Docker alternatives that often reserve port 8001 (e.g., OrbStack's vcom-tunnel)
- The container uses a keep-alive wrapper to allow testing worker crash scenarios

---

### Overview (`tests/docker/stress/README.md`)

# Stress and resilience suite

Drives real load at gunicorn with [k6](https://k6.io) and asserts the server
stays correct under it: no failed requests, intact bodies and checksums, the
negotiated protocol, and no worker tracebacks. Use it when a change touches the
workers, the ASGI/HTTP-2 paths, or process management and you need to know it
holds up under concurrency, not just on a single request.

Load runs as a pinned k6 container inside the compose network; network faults
run through a pinned [Toxiproxy](https://github.com/Shopify/toxiproxy). Nothing
is installed on the host and Grafana Cloud is not used. HTTP/2 and HPACK
conformance stay with the `h2spec` suite; k6 is a load driver, not a fuzzer.

## Run it

```sh
PYTEST=".venv/bin/python -m pytest" scripts/run_docker_tests.sh stress
```

This runs the smoke matrix only. The heavier resilience and Toxiproxy scenarios
are opt-in:

```sh
GUNICORN_STRESS_HEAVY=1 .venv/bin/python -m pytest tests/docker/stress -v
```

Requires a running Docker daemon. The first run builds the gunicorn and nginx
images and pulls `grafana/k6` and `ghcr.io/shopify/toxiproxy`; later runs reuse
them.

## What it covers

Smoke matrix (wired and verified):

| Config | Worker | Path | Protocol |
| --- | --- | --- | --- |
| `sync-direct-h1` | sync | direct | HTTP/1.1 |
| `gthread-nginx-h1` | gthread | behind nginx | HTTP/1.1 |
| `asgi-direct-h1` | asgi | direct | HTTP/1.1 |
| `asgi-nginx-h1` | asgi | behind nginx | HTTP/1.1 |
| `asgi-ws-nginx` | asgi | behind nginx | WebSocket |
| `h2-direct-tls` | asgi | direct | HTTP/2 (TLS) |

nginx terminates HTTP/2 and the uWSGI protocol downstream only; the gunicorn
upstream always sees HTTP/1.1 (proxy) or the uWSGI protocol (`uwsgi_pass`). The
`uwsgi_pass` topology targets the sync worker, gunicorn's supported uWSGI path;
the asgi worker's uWSGI parser is not exercised here.

Heavy scenarios (`GUNICORN_STRESS_HEAVY=1`): kill a worker, HUP reload, TTIN/TTOU
scaling, and Toxiproxy latency, bandwidth, and connection-reset faults, each
applied while load runs.

## Selecting worker, topology, protocol, and profile

Every combination is reachable by environment. The k6 scenario is chosen with
`SCENARIO` (`smoke`, `constant`, `ramping`, `spike`, `churn`, `soak`) and tuned
with `RATE`, `DURATION`, `VUS`, `MAXVUS`, `FAIL_BUDGET`, `MAX_P95`, `MAX_P99`.
Gunicorn services are shaped by the compose environment: `MAX_REQUESTS`,
`ASGI_LOOP` (`auto`/`asyncio`/`uvloop`), and `H2_WORKER` (which worker backs the
HTTP/2 service). For example, to run the asgi worker on uvloop with request
recycling:

```sh
ASGI_LOOP=uvloop MAX_REQUESTS=1000 docker compose -p gunicorn_stress \
  -f tests/docker/stress/docker-compose.yml up -d --build
```

## Ports

Fixed host ports (run one docker suite at a time; a collision answers from the
wrong stack):

| Port | Target |
| --- | --- |
| 8460 | sync, direct |
| 8461 | gthread, behind nginx |
| 8462 | asgi, direct |
| 8463 | asgi, behind nginx (HTTP + WebSocket) |
| 8464 | HTTP/2 (TLS) behind nginx |
| 8465 | asgi HTTP/2 (TLS), direct |
| 8466 | sync behind nginx `uwsgi_pass` |
| 8474 | Toxiproxy admin API |
| 8475 | Toxiproxy proxy |

## Resource expectations

The smoke matrix runs in a few minutes on a laptop. The nightly profiles
(`ramping`, `spike`, `soak`) and the heavy resilience/fault tests run longer and
push more concurrency; give them a host with a few spare cores and ~2 GB free.
The `soak` scenario defaults to 30 minutes (`DURATION` overrides it).

## Evidence

k6 writes a machine-readable summary per run to `_results/<config>-<scenario>.json`
(git-ignored) and prints error rate, check rate, p95/p99/max latency, and dropped
iterations. Container logs are available with
`docker compose -p gunicorn_stress logs <service>`.

---

### Overview (`tests/docker/uwsgi/README.md`)

# uWSGI Protocol Docker Integration Tests

This directory contains Docker-based integration tests that verify gunicorn's
uWSGI binary protocol implementation works correctly with nginx's `uwsgi_pass`
directive.

## Architecture

```
[pytest] --HTTP--> [nginx:8080] --uwsgi_pass--> [gunicorn:8000]
```

The tests make HTTP requests to nginx, which proxies them to gunicorn using the
uWSGI binary protocol. This validates the complete request/response cycle through
the protocol.

## Prerequisites

- Docker
- Docker Compose (v2)
- Python 3.8+
- pytest
- requests

## Running Tests

### From repository root:

```bash
# Run all uWSGI integration tests
pytest tests/docker/uwsgi/ -v

# Run specific test class
pytest tests/docker/uwsgi/ -v -k TestBasicRequests

# Skip Docker tests (for CI environments without Docker)
pytest tests/ -v -m "not docker"
```

### Manual testing:

```bash
cd tests/docker/uwsgi

# Start services
docker compose up -d

# Wait for services to be healthy
docker compose ps

# Test endpoints
curl http://localhost:8080/
curl -X POST -d "test body" http://localhost:8080/echo
curl http://localhost:8080/headers
curl "http://localhost:8080/query?foo=bar"
curl http://localhost:8080/environ
curl http://localhost:8080/error/404
curl http://localhost:8080/large > /dev/null  # 1MB response

# View logs
docker compose logs gunicorn
docker compose logs nginx

# Stop services
docker compose down -v
```

## Test Categories

| Category | Description |
|----------|-------------|
| `TestBasicRequests` | GET, POST, query strings, large bodies |
| `TestHeaderPreservation` | Custom headers, Host, Content-Type, User-Agent |
| `TestKeepAlive` | Multiple requests per connection |
| `TestErrorResponses` | HTTP error codes (400, 404, 500, etc.) |
| `TestEnvironVariables` | WSGI environ: REQUEST_METHOD, PATH_INFO, etc. |
| `TestLargeResponses` | 1MB response body streaming |
| `TestConcurrency` | Parallel request handling |
| `TestSpecialCases` | Edge cases: binary data, unicode, long headers |

## Files

| File | Purpose |
|------|---------|
| `docker-compose.yml` | Orchestrates nginx + gunicorn containers |
| `Dockerfile.gunicorn` | Builds gunicorn image with test app |
| `Dockerfile.nginx` | Builds nginx with uwsgi config |
| `nginx.conf` | nginx configuration using `uwsgi_pass` |
| `uwsgi_params` | Standard uwsgi parameter mappings |
| `app.py` | Test WSGI application with multiple endpoints |
| `conftest.py` | pytest fixtures for Docker lifecycle |
| `test_uwsgi_integration.py` | Test cases |

## Test App Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Basic hello response |
| `/echo` | POST | Echo request body |
| `/headers` | GET/POST | Return received headers as JSON |
| `/environ` | GET/POST | Return WSGI environ as JSON |
| `/query` | GET | Return query params as JSON |
| `/json` | POST | Parse and echo JSON body |
| `/error/{code}` | GET | Return specified HTTP error |
| `/large` | GET | Return 1MB response |

## Gunicorn Configuration

The gunicorn container runs with:

```bash
gunicorn \
  --protocol uwsgi \
  --uwsgi-allow-from "*" \
  --bind 0.0.0.0:8000 \
  --workers 2 \
  --log-level debug \
  app:application
```

Key settings:
- `--protocol uwsgi`: Enable uWSGI binary protocol
- `--uwsgi-allow-from "*"`: Accept connections from Docker network IPs

## Troubleshooting

### Services won't start

Check Docker logs:
```bash
docker compose logs
```

### Connection refused

Wait for health checks:
```bash
docker compose ps  # Check health status
```

### Tests timing out

Increase `STARTUP_TIMEOUT` in `conftest.py` or check if ports are in use:
```bash
lsof -i :8080
lsof -i :8000
```

### Rebuild after code changes

```bash
docker compose build --no-cache
docker compose up -d
```

---

### Content Index (`docs/content/index.md`)

---
template: home.html
title: Gunicorn - Python WSGI HTTP Server
---

<section class="hero">
  <div class="container">
    <img class="hero__logo" src="assets/gunicorn.svg" alt="Gunicorn" style="width: 350px;" />
    <h1>Serve Python on the Web</h1>
    <p class="hero__tagline">
      Battle-tested. Production-ready. One command to serve your Python apps.
    </p>
    <div class="hero__buttons">
      <a class="btn btn--primary" href="quickstart/">Get Started</a>
      <a class="btn btn--secondary" href="https://github.com/benoitc/gunicorn">View on GitHub</a>
    </div>
    <div class="terminal">
      <div class="terminal__header">
        <span class="terminal__dot terminal__dot--red"></span>
        <span class="terminal__dot terminal__dot--yellow"></span>
        <span class="terminal__dot terminal__dot--green"></span>
      </div>
      <div class="terminal__body">
        <span class="terminal__line"><span class="terminal__prompt">$ </span>pip install gunicorn</span>
        <span class="terminal__line"><span class="terminal__prompt">$ </span>gunicorn myapp:app</span>
        <span class="terminal__line terminal__comment"># Listening at http://127.0.0.1:8000</span>
      </div>
    </div>
  </div>
</section>

<section class="why">
  <div class="container">
    <h2>Why Gunicorn?</h2>
    <div class="pillars">
      <div class="pillar">
        <h3>Production-Proven</h3>
        <p>Trusted by thousands of companies. The pre-fork worker model handles traffic spikes gracefully.</p>
      </div>
      <div class="pillar">
        <h3>Lightweight</h3>
        <p>Minimal dependencies, simple configuration. Efficient from containers to bare metal.</p>
      </div>
      <div class="pillar">
        <h3>Compatible</h3>
        <p>Works with any WSGI or ASGI framework. Django, Flask, FastAPI—it just runs.</p>
      </div>
    </div>
  </div>
</section>

<section class="frameworks">
  <div class="container">
    <h2>Works With Your Stack</h2>
    <p class="frameworks__subtitle">WSGI and ASGI frameworks, no changes needed</p>
    <div class="frameworks__list">
      <span class="framework-tag">Django</span>
      <span class="framework-tag">Flask</span>
      <span class="framework-tag framework-tag--new">FastAPI</span>
      <span class="framework-tag">Pyramid</span>
      <span class="framework-tag framework-tag--new">Starlette</span>
      <span class="framework-tag">Falcon</span>
      <span class="framework-tag">Bottle</span>
      <span class="framework-tag framework-tag--new">Quart</span>
    </div>
  </div>
</section>

<section class="workers">
  <div class="container">
    <h2>Choose Your Worker</h2>
    <div class="workers__grid">
      <a class="worker" href="design/#sync-workers">
        <h3>Sync</h3>
        <p>The default. One request per worker. Simple and predictable.</p>
      </a>
      <a class="worker" href="design/#async-workers">
        <h3>Async (Gevent/Eventlet)</h3>
        <p>Thousands of concurrent connections for I/O-bound workloads.</p>
      </a>
      <a class="worker" href="reference/settings/#threads">
        <h3>Threads</h3>
        <p>Multiple threads per worker. Balance concurrency and simplicity.</p>
      </a>
      <a class="worker" href="asgi/">
        <h3>ASGI</h3>
        <p>Native asyncio for FastAPI, Starlette, and async frameworks.</p>
      </a>
    </div>
  </div>
</section>

<section class="quick-links">
  <div class="container">
    <h2>Documentation</h2>
    <div class="quick-links__grid">
      <a class="quick-link" href="quickstart/">
        <strong>Quickstart</strong>
        <span>Get running in 5 minutes</span>
      </a>
      <a class="quick-link" href="deploy/">
        <strong>Deployment</strong>
        <span>Nginx, systemd, Docker</span>
      </a>
      <a class="quick-link" href="reference/settings/">
        <strong>Settings</strong>
        <span>All configuration options</span>
      </a>
      <a class="quick-link" href="faq/">
        <strong>FAQ</strong>
        <span>Common questions</span>
      </a>
      <a class="quick-link" href="roadmap/">
        <strong>Roadmap</strong>
        <span>Where the project is going</span>
      </a>
    </div>
  </div>
</section>

<section class="sponsors">
  <div class="container">
    <h2>Support</h2>
    <p>Powering Python apps since 2010. Support continued development.</p>
    <a class="btn btn--secondary" href="sponsor/">Become a Sponsor</a>
    <div class="sponsors__list" style="margin-top: 2rem;">
      <p style="font-size: 0.9rem; color: #666;">Sponsored by</p>
      <a href="https://enki-multimedia.eu" target="_blank" rel="noopener">
        <img src="assets/enki-multimedia.svg" alt="Enki Multimedia" style="height: 50px;" />
      </a>
    </div>
  </div>
</section>

<section class="home-footer">
  <div class="container">
    <h2>Join the Community</h2>
    <p>Questions? Bugs? Ideas? We're here to help.</p>
    <div class="home-footer__links">
      <a href="https://github.com/benoitc/gunicorn/issues">GitHub Issues</a>
      <a href="https://web.libera.chat/?channels=%23gunicorn">Chat in your browser</a>
      <a href="roadmap/">Roadmap</a>
      <a href="community/">Contributing</a>
    </div>
  </div>
</section>

---

### Content Quickstart (`docs/content/quickstart.md`)

# Quickstart

Get a Python web application running with Gunicorn in 5 minutes.

## Install

```bash
pip install gunicorn
```

## Create an Application

Create `app.py`:

=== "Flask"

    ```python
    from flask import Flask

    app = Flask(__name__)

    @app.route("/")
    def hello():
        return "Hello, World!"
    ```

=== "FastAPI"

    ```python
    from fastapi import FastAPI

    app = FastAPI()

    @app.get("/")
    def hello():
        return {"message": "Hello, World!"}
    ```

=== "Django"

    Django projects already have a WSGI application at `myproject/wsgi.py`.
    No additional code is needed.

=== "Plain WSGI"

    ```python
    def app(environ, start_response):
        data = b"Hello, World!"
        start_response("200 OK", [
            ("Content-Type", "text/plain"),
            ("Content-Length", str(len(data)))
        ])
        return [data]
    ```

## Run

```bash
gunicorn app:app
```

For Django:

```bash
gunicorn myproject.wsgi
```

For FastAPI (ASGI):

```bash
gunicorn app:app --worker-class asgi
```

## Add Workers

Use multiple workers to handle concurrent requests:

```bash
gunicorn app:app --workers 4
```

A good starting point is `2 * CPU_CORES + 1` workers.

## Bind to a Port

By default Gunicorn binds to `127.0.0.1:8000`. Change it with:

```bash
gunicorn app:app --bind 0.0.0.0:8080
```

## Configuration File

Create `gunicorn.conf.py` for reusable settings:

```python
bind = "0.0.0.0:8000"
workers = 4
accesslog = "-"
```

Then run:

```bash
gunicorn app:app
```

Gunicorn automatically loads `gunicorn.conf.py` from the current directory.

## Next Steps

- [Run](run.md) - Full CLI reference and framework integration
- [Configure](configure.md) - Configuration file options
- [Deploy](deploy.md) - Production deployment with nginx and process managers
- [Settings](reference/settings.md) - Complete settings reference

---

### Content 2010 News (`docs/content/2010-news.md`)

<span id="news-2010"></span>
# Changelog - 2010

## 0.12.0 / 2010-12-22

- Add support for logging configuration using a ini file.
  It uses the standard Python logging's module Configuration
  file format and allows anyone to use his custom file handler
- Add IPV6 support
- Add multidomain application example
- Improve gunicorn_django command when importing settings module
  using DJANGO_SETTINGS_MODULE environment variable
- Send appropriate error status on http parsing
- Fix pidfile, set permissions so other user can read
  it and use it.
- Fix temporary file leaking
- Fix setpgrp issue, can now be launched via ubuntu upstart
- Set the number of workers to zero on WINCH

## 0.11.2 / 2010-10-30

* Add SERVER_SOFTWARE to the os.environ
* Add support for django settings environment variable
* Add support for logging configuration in Paster ini-files
* Improve arbiter notification in asynchronous workers
* Display the right error when a worker can't be used
* Fix Django support
* Fix HUP with Paster applications
* Fix readline in wsgi.input

## 0.11.1 / 2010-09-02

* Implement max-requests feature to prevent memory leaks.
* Added 'worker_exit' server hook.
* Reseed the random number generator after fork().
* Improve Eventlet worker.
* Fix Django command `run_gunicorn`.
* Fix the default proc name internal setting.
* Workaround to prevent Gevent worker to segfault on MacOSX.

## 0.11.0 / 2010-08-12

* Improve dramatically performances of Gevent and Eventlet workers
* Optimize HTTP parsing
* Drop Server and Date headers in start_response when provided.
* Fix latency issue in async workers

## 0.10.1 / 2010-08-06

* Improve gevent's workers. Add "egg:gunicorn#gevent_wsgi" worker using
  `gevent.wsgi <http://www.gevent.org/gevent.wsgi.html>`_ and
  "egg:gunicorn#gevent_pywsgi" worker using `gevent.pywsgi
  <http://www.gevent.org/gevent.pywsgi.html>`_ .
  **"egg:gunicorn#gevent"** using our own HTTP parser is still here and
  is **recommended** for normal uses. Use the "gevent.wsgi" parser if you
  need really fast connections and don't need streaming, keepalive or ssl.
* Add pre/post request hooks
* Exit more quietly
* Fix gevent dns issue

## 0.10.0 / 2010-07-08

* New HTTP parser.
* New HUP behaviour. Re-reads the configuration and then reloads all
  worker processes without changing the master process id. Helpful for
  code reloading and monitoring applications like supervisord and runit.
* Added a preload configuration parameter. By default, application code
  is now loaded after a worker forks. This couple with the new HUP
  handling can be used for dev servers to do hot code reloading. Using
  the preload flag can help a bit in small memory VM's.
* Allow people to pass command line arguments to WSGI applications. See:
  `examples/alt_spec.py
  <http://github.com/benoitc/gunicorn/raw/master/examples/alt_spec.py>`_
* Added an example gevent reloader configuration:
  `examples/example_gevent_reloader.py
  <http://github.com/benoitc/gunicorn/blob/master/examples/example_gevent_reloader.py>`_.
* New gevent worker "egg:gunicorn#gevent2", working with gevent.wsgi.
* Internal refactoring and various bug fixes.
* New documentation website.

## 0.9.1 / 2010-05-26

* Support https via X-Forwarded-Protocol or X-Forwarded-Ssl headers
* Fix configuration
* Remove -d options which was used instead of -D for daemon.
* Fix umask in unix socket

## 0.9.0 / 2010-05-24

* Added *when_ready* hook. Called just after the server is started
* Added *preload* setting. Load application code before the worker processes
  are forked.
* Refactored Config
* Fix pidfile
* Fix QUIT/HUP in async workers
* Fix reexec
* Documentation improvements

## 0.8.1 / 2010-04-29

* Fix builtins import in config
* Fix installation with pip
* Fix Tornado WSGI support
* Delay application loading until after processing all configuration

## 0.8.0 / 2010-04-22

* Refactored Worker management for better async support. Now use the -k option
  to set the type of request processing to use
* Added support for Tornado_

## 0.7.2 / 2010-04-15

* Added --spew option to help debugging (installs a system trace hook)
* Some fixes in async arbiters
* Fix a bug in start_response on error

## 0.7.1 / 2010-04-01

* Fix bug when responses have no body.

## 0.7.0 / 2010-03-26

* Added support for Eventlet_ and Gevent_ based workers.
* Added Websockets_ support
* Fix Chunked Encoding
* Fix SIGWINCH on OpenBSD_
* Fix `PEP 333`_ compliance for the write callable.

## 0.6.5 / 2010-03-11

* Fix pidfile handling
* Fix Exception Error

## 0.6.4 / 2010-03-08

* Use cStringIO for performance when possible.
* Fix worker freeze when a remote connection closes unexpectedly.

## 0.6.3 / 2010-03-07

* Make HTTP parsing faster.
* Various bug fixes

## 0.6.2 / 2010-03-01

* Added support for chunked response.
* Added proc_name option to the config file.
* Improved the HTTP parser. It now uses buffers instead of strings to store
  temporary data.
* Improved performance when sending responses.
* Workers are now murdered by age (the oldest is killed first).

## 0.6.1 / 2010-02-24

* Added gunicorn config file support for Django admin command
* Fix gunicorn config file. -c was broken.
* Removed TTIN/TTOU from workers which blocked other signals.

## 0.6.0 / 2010-02-22

* Added setproctitle support
* Change privilege switch behavior. We now work like NGINX, master keeps the
  permissions, new uid/gid permissions are only set for workers.

## 0.5.1 / 2010-02-22

* Fix umask
* Added Debian packaging

## 0.5.0 / 2010-02-20

* Added `configuration file <configuration.html>`_ handler.
* Added support for pre/post fork hooks
* Added support for before_exec hook
* Added support for unix sockets
* Added launch of workers processes under different user/group
* Added umask option
* Added SCRIPT_NAME support
* Better support of some exotic settings for Django projects
* Better support of Paste-compatible applications
* Some refactoring to make the code easier to hack
* Allow multiple keys in request and response headers

.. _Tornado: http://www.tornadoweb.org/
.. _`PEP 333`: https://www.python.org/dev/peps/pep-0333/
.. _Eventlet: http://eventlet.net/
.. _Gevent: http://www.gevent.org/
.. _OpenBSD: https://www.openbsd.org/
.. _Websockets: https://html.spec.whatwg.org/multipage/web-sockets.html

---

### Content 2011 News (`docs/content/2011-news.md`)

<span id="news-2011"></span>
# Changelog - 2011

## 0.13.4 / 2011-09-23

- fix util.closerange function used to prevent leaking fds on python 2.5
  (typo.md)

## 0.13.3 / 2011-09-19
- refactor gevent worker
- prevent leaking fds on reexec
- fix inverted request_time computation

## 0.13.2 / 2011-09-17

- Add support for Tornado 2.0 in tornado worker
- Improve access logs: allows customisation of the log format & add
  request time
- Logger module is now pluggable
- Improve graceful shutdown in Python versions >= 2.6
- Fix post_request root arity for compatibility
- Fix sendfile support
- Fix Django reloading

## 0.13.1 / 2011-08-22

- Fix unix socket. log argument was missing.

## 0.13.0 / 2011-08-22

- Improve logging: allows file-reopening and add access log file
  compatible with the `apache combined log format <http://httpd.apache.org/docs/2.0/logs.html#combined>`_
- Add the possibility to set custom SSL headers. X-Forwarded-Protocol
  and X-Forwarded-SSL are still the default
- New `on_reload` hook to customize how gunicorn spawn new workers on
  SIGHUP
- Handle projects with relative path in django_gunicorn command
- Preserve path parameters in PATH_INFO
- post_request hook now accepts the environ as argument.
- When stopping the arbiter, close the listener asap.
- Fix Django command `run_gunicorn` in settings reloading
- Fix Tornado_ worker exiting
- Fix the use of sendfile in wsgi.file_wrapper


## 0.12.2 / 2011-05-18

- Add wsgi.file_wrapper optimised for FreeBSD, Linux & MacOSX (use
  sendfile if available)
- Fix django run_gunicorn command. Make sure we reload the application
  code.
- Fix django localisation
- Compatible with gevent 0.14dev

## 0.12.1 / 2011-03-23

- Add "on_starting" hook. This hook can be used to set anything before
  the arbiter really start
- Support bdist_rpm in setup
- Improve content-length handling (pep 3333)
- Improve Django support
- Fix daemonizing (#142)
- Fix ipv6 handling


.. _Tornado: http://www.tornadoweb.org/

---

### Content 2012 News (`docs/content/2012-news.md`)

<span id="news-2012"></span>
# Changelog - 2012

## 0.17.0 / 2012-12-25

- allows gunicorn to bind to multiple address
- add SSL support
- add syslog support
- add nworkers_changed hook
- add response arg for post_request hook
- parse command line with argparse (replace deprecated optparse)
- fix PWD detection in arbiter
- miscellaneous PEP8 fixes

## 0.16.1 / 2012-11-19

- Fix packaging

## 0.16.0 / 2012-11-19

- **Added support for Python 3.2 & 3.3**
- Expose --pythonpath command to all gunicorn commands
- Honor $PORT environment variable, useful for deployment on heroku
- Removed support for Python 2.5
- Make sure we reopen the logs on the console
- Fix django settings module detection from path
- Reverted timeout for client socket. Fix issue on blocking issues.
- Fixed gevent worker

## 0.15.0 / 2012-10-18

- new documentation site on https://gunicorn.org
- new website on http://gunicorn.org
- add `haproxy PROXY protocol <http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt>`_ support
- add  ForwardedAllowIPS option: allows to filter Front-end's IPs
  allowed to handle X-Forwarded-* headers.
- add callable hooks for paster config
- add x-forwarded-proto as secure scheme default (Heroku is using this)
- allows gunicorn to load a pre-compiled application
- support file reopening & reexec for all loggers
- initialize the logging config file with defaults.
- set timeout for client socket (slow client DoS).
- NoMoreData, ChunkMissingTerminator, InvalidChunkSize are now
  IOError exceptions
- fix graceful shutdown in gevent
- fix limit request line check

## 0.14.6 / 2012-07-26


- fix gevent & subproces
- fix request line length check
- fix keepalive = 0
- fix tornado worker

## 0.14.5 / 2012-06-24

- fix logging during daemonisation

## 0.14.4 / 2012-06-24

- new --graceful-timeout option
- fix multiple issues with request limit
- more fixes in django settings resolutions
- fix gevent.core import
- fix keepalive=0 in eventlet worker
- fix handle_error display with the unix worker
- fix tornado.wsgi.WSGIApplication calling error

- **breaking change**: take the control on graceful reload back.
  graceful can't be overridden anymore using the on_reload function.

## 0.14.3 / 2012-05-15

- improvement: performance of http.body.Body.readline()
- improvement: log HTTP errors in access log like Apache
- improvement: display traceback when the worker fails to boot
- improvement: makes gunicorn work with gevent 1.0
- examples: websocket example now supports hybi13
- fix: reopen log files after initialization
- fix: websockets support
- fix: django1.4 support
- fix: only load the paster application 1 time

## 0.14.2 / 2012-03-16

- add validate_class validator: allows to use a class or a method to
  initialize the app during in-code configuration
- add support for max_requests in tornado worker
- add support for disabling x_forwarded_for_header in tornado worker
- gevent_wsgi is now an alias of gevent_pywsgi
- Fix gevent_pywsgi worker

## 0.14.1 / 2012-03-02

- fixing source archive, reducing its size

## 0.14.0 / 2012-02-27

- check if Request line is too large: You can now pass the parameter
  ``--limit-request-line`` or set the ``limit_request_line`` in your
  configuration file to set the max size of the request line in bytes.
- limit the number of headers fields and their size. Add
  ``--limit-request-field`` and ``limit-request-field-size`` settings
- add ``p`` variable to the log access format to log pidfile
- add ``{HeaderName}o`` variable to the logo access format to log the
  response header HeaderName
- request header is now logged with the variable ``{HeaderName}i`` in the
  access log file
- improve error logging
- support logging.configFile
- support django 1.4 in both gunicorn_django & run_gunicorn command
- improve reload in django run_gunicorn command (should just work now)
- allows people to set the ``X-Forwarded-For`` header key and disable it by
  setting an empty string.
- fix support of Tornado
- many other fixes.

---

### Content 2013 News (`docs/content/2013-news.md`)

<span id="news-2013"></span>
# Changelog - 2013

## 18.0 / 2013-08-26

- new: add ``-e/--env`` command line argument to pass an environment variables to
  gunicorn
- new: add ``--chdir`` command line argument to specified directory
  before apps loading.  - new: add wsgi.file_wrapper support in async workers
- new: add ``--paste`` command line argument to set the paster config file
- deprecated: the command ``gunicorn_django`` is now deprecated. You should now
  run your application with the WSGI interface installed with your project (see
  https://docs.djangoproject.com/en/1.4/howto/deployment/wsgi/gunicorn/) for
  more infos.
- deprecated:  the command ``gunicorn_paste`` is deprecated. You now should use
  the new ``--paste`` argument to set the configuration file of your paster
  application.
- fix: Removes unmatched leading quote from the beginning of the default access
  log format string
- fix: null timeout
- fix: gevent worker
- fix: don't reload the paster app when using pserve
- fix: after closing for error do not keep alive the connection
- fix: responses 1xx, 204 and 304 should not force the connection to be closed

## 17.5 / 2013-07-03

- new: add signals documentation
- new: add post_worker_init hook for workers
- new: try to use gunicorn.conf.py in current folder as the default
  config file.
- fix graceful timeout with the Eventlet worker
- fix: don't raise an error when closing the socket if already closed
- fix: fix --settings parameter for django application and try to find
  the django settings when using the ``gunicorn`` command.
- fix: give the initial global_conf to paster application
- fix: fix 'Expect: 100-continue' support on Python 3

### New versioning:

With this release, the versioning of Gunicorn is changing. Gunicorn is
stable since a long time and there is no point to release a "1.0" now.
It should have been done since a long time. 0.17 really meant it was the
17th stable version. From the beginning we have only 2 kind of
releases:

major release: releases with major changes or huge features added
services releases: fixes and minor features added So from now we will
apply the following versioning ``<major>.<service>``. For example ``17.5`` is a
service release.

## 0.17.4 / 2013-04-24

- fix unix socket address parsing

## 0.17.3 / 2013-04-23

- add systemd sockets support
- add ``python -m gunicorn.app.wsgiapp`` support
- improve logger class inheritance
- exit when the config file isn't found
- add the -R option to enable stdio inheritance in daemon mode
- don't close file descriptors > 3 in daemon mode
- improve STDOUT/STDERR logging
- fix pythonpath option
- fix pidfile creation on Python 3
- fix gevent worker exit
- fix ipv6 detection when the platform isn't supporting it

## 0.17.2 / 2013-01-07

- optimize readline
- make imports errors more visible when loading an app or a logging
  class
- fix tornado worker: don't pass ssl options if there are none
- fix PEP3333: accept only bytetrings in the response body
- fix support on CYGWIN platforms

## 0.17.1 / 2013-01-05

- add syslog facility name setting
- fix ``--version`` command line argument
- fix wsgi url_scheme for https

---

### Content 2014 News (`docs/content/2014-news.md`)

<span id="news-2014"></span>
# Changelog - 2014

!!! note
    Please see [news](news.md) for the latest changes.


## 19.1.1 / 2014-08-16

### Changes

### Core

- fix [Issue #835](https://github.com/benoitc/gunicorn/issues/835): display correct pid of already running instance
- fix [PR #833](https://github.com/benoitc/gunicorn/pull/833): fix `PyTest` class in setup.py.


### Logging

- fix [Issue #838](https://github.com/benoitc/gunicorn/issues/838): statsd logger, send statsd timing metrics in milliseconds
- fix [Issue #839](https://github.com/benoitc/gunicorn/issues/839): statsd logger, allows for empty log message while pushing
  metrics and restore worker number in DEBUG logs
- fix [Issue #850](https://github.com/benoitc/gunicorn/issues/850): add timezone to logging
- fix [Issue #853](https://github.com/benoitc/gunicorn/issues/853): Respect logger_class setting unless statsd is on

### AioHttp worker

- fix [Issue #830](https://github.com/benoitc/gunicorn/issues/830) make sure gaiohttp worker is shipped with gunicorn.

## 19.1 / 2014-07-26

### Changes

### Core

- fix [Issue #785](https://github.com/benoitc/gunicorn/issues/785): handle binary type address given to a client socket address
- fix graceful shutdown. make sure QUIT and TERMS signals are switched everywhere.
- [Issue #799](https://github.com/benoitc/gunicorn/issues/799): fix support loading config from module
- [Issue #805](https://github.com/benoitc/gunicorn/issues/805): fix check for file-like objects
- fix [Issue #815](https://github.com/benoitc/gunicorn/issues/815): args validation in WSGIApplication.init
- fix [Issue #787](https://github.com/benoitc/gunicorn/issues/787): check if we load a pyc file or not.


### Tornado worker

- fix [Issue #771](https://github.com/benoitc/gunicorn/issues/771): support tornado 4.0
- fix [Issue #783](https://github.com/benoitc/gunicorn/issues/783): x_headers error. The x-forwarded-headers option has been removed
  in `c4873681299212d6082cd9902740eef18c2f14f1
  <https://github.com/benoitc/gunicorn/commit/c4873681299212d6082cd9902740eef18c2f14f1>`_.
  The discussion is available on [PR #633](https://github.com/benoitc/gunicorn/pull/633).


### AioHttp worker

- fix: fetch all body in input. fix [Issue #803](https://github.com/benoitc/gunicorn/issues/803)
- fix: don't install the worker if python < 3.3
- fix [Issue #822](https://github.com/benoitc/gunicorn/issues/822): Support UNIX sockets in gaiohttp worker


### Async worker

- fix [Issue #790](https://github.com/benoitc/gunicorn/issues/790): StopIteration shouldn't be caught at this level.


### Logging

- add statsd logging handler fix [Issue #748](https://github.com/benoitc/gunicorn/issues/748)


### Paster

- fix [Issue #809](https://github.com/benoitc/gunicorn/issues/809): Set global logging configuration from a Paste config.


### Extra

- fix RuntimeError in gunicorn.reloader ([Issue #807](https://github.com/benoitc/gunicorn/issues/807))


### Documentation

- update faq: put a note on how `watch logs in the console
  <https://gunicorn.org/faq/#why-i-dont-see-any-logs-in-the-console>`_
  since many people asked for it.


## 19.0 / 2014-06-12

Gunicorn 19.0 is a major release with new features and fixes. This
version improve a lot the usage of Gunicorn with python 3 by adding `two
new workers <https://gunicorn.org/design/#asyncio-workers>`_
to it: `gthread` a fully threaded async worker using futures and `gaiohttp` a
worker using asyncio.


### Breaking Changes

### Switch QUIT and TERM signals

With this change, when gunicorn receives a QUIT all the workers are
killed immediately and exit and TERM is used for the graceful shutdown.

Note: the old behaviour was based on the NGINX but the new one is more
correct according the following doc:

https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html

also it is complying with the way the signals are sent by heroku:

https://devcenter.heroku.com/articles/python-faq#what-constraints-exist-when-developing-applications-on-heroku

### Deprecations

`run_gunicorn`, `gunicorn_django` and `gunicorn_paster` are now
completely deprecated and will be removed in the next release. Use the
`gunicorn` command instead.


### Changes

### core

- add aiohttp worker named `gaiohttp` using asyncio. Full async worker
  on python 3.
- fix HTTP-violating excess whitespace in write_error output
- fix: try to log what happened in the worker after a timeout, add a
  `worker_abort` hook on SIGABRT signal.
- fix: save listener socket name in workers so we can handle buffered
  keep-alive requests after the listener has closed.
- add on_exit hook called just before exiting gunicorn.
- add support for python 3.4
- fix: do not swallow unexpected errors when reaping
- fix: remove incompatible SSL option with python 2.6
- add new async gthread worker and `--threads` options allows to set multiple
  threads to listen on connection
- deprecate `gunicorn_django` and `gunicorn_paster`
- switch QUIT and TERM signal
- reap workers in SIGCHLD handler
- add universal wheel support
- use `email.utils.formatdate` in gunicorn.util.http_date
- deprecate the `--debug` option
- fix: log exceptions that occur after response start …
- allows loading of applications from `.pyc` files (#693)
- fix: issue #691, raw_env config file parsing
- use a dynamic timeout to wait for the optimal time. (Reduce power
  usage)
- fix python3 support when notifying the arbiter
- add: honor $WEB_CONCURRENCY environment variable. Useful for heroku
  setups.
- add: include tz offset in access log
- add: include access logs in the syslog handler.
- add --reload option for code reloading
- add the capability to load `gunicorn.base.Application` without the loading of
  the arguments of the command line. It allows you to [embed gunicorn in   your own application](custom.md).
- improve: set wsgi.multithread to True for async workers
- fix logging: make sure to redirect wsgi.errors when needed
- add: syslog logging can now be done to a unix socket
- fix logging: don't try to redirect stdout/stderr to the logfile.
- fix logging: don't propagate log
- improve logging: file option can be overridden by the gunicorn options
  `--error-logfile` and `--access-logfile` if they are given.
- fix: don't override SERVER_* by the Host header
- fix: handle_error
- add more option to configure SSL
- fix: sendfile with SSL
- add: worker_int callback (to react on SIGTERM)
- fix: don't depend on entry point for internal classes, now absolute
  modules path can be given.
- fix: Error messages are now encoded in latin1
- fix: request line length check
- improvement: proxy_allow_ips: Allow proxy protocol if "*" specified
- fix: run worker's `setup` method  before setting num_workers
- fix: FileWrapper inherit from `object` now
- fix: Error messages are now encoded in latin1
- fix: don't spam the console on SIGWINCH.
- fix: logging -don't stringify T and D logging atoms (#621)
- add support for the latest django version
- deprecate `run_gunicorn` django option
- fix: sys imported twice


### gevent worker

- fix: make sure to stop all listeners
- fix: monkey patching is now done in the worker
- fix: "global name 'hub' is not defined"
- fix: reinit `hub` on old versions of gevent
- support gevent 1.0
- fix: add subprocess in monkey patching
- fix: add support for multiple listener


### eventlet worker

- fix: merge duplicate EventletWorker.init_process method (fixes #657)
- fix: missing errno import for eventlet sendfile patch
- fix: add support for multiple listener


### tornado worker

- add graceful stop support

---

### Content 2015 News (`docs/content/2015-news.md`)

<span id="news-2015"></span>
# Changelog - 2015

!!! note
    Please see [news](news.md) for the latest changes.


## 19.4.3 / 2015/12/30

- fix: don't check if a file is writable using os.stat with SELINUX ([Issue #1171](https://github.com/benoitc/gunicorn/issues/1171))

## 19.4.2 / 2015/12/29

### Core

- improvement: handle HaltServer in manage_workers ([Issue #1095](https://github.com/benoitc/gunicorn/issues/1095))
- fix: Do not rely on sendfile sending requested count ([Issue #1155](https://github.com/benoitc/gunicorn/issues/1155))
- fix: claridy --no-sendfile default ([Issue #1156](https://github.com/benoitc/gunicorn/issues/1156))
- fix: LoggingCatch sendfile failure from no file descriptor ([Issue #1160](https://github.com/benoitc/gunicorn/issues/1160))

### Logging

- fix: Always send access log to syslog if syslog is on
- fix: check auth before trying to own a file ([Issue #1157](https://github.com/benoitc/gunicorn/issues/1157))


### Documentation

- fix: Fix Slowloris broken link. ([Issue #1142](https://github.com/benoitc/gunicorn/issues/1142))
- Tweak markup in faq.rst

### Testing

- fix: gaiohttp test ([Issue #1164](https://github.com/benoitc/gunicorn/issues/1164))

## 19.4.1 / 2015/11/25

- fix tornado worker ([Issue #1154](https://github.com/benoitc/gunicorn/issues/1154))

## 19.4.0 / 2015/11/20

### Core

- fix: make sure that a user is able to access to the logs after dropping a
  privilege ([Issue #1116](https://github.com/benoitc/gunicorn/issues/1116))
- improvement: inherit the `Exception` class where it needs to be ([Issue #997](https://github.com/benoitc/gunicorn/issues/997))
- fix: make sure headers are always encoded as latin1 RFC 2616 ([Issue #1102](https://github.com/benoitc/gunicorn/issues/1102))
- improvement: reduce arbiter noise ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078))
- fix: don't close the unix socket when the worker exit ([Issue #1088](https://github.com/benoitc/gunicorn/issues/1088))
- improvement: Make last logged worker count an explicit instance var ([Issue #1078](https://github.com/benoitc/gunicorn/issues/1078))
- improvement: prefix config file with its type ([Issue #836](https://github.com/benoitc/gunicorn/issues/836))
- improvement: pidfile handing ([Issue #1042](https://github.com/benoitc/gunicorn/issues/1042))
- fix: catch OSError as well as ValueError on race condition ([Issue #1052](https://github.com/benoitc/gunicorn/issues/1052))
- improve support of ipv6 by backporting urlparse.urlsplit from Python 2.7 to
  Python 2.6.
- fix: raise InvalidRequestLine when the line contains malicious data
  ([Issue #1023](https://github.com/benoitc/gunicorn/issues/1023))
- fix: fix argument to disable sendfile
- fix: add gthread to the list of supported workers ([Issue #1011](https://github.com/benoitc/gunicorn/issues/1011))
- improvement: retry socket binding up to five times upon EADDRNOTAVAIL
  ([Issue #1004](https://github.com/benoitc/gunicorn/issues/1004))
- **breaking change**: only honor headers that can be encoded in ascii to comply to
  the RFC 7230 (See [Issue #1151](https://github.com/benoitc/gunicorn/issues/1151)).

### Logging

- add new parameters to access log ([Issue #1132](https://github.com/benoitc/gunicorn/issues/1132))
- fix: make sure that files handles are correctly reopened on HUP
  ([Issue #627](https://github.com/benoitc/gunicorn/issues/627))
- include request URL in error message ([Issue #1071](https://github.com/benoitc/gunicorn/issues/1071))
- get username in access logs ([Issue #1069](https://github.com/benoitc/gunicorn/issues/1069))
- fix statsd logging support on Python 3 ([Issue #1010](https://github.com/benoitc/gunicorn/issues/1010))

### Testing

- use last version of mock.
- many fixes in Travis CI support
- miscellaneous improvements in tests

### Thread worker

- fix: Fix self.nr usage in ThreadedWorker so that auto restart works as
  expected ([Issue #1031](https://github.com/benoitc/gunicorn/issues/1031))

### Gevent worker

- fix quit signal handling ([Issue #1128](https://github.com/benoitc/gunicorn/issues/1128))
- add support for Python 3 ([Issue #1066](https://github.com/benoitc/gunicorn/issues/1066))
- fix: make graceful shutdown thread-safe ([Issue #1032](https://github.com/benoitc/gunicorn/issues/1032))

### Tornado worker

- fix ssl options ([Issue #1146](https://github.com/benoitc/gunicorn/issues/1146), [Issue #1135](https://github.com/benoitc/gunicorn/issues/1135))
- don't check timeout when stopping gracefully ([Issue #1106](https://github.com/benoitc/gunicorn/issues/1106))

### AIOHttp worker

- add SSL support ([Issue #1105](https://github.com/benoitc/gunicorn/issues/1105))

### Documentation

- fix link to proc name setting ([Issue #1144](https://github.com/benoitc/gunicorn/issues/1144))
- fix worker class documentation ([Issue #1141](https://github.com/benoitc/gunicorn/issues/1141), [Issue #1104](https://github.com/benoitc/gunicorn/issues/1104))
- clarify graceful timeout documentation ([Issue #1137](https://github.com/benoitc/gunicorn/issues/1137))
- don't duplicate NGINX config files examples ([Issue #1050](https://github.com/benoitc/gunicorn/issues/1050), [Issue #1048](https://github.com/benoitc/gunicorn/issues/1048))
- add `web.py` framework example ([Issue #1117](https://github.com/benoitc/gunicorn/issues/1117))
- update Debian/Ubuntu installations instructions ([Issue #1112](https://github.com/benoitc/gunicorn/issues/1112))
- clarify `pythonpath` setting description ([Issue #1080](https://github.com/benoitc/gunicorn/issues/1080))
- tweak some example for python3
- clarify `sendfile` documentation
- miscellaneous typos in source code comments (thanks!)
- clarify why REMOTE_ADD may not be the user's IP address ([Issue #1037](https://github.com/benoitc/gunicorn/issues/1037))


### Misc

- fix: reloader should survive SyntaxError ([Issue #994](https://github.com/benoitc/gunicorn/issues/994))
- fix: expose the reloader class to the worker.



## 19.3.0 / 2015/03/06

### Core

- fix: [Issue #978](https://github.com/benoitc/gunicorn/issues/978) make sure a listener is inheritable
- add `check_config` class method to workers
- fix: [Issue #983](https://github.com/benoitc/gunicorn/issues/983) fix select timeout in sync worker with multiple
  connections
- allows workers to access to the reloader. close [Issue #984](https://github.com/benoitc/gunicorn/issues/984)
- raise TypeError instead of AssertionError

### Logging

- make Logger.loglevel a class attribute

### Documentation

- fix: [Issue #988](https://github.com/benoitc/gunicorn/issues/988) fix syntax errors in examples/gunicorn_rc


## 19.2.1 / 2015/02/4

### Logging

- expose loglevel in the Logger class

### AsyncIO worker (gaiohttp.md)

- fix [Issue #977](https://github.com/benoitc/gunicorn/issues/977) fix initial crash

### Documentation

- document security mailing-list in the contributing page.

## 19.2 / 2015/01/30

### Core

- optimize the sync workers when listening on a single interface
- add `--sendfile` settings to enable/disable sendfile. fix [Issue #856](https://github.com/benoitc/gunicorn/issues/856) .
- add the selectors module to the code base. [Issue #886](https://github.com/benoitc/gunicorn/issues/886)
- add `--max-requests-jitter` setting to set the maximum jitter to add to the
  max-requests setting.
- fix [Issue #899](https://github.com/benoitc/gunicorn/issues/899) propagate proxy_protocol_info to keep-alive requests
- fix [Issue #863](https://github.com/benoitc/gunicorn/issues/863) worker timeout: dynamic timeout has been removed
- fix: Avoid world writable file

### Logging

- fix [Issue #941](https://github.com/benoitc/gunicorn/issues/941)  set logconfig default to paster more trivially
- add statsd-prefix config setting: set the prefix to use when emitting statsd
  metrics
- [Issue #832](https://github.com/benoitc/gunicorn/issues/832) log to console by default

### Thread Worker

- fix [Issue #908](https://github.com/benoitc/gunicorn/issues/908) make sure the worker can continue to accept requests

### Eventlet Worker

- fix [Issue #867](https://github.com/benoitc/gunicorn/issues/867) Fix eventlet shutdown to actively shut down the workers.

### Documentation

Many improvements and fixes have been done, see the detailed changelog for
more information.

---

### Content 2016 News (`docs/content/2016-news.md`)

<span id="news-2016"></span>
# Changelog - 2016

!!! note
    Please see [news](news.md) for the latest changes


## 19.6.0 / 2016/05/21

### Core & Logging

- improvement of the binary upgrade behaviour using USR2: remove file locking ([Issue #1270](https://github.com/benoitc/gunicorn/issues/1270))
- add the ``--capture-output`` setting to capture stdout/stderr tot the log
  file ([Issue #1271](https://github.com/benoitc/gunicorn/issues/1271))
- Allow disabling ``sendfile()`` via the ``SENDFILE`` environment variable
  ([Issue #1252](https://github.com/benoitc/gunicorn/issues/1252))
- fix reload under pycharm ([Issue #1129](https://github.com/benoitc/gunicorn/issues/1129))

### Workers

- fix: make sure to remove the signal from the worker pipe ([Issue #1269](https://github.com/benoitc/gunicorn/issues/1269))
- fix: **gthread** worker, handle removed socket in the select loop
  ([Issue #1258](https://github.com/benoitc/gunicorn/issues/1258))

## 19.5.0 / 2016/05/10

### Core

- fix: Ensure response to HEAD request won't have message body
- fix: lock domain socket and remove on last arbiter exit ([Issue #1220](https://github.com/benoitc/gunicorn/issues/1220))
- improvement: use EnvironmentError instead of socket.error ([Issue #939](https://github.com/benoitc/gunicorn/issues/939))
- add: new ``FORWARDED_ALLOW_IPS`` environment variable ([Issue #1205](https://github.com/benoitc/gunicorn/issues/1205))
- fix: infinite recursion when destroying sockets ([Issue #1219](https://github.com/benoitc/gunicorn/issues/1219))
- fix: close sockets on shutdown ([Issue #922](https://github.com/benoitc/gunicorn/issues/922))
- fix: clean up sys.exc_info calls to drop circular refs ([Issue #1228](https://github.com/benoitc/gunicorn/issues/1228))
- fix: do post_worker_init after load_wsgi ([Issue #1248](https://github.com/benoitc/gunicorn/issues/1248))

### Workers

- fix access logging in gaiohttp worker ([Issue #1193](https://github.com/benoitc/gunicorn/issues/1193))
- eventlet: handle QUIT in a new coroutine ([Issue #1217](https://github.com/benoitc/gunicorn/issues/1217))
- gevent: remove obsolete exception clauses in run ([Issue #1218](https://github.com/benoitc/gunicorn/issues/1218))
- tornado: fix extra "Server" response header ([Issue #1246](https://github.com/benoitc/gunicorn/issues/1246))
- fix: unblock the wait loop under python 3.5 in sync worker ([Issue #1256](https://github.com/benoitc/gunicorn/issues/1256))

### Logging

- fix: log message for listener reloading ([Issue #1181](https://github.com/benoitc/gunicorn/issues/1181))
- Let logging module handle traceback printing ([Issue #1201](https://github.com/benoitc/gunicorn/issues/1201))
- improvement: Allow configuring logger_class with statsd_host ([Issue #1188](https://github.com/benoitc/gunicorn/issues/1188))
- fix: traceback formatting ([Issue #1235](https://github.com/benoitc/gunicorn/issues/1235))
- fix: print error logs on stderr and access logs on stdout ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184))


### Documentation

- Simplify installation instructions in gunicorn.org ([Issue #1072](https://github.com/benoitc/gunicorn/issues/1072))
- Fix URL and default worker type in example_config ([Issue #1209](https://github.com/benoitc/gunicorn/issues/1209))
- update django doc url to 1.8 lts ([Issue #1213](https://github.com/benoitc/gunicorn/issues/1213))
- fix: miscellaneous wording corrections ([Issue #1216](https://github.com/benoitc/gunicorn/issues/1216))
- Add PSF License Agreement of selectors.py to NOTICE (:issue: `1226`)
- document LOGGING overriding ([Issue #1051](https://github.com/benoitc/gunicorn/issues/1051))
- put a note that error logs are only errors from Gunicorn ([Issue #1124](https://github.com/benoitc/gunicorn/issues/1124))
- add a note about the requirements of the threads workers under python 2.x ([Issue #1200](https://github.com/benoitc/gunicorn/issues/1200))
- add access_log_format to config example ([Issue #1251](https://github.com/benoitc/gunicorn/issues/1251))

### Tests

- Use more pytest.raises() in test_http.py


## 19.4.5 / 2016/01/05

- fix: NameError fileno in gunicorn.http.wsgi ([Issue #1178](https://github.com/benoitc/gunicorn/issues/1178))

## 19.4.4 / 2016/01/04

- fix: check if a fileobject can be used with sendfile(2.md) ([Issue #1174](https://github.com/benoitc/gunicorn/issues/1174))
- doc: be more descriptive in errorlog option ([Issue #1173](https://github.com/benoitc/gunicorn/issues/1173))

---

### Content 2017 News (`docs/content/2017-news.md`)

<span id="news-2017"></span>
# Changelog - 2017

!!! note
    Please see [news](news.md) for the latest changes


## 19.7.1 / 2017/03/21

- fix: continue if SO_REUSEPORT seems to be available but fails ([Issue #1480](https://github.com/benoitc/gunicorn/issues/1480))
- fix: support non-decimal values for the umask command line option ([Issue #1325](https://github.com/benoitc/gunicorn/issues/1325))

## 19.7.0 / 2017/03/01

- The previously deprecated ``gunicorn_django`` command has been removed.
  Use the [gunicorn-cmd](run.md#gunicorn) command-line interface instead.
- The previously deprecated ``django_settings`` setting has been removed.
  Use the [raw-env](reference/settings.md#raw_env) setting instead.
- The default value of [ssl-version](reference/settings.md#ssl_version) has been changed from
  ``ssl.PROTOCOL_TLSv1`` to ``ssl.PROTOCOL_SSLv23``.
- fix: initialize the group access list when initgroups is set ([Issue #1297](https://github.com/benoitc/gunicorn/issues/1297))
- add environment variables to gunicorn access log format ([Issue #1291](https://github.com/benoitc/gunicorn/issues/1291))
- add --paste-global-conf option ([Issue #1304](https://github.com/benoitc/gunicorn/issues/1304))
- fix: print access logs to STDOUT ([Issue #1184](https://github.com/benoitc/gunicorn/issues/1184))
- remove upper limit on max header size config ([Issue #1313](https://github.com/benoitc/gunicorn/issues/1313))
- fix: print original exception on AppImportError ([Issue #1334](https://github.com/benoitc/gunicorn/issues/1334))
- use SO_REUSEPORT if available ([Issue #1344](https://github.com/benoitc/gunicorn/issues/1344))
- `fix leak <https://github.com/benoitc/gunicorn/commit/b4c41481e2d5ef127199a4601417a6819053c3fd>`_ of duplicate file descriptor for bound sockets.
- add --reload-engine option, support inotify and other backends ([Issue #1368](https://github.com/benoitc/gunicorn/issues/1368), [Issue #1459](https://github.com/benoitc/gunicorn/issues/1459))
- fix: reject request with invalid HTTP versions
- add ``child_exit`` callback ([Issue #1394](https://github.com/benoitc/gunicorn/issues/1394))
- add support for eventlets _AlreadyHandled object ([Issue #1406](https://github.com/benoitc/gunicorn/issues/1406))
- format boot tracebacks properly with reloader ([Issue #1408](https://github.com/benoitc/gunicorn/issues/1408))
- refactor socket activation and fd inheritance for better support of SystemD ([Issue #1310](https://github.com/benoitc/gunicorn/issues/1310))
- fix: o fds are given by default in gunicorn ([Issue #1423](https://github.com/benoitc/gunicorn/issues/1423))
- add ability to pass settings to GUNICORN_CMD_ARGS environment variable which helps in container world ([Issue #1385](https://github.com/benoitc/gunicorn/issues/1385))
- fix:  catch access denied to pid file ([Issue #1091](https://github.com/benoitc/gunicorn/issues/1091))
-  many additions and improvements to the documentation

### Breaking Change

- **Python 2.6.0** is the last supported version

---

### Content 2018 News (`docs/content/2018-news.md`)

<span id="news-2018"></span>
# Changelog - 2018

!!! note
    Please see [news](news.md) for the latest changes


## 19.9.0 / 2018/07/03

- fix: address a regression that prevented syslog support from working
  ([Issue #1668](https://github.com/benoitc/gunicorn/issues/1668), [PR #1773](https://github.com/benoitc/gunicorn/pull/1773))
- fix: correctly set `REMOTE_ADDR` on versions of Python 3 affected by
  `Python Issue 30205 <https://bugs.python.org/issue30205>`_
  ([Issue #1755](https://github.com/benoitc/gunicorn/issues/1755), [PR #1796](https://github.com/benoitc/gunicorn/pull/1796))
- fix: show zero response length correctly in access log ([PR #1787](https://github.com/benoitc/gunicorn/pull/1787))
- fix: prevent raising `AttributeError` when ``--reload`` is not passed
  in case of a `SyntaxError` raised from the WSGI application.
  ([Issue #1805](https://github.com/benoitc/gunicorn/issues/1805), [PR #1806](https://github.com/benoitc/gunicorn/pull/1806))
- The internal module ``gunicorn.workers.async`` was renamed to ``gunicorn.workers.base_async``
  since ``async`` is now a reserved word in Python 3.7.
  ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527))

## 19.8.1 / 2018/04/30

- fix: secure scheme headers when bound to a unix socket
  ([Issue #1766](https://github.com/benoitc/gunicorn/issues/1766), [PR #1767](https://github.com/benoitc/gunicorn/pull/1767))

## 19.8.0 / 2018/04/28

- Eventlet 0.21.0 support ([Issue #1584](https://github.com/benoitc/gunicorn/issues/1584))
- Tornado 5 support ([Issue #1728](https://github.com/benoitc/gunicorn/issues/1728), [PR #1752](https://github.com/benoitc/gunicorn/pull/1752))
- support watching additional files with ``--reload-extra-file``
  ([PR #1527](https://github.com/benoitc/gunicorn/pull/1527))
- support configuring logging with a dictionary with ``--logging-config-dict``
  ([Issue #1087](https://github.com/benoitc/gunicorn/issues/1087), [PR #1110](https://github.com/benoitc/gunicorn/pull/1110), [PR #1602](https://github.com/benoitc/gunicorn/pull/1602))
- add support for the ``--config`` flag in the ``GUNICORN_CMD_ARGS`` environment
  variable ([Issue #1576](https://github.com/benoitc/gunicorn/issues/1576), [PR #1581](https://github.com/benoitc/gunicorn/pull/1581))
- disable ``SO_REUSEPORT`` by default and add the ``--reuse-port`` setting
  ([Issue #1553](https://github.com/benoitc/gunicorn/issues/1553), [Issue #1603](https://github.com/benoitc/gunicorn/issues/1603), [PR #1669](https://github.com/benoitc/gunicorn/pull/1669))
- fix: installing `inotify` on MacOS no longer breaks the reloader
  ([Issue #1540](https://github.com/benoitc/gunicorn/issues/1540), [PR #1541](https://github.com/benoitc/gunicorn/pull/1541))
- fix: do not throw ``TypeError`` when ``SO_REUSEPORT`` is not available
  ([Issue #1501](https://github.com/benoitc/gunicorn/issues/1501), [PR #1491](https://github.com/benoitc/gunicorn/pull/1491))
- fix: properly decode HTTP paths containing certain non-ASCII characters
  ([Issue #1577](https://github.com/benoitc/gunicorn/issues/1577), [PR #1578](https://github.com/benoitc/gunicorn/pull/1578))
- fix: remove whitespace when logging header values under gevent ([PR #1607](https://github.com/benoitc/gunicorn/pull/1607))
- fix: close unlinked temporary files ([Issue #1327](https://github.com/benoitc/gunicorn/issues/1327), [PR #1428](https://github.com/benoitc/gunicorn/pull/1428))
- fix: parse ``--umask=0`` correctly ([Issue #1622](https://github.com/benoitc/gunicorn/issues/1622), [PR #1632](https://github.com/benoitc/gunicorn/pull/1632))
- fix: allow loading applications using relative file paths
  ([Issue #1349](https://github.com/benoitc/gunicorn/issues/1349), [PR #1481](https://github.com/benoitc/gunicorn/pull/1481))
- fix: force blocking mode on the gevent sockets ([Issue #880](https://github.com/benoitc/gunicorn/issues/880), [PR #1616](https://github.com/benoitc/gunicorn/pull/1616))
- fix: preserve leading `/` in request path ([Issue #1512](https://github.com/benoitc/gunicorn/issues/1512), [PR #1511](https://github.com/benoitc/gunicorn/pull/1511))
- fix: forbid contradictory secure scheme headers
- fix: handle malformed basic authentication headers in access log
  ([Issue #1683](https://github.com/benoitc/gunicorn/issues/1683), [PR #1684](https://github.com/benoitc/gunicorn/pull/1684))
- fix: defer handling of ``USR1`` signal to a new greenlet under gevent
  ([Issue #1645](https://github.com/benoitc/gunicorn/issues/1645), [PR #1651](https://github.com/benoitc/gunicorn/pull/1651))
- fix: the threaded worker would sometimes close the wrong keep-alive
  connection under Python 2 ([Issue #1698](https://github.com/benoitc/gunicorn/issues/1698), [PR #1699](https://github.com/benoitc/gunicorn/pull/1699))
- fix: re-open log files on ``USR1`` signal using ``handler._open`` to
  support subclasses of ``FileHandler`` ([Issue #1739](https://github.com/benoitc/gunicorn/issues/1739), [PR #1742](https://github.com/benoitc/gunicorn/pull/1742))
- deprecation: the ``gaiohttp`` worker is deprecated, see the
  [worker-class](reference/settings.md#worker_class) documentation for more information
  ([Issue #1338](https://github.com/benoitc/gunicorn/issues/1338), [PR #1418](https://github.com/benoitc/gunicorn/pull/1418), [PR #1569](https://github.com/benoitc/gunicorn/pull/1569))

---

### Content 2019 News (`docs/content/2019-news.md`)

<span id="news-2019"></span>
# Changelog - 2019

!!! note
    Please see [news](news.md) for the latest changes


## 20.0.4 / 2019/11/26

- fix binding a socket using the file descriptor
- remove support for the `bdist_rpm` build

## 20.0.3 / 2019/11/24

- fixed load of a config file without a Python extension
- fixed `socketfromfd.fromfd` when defaults are not set

!!! note
    ```
    ## 20.0.2 / 2019/11/23
    
    - fix changelog
    
    ## 20.0.1 / 2019/11/23
    
    - fixed the way the config module is loaded. `__file__` is now available
    - fixed `wsgi.input_terminated`. It is always true.
    - use the highest protocol version of openssl by default
    - only support Python >= 3.5
    - added `__repr__` method to `Config` instance
    - fixed support of AIX platform and musl libc in  `socketfromfd.fromfd` function
    - fixed support of applications loaded from a factory function
    - fixed chunked encoding support to prevent any `request smuggling <https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn>`_
    - Capture os.sendfile before patching in gevent and eventlet workers.
      fix `RecursionError`.
    - removed locking in reloader when adding new files
    - load the WSGI application before the loader to pick up all files

{note}
as documented in Flask and other places.
```
## 19.10.0 / 2019/11/23

- unblock select loop during reload of a sync worker
- security fix: http desync attack
- handle `wsgi.input_terminated`
- added support for str and bytes in unix  socket addresses
- fixed `max_requests` setting
- headers values are now encoded as LATN1, not ASCII
- fixed `InotifyReloadeder`:  handle `module.__file__` is None
- fixed compatibility with tornado 6
- fixed root logging
- Prevent removalof unix sockets from `reuse_port`
- Clear tornado ioloop before os.fork
- Miscellaneous fixes and improvement for linting using Pylint

## 20.0 / 2019/10/30

- Fixed `fdopen` `RuntimeWarning` in Python 3.8
- Added  check and exception for str type on value in Response process_headers method.
- Ensure WSGI header value is string before conducting regex search on it.
- Added pypy3 to list of tested environments
- Grouped `StopIteration` and `KeyboardInterrupt` exceptions with same body together in Arbiter.run()
- Added `setproctitle` module to `extras_require` in setup.py
- Avoid unnecessary chown of temporary files
- Logging: Handle auth type case insensitively
- Removed `util.import_module`
- Removed fallback for `types.SimpleNamespace` in tests utils
- Use `SourceFileLoader` instead instead of `execfile_`
- Use `importlib` instead of `__import__` and eval`
- Fixed eventlet patching
- Added optional `datadog <https://www.datadoghq.com>`_ tags for statsd metrics
- Header values now are encoded using latin-1, not ascii.
- Rewritten `parse_address` util added test
- Removed redundant super() arguments
- Simplify `futures` import in gthread module
- Fixed worker_connections` setting to also affects the Gthread worker type
- Fixed setting max_requests
- Bump minimum Eventlet and Gevent versions to 0.24 and 1.4
- Use Python default SSL cipher list by default
- handle `wsgi.input_terminated` extension
- Simplify Paste Deployment documentation
- Fix root logging: root and logger are same level.
- Fixed typo in ssl_version documentation
- Documented  systemd deployment unit examples
- Added systemd sd_notify support
- Fixed typo in gthread.py
- Added `tornado <https://www.tornadoweb.org/>`_ 5 and  6 support
- Declare our setuptools dependency
- Added support to `--bind` to open file descriptors
- Document how to serve WSGI app modules from Gunicorn
- Provide guidance on X-Forwarded-For access log in documentation
- Add support for named constants in the `--ssl-version` flag
- Clarify log format usage of header & environment in documentation
- Fixed systemd documentation to properly setup gunicorn unix socket
- Prevent removal unix socket for reuse_port
- Fix `ResourceWarning` when reading a Python config module
- Remove unnecessary call to dict keys method
- Support str and bytes for UNIX socket addresses
- fixed `InotifyReloadeder`:  handle `module.__file__` is None
- `/dev/shm` as a convenient alternative to making your own tmpfs mount in fchmod FAQ
- fix examples to work on python3
- Fix typo in `--max-requests` documentation
- Clear tornado ioloop before os.fork
- Miscellaneous fixes and improvement for linting using Pylint

### Breaking Change

- Removed gaiohttp worker
- Drop support for Python 2.x
- Drop support for EOL Python 3.2 and 3.3
- Drop support for Paste Deploy server blocks

---

### Content 2020 News (`docs/content/2020-news.md`)

<span id="news-2020"></span>
# Changelog - 2020

!!! note
    Please see [news](news.md) for the latest changes

---

### Content 2021 News (`docs/content/2021-news.md`)

<span id="news-2021"></span>
# Changelog - 2021

!!! note
    Please see [news](news.md) for the latest changes


## 20.1.0 - 2021-02-12

- document WEB_CONCURRENCY is set by, at least, Heroku
- capture peername from accept: Avoid calls to getpeername by capturing the peer name returned by
  accept
- log a warning when a worker was terminated due to a signal
- fix tornado usage with latest versions of Django 
- add support for python -m gunicorn
- fix systemd socket activation example
- allows to set wsgi application in config file using `wsgi_app`
- document `--timeout = 0`
- always close a connection when the number of requests exceeds the max requests
- Disable keepalive during graceful shutdown
- kill tasks in the gthread workers during upgrade
- fix latency in gevent worker when accepting new requests
- fix file watcher: handle errors when new worker reboot and ensure the list of files is kept
- document the default name and path of the configuration file
- document how variable impact configuration
- document the `$PORT` environment variable
- added milliseconds option to request_time in access_log
- added PIP requirements to be used for example
- remove version from the Server header
- fix sendfile: use `socket.sendfile` instead of `os.sendfile`
- reloader: use  absolute path to prevent empty to prevent0 `InotifyError` when a file 
  is added to the working directory
- Add --print-config option to print the resolved settings at startup.
- remove the `--log-dict-config` CLI flag because it never had a working format
  (the `logconfig_dict` setting in configuration files continues to work)


### Breaking changes

- minimum version is Python 3.5
- remove version from the Server header 

** Documentation **



** Others **

- miscellaneous changes in the code base to be a better citizen with Python 3
- remove dead code
- fix documentation generation

---

### Content 2023 News (`docs/content/2023-news.md`)

<span id="news-2023"></span>
# Changelog - 2023

## 21.2.0 - 2023-07-19

- fix thread worker: revert change considering connection as idle . 

!!! note
    This is fixing the bad file description error.
    
    21.1.0 - 2023-07-18


===================

- fix thread worker: fix socket removal from the queue

## 21.0.1 - 2023-07-17

- fix documentation build

## 21.0.0 - 2023-07-17

- support python 3.11
- fix gevent and eventlet workers
- fix threads support (gththread.md): improve performance and unblock requests
- SSL: now use SSLContext object
- HTTP parser: miscellaneous fixes
- remove unnecessary setuid calls
- fix testing
- improve logging
- miscellaneous fixes to core engine

*** RELEASE NOTE ***

We made this release major to start our new release cycle. More info will be provided on our discussion forum.

---

### Content 2024 News (`docs/content/2024-news.md`)

<span id="news-2024"></span>
# Changelog - 2024

## 23.0.0 - 2024-08-10

- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167))
- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079))
- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688))
- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))
- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))
- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257))
- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336))
- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258))
- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))
- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))

!!! note
    - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release
    - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted
    - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0


### Breaking changes

- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255))
- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253))
- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260))
- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies.

## 22.0.0 - 2024-04-17

- use `utime` to notify workers liveness 
- migrate setup to pyproject.toml
- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors)
- parsing additional requests is no longer attempted past unsupported request framing
- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits)
- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error
- Trailer fields are no longer inspected for headers indicating secure scheme
- support Python 3.12

### Breaking changes

- minimum version is Python 3.7
- the limitations on valid characters in the HTTP method have been bounded to Internet Standards
- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md)
- HTTP methods are no longer casefolded by default (IANA method registry contains none affected)
- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md)
- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported)
- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted
- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software
- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits)
- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling)
- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies)


### Security

- fix CVE-2024-1135

---

### Content 2026 News (`docs/content/2026-news.md`)

<span id="news-2026"></span>
# Changelog - 2026

## 26.2.2 - 2026-09-06

### Bug Fixes

- **ASGI worker closed HTTP/1.1 connections without saying so**: when the
  worker would not reuse a connection (the client sent `Connection: close`,
  keepalive is disabled, the worker is shutting down, or the response trips
  `max_requests`) it closed the socket without a `Connection: close` header, so
  a client treated the response as persistent and reused a socket the server
  had already closed. The header is now sent whenever the connection will close
  ([#3726](https://github.com/benoitc/gunicorn/pull/3726)).

## 26.2.1 - 2026-09-05

### Bug Fixes

- **ASGI worker dropped a pipelined request on keepalive**: a second request
  sent on a kept-alive connection while the worker was finishing the first was
  left in the parser buffer, then discarded when the parser was reset between
  requests, so the connection hung until the client timed out. The buffered
  request is now recovered and served. Most visible with clients that pipeline
  or with frameworks that respond with `Transfer-Encoding: chunked`.

- **ASGI worker returned 502 for the uWSGI protocol**: with
  `--worker-class asgi --protocol uwsgi` behind nginx `uwsgi_pass`, every
  request failed with `Invalid uWSGI header: incomplete header`. The worker
  set up the HTTP/1 parser instead of a uWSGI reader, so inbound bytes never
  reached the uWSGI handler. The connection now starts a reader for the uWSGI
  protocol, builds the request body for the app, and sets `raw_path`.

- **Chunked framing lines were read without a bound**: a chunk-size line or
  trailer section that never terminates was reread in full on every socket read
  with no size limit, so one request could keep a worker busy without ever
  reaching the application. These lines are now bounded by the existing request
  limits and scanned incrementally, and a malformed chunked body answers
  `400 Bad Request` with one log line instead of a traceback.

- **HTTP/2 follow-up to the review**: a bad request now resets its own
  stream instead of the connection; the request line and field limits and
  the method token rule apply to HTTP/2 requests; forbidden trailers are
  dropped; `Expect: 100-continue` is answered; `RST_STREAM` floods close the
  connection with `ENHANCE_YOUR_CALM`; outbound frames respect the peer's
  `SETTINGS_MAX_FRAME_SIZE`; a malformed `HTTP2-Settings` header refuses the
  h2c upgrade before any `101` goes out and an oversized upgrade body is not
  upgraded; `http2_initial_window_size` and `http2_max_concurrent_streams` are
  bounded at startup, `http2_max_header_list_size = 0` means unlimited as
  documented, `h3` is no longer accepted in `http_protocols`, and `h2` without
  the h2 package is a configuration error. On `gthread` and `gevent` the
  h2c-upgraded stream is cleaned up like any other, requests queued behind a
  body read are served before the `max_requests` `GOAWAY`, which names the
  last stream served, peer protocol errors are logged at debug, and responses
  carry `Server` and `Date`. On the ASGI worker `send()` raises an `OSError`
  once the peer is gone (ASGI 2.4), trailers announced on
  `http.response.start` are sent after the body, no-body statuses drop their
  framing headers, the body wait is bounded by `timeout`, write backpressure
  reaches `writer.drain()`, and an idle connection closes after `keepalive`.
  h2spec runs against every HTTP/2 worker in the docker suites.

- **HTTP/2 request bodies were buffered in full before dispatch**: DATA
  frames were kept in two buffers, copied twice more when the request was
  built, and flow control credit went back to the peer as each frame arrived,
  so a client that never sent END_STREAM could grow a worker without the
  application being called. A request is now dispatched on its headers and
  `wsgi.input` (or ASGI `receive()`) pulls the body from the stream as it
  arrives, returning window credit only for what the application has read.
  A peer can have no more than the receive window in flight per stream. A
  body the application leaves unread is cut off with `RST_STREAM(NO_ERROR)`
  once the response is sent. On the ASGI worker, streams on a connection are
  served concurrently. Only listeners with `h2` in `http_protocols` were
  affected.
- **HTTP/2 review fixes**: a review of the HTTP/2 support found these, all
  fixed on every h2-capable worker unless noted. A graceful `GOAWAY` from the
  peer is honoured wherever it lands in a read: established streams finish,
  later ones are refused, and no private h2 state is touched. Waiting for
  send credit is bounded by `timeout` instead of a fixed five seconds; a
  peer that stops reading gets `RST_STREAM(CANCEL)` and the application is
  stopped rather than told the response succeeded. An application error
  after the response headers went out resets the stream instead of sending a
  second `HEADERS` block, which corrupted the HPACK table for every later
  response. An empty final body chunk now ends the stream. On `gthread` and
  `gevent`, frames read while waiting for credit are handled in order, an
  idle connection is closed after `keepalive` and a body that stalls for
  `timeout` is cancelled, so a peer can no longer pin a thread, and streams
  the peer resets before they are served are dropped so `HEADERS+RST_STREAM`
  floods cannot grow the worker. On the ASGI worker, `receive()` after the
  body blocks until the peer goes away instead of spinning the event loop, a
  reader paused under backpressure is resumed, and in-flight streams get the
  disconnect grace period before they are cancelled.

- Worker timeout used to dump a 500 response onto a body that had already
  started, because `sys.exit()` from the abort handler is a `BaseException`.
  Once headers are out we just close the connection
  ([#3410](https://github.com/benoitc/gunicorn/issues/3410)).

## 26.2.0 - 2026-08-24

### New Features

- **Cleartext HTTP/2 (h2c)**: `http2_cleartext` accepts `prior-knowledge`,
  `upgrade`, `both` or `off` (the default). With `prior-knowledge`, a connection
  opening with the HTTP/2 preface is served as HTTP/2, which is what a
  TLS-terminating proxy in front of gunicorn needs to avoid dropping to HTTP/1.1
  upstream. `upgrade` honours an HTTP/1.1 `Upgrade: h2c` request. Both work on
  the gthread, gevent and ASGI workers. Only peers in `forwarded_allow_ips` are
  considered; anything else from such a peer is refused with 400 rather than
  silently downgraded. Each mechanism is enabled separately, so turning one on
  does not turn the other on
  ([#3489](https://github.com/benoitc/gunicorn/issues/3489),
  [#3663](https://github.com/benoitc/gunicorn/pull/3663),
  [#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Security

- **HTTP/2 bypassed the header policy**: `HTTP2Request` built its headers
  straight from the stream, so nothing the HTTP/1 path enforces applied over
  HTTP/2: the underscore and `header_map` policy, duplicate `Host` and
  `Content-Type`, control characters in values, and the `forwarded_allow_ips`
  trust gate. An untrusted client could set `SCRIPT_NAME` and forge `HTTP_*`
  entries in the WSGI environ, and decide `wsgi.url_scheme` through `:scheme`.
  The policy now lives on a mixin both request classes share, and the scheme is
  derived from the transport
  ([#3705](https://github.com/benoitc/gunicorn/pull/3705)).

### Bug Fixes

- **WSGI HTTP/2 responses were buffered whole**: both WSGI workers collected the
  entire body in memory before sending anything. They write through an
  `HTTP2Response` now, so the body leaves as it is produced
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709)).

- **No-body responses carried a body over HTTP/2**: HEAD, 204 and 304 sent
  application body bytes on all three workers, while the HTTP/1 path had always
  dropped them per RFC 9110. They no longer do
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709),
  [#3710](https://github.com/benoitc/gunicorn/pull/3710)).

- **Events lost during flow-control waits**: while blocked waiting for a
  WINDOW_UPDATE, both HTTP/2 connections read from the socket and discarded
  every event that was not a stream reset or connection termination, losing
  requests and body data outright. They are queued for the main loop now
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709),
  [#3710](https://github.com/benoitc/gunicorn/pull/3710)).

- **`sendfile()` on HTTP/2**: it was guarded by `cfg.is_ssl`, which covered
  HTTP/2 only for as long as HTTP/2 implied TLS. It is refused on HTTP/2
  responses directly, so cleartext cannot bypass HTTP/2 framing
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709)).

- **Request bodies dropped on `Upgrade` requests**: on the ASGI worker with the
  fast parser, any request carrying an `Upgrade` header reached the application
  with an empty body, whatever the header's value and with HTTP/2 switched off.
  The parser treated the header as meaning the rest of the connection was no
  longer HTTP/1, so it never read the body it had just been told the length of.
  Fixed in `gunicorn_h1c` 0.6.9, which the requirement below now pins
  ([#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Changes

- **Fast HTTP Parser**: require `gunicorn_h1c >= 0.6.9`, which adds
  `remaining()` for recovering bytes pipelined behind a completed message, and
  stops an `Upgrade` header from suppressing body parsing
  ([#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Documentation

- Describe `http_parser` in its own terms rather than as an ASGI-only setting;
  it applies to the WSGI workers too. Thanks to
  [@methane](https://github.com/methane)
  ([#3704](https://github.com/benoitc/gunicorn/pull/3704)).

- Correct the default control socket path in the `gunicorn.c` guide. Thanks to
  [@cormier](https://github.com/cormier)
  ([#3703](https://github.com/benoitc/gunicorn/pull/3703)).

- State the supported Python version as 3.10+ in the README, matching
  `requires-python`. Thanks to [@Rotzbua](https://github.com/Rotzbua)
  ([#3712](https://github.com/benoitc/gunicorn/pull/3712)).

- Point CONTRIBUTING at the mkdocs settings reference instead of the retired
  Sphinx path. Thanks to [@melbinjp](https://github.com/melbinjp)
  ([#3690](https://github.com/benoitc/gunicorn/pull/3690)).

- Fix the sponsor logo path on the sponsor page
  ([#3700](https://github.com/benoitc/gunicorn/pull/3700)).

## 26.1.0 - 2026-08-18

### New Features

- **Glob patterns in `reload_extra_files`**: entries containing `*`, `?` or `[`
  are treated as patterns, so `ui/*/config.json` watches every view's config
  without listing them one by one. Patterns are re-expanded on every reload
  check rather than once at startup, so a file created later starts being
  watched without restarting gunicorn, and `**` recurses. A pattern matching
  nothing warns instead of failing, since with live expansion it may match later
  ([#1643](https://github.com/benoitc/gunicorn/issues/1643),
  [#3662](https://github.com/benoitc/gunicorn/pull/3662)).

### Security

- **Dependency floors raised past known advisories**: every declared floor was
  checked against the advisory database. `tornado`, `h2`, `setuptools` and
  `pymdown-extensions` permitted vulnerable versions and now require the first
  clean release; `pytest` and `httpx` were unpinned and now carry floors. The
  `tornado` example pinned `tornado<6`, which was both the source of several
  advisories and older than the `>=6.5.0` the tornado worker needs, so the
  example could not run as pinned.

### Bug Fixes

- **SIGHUP did not reload the logger configuration**: `Arbiter.reload()`
  re-read the configuration file but kept using the logger built at startup,
  calling only `reopen_files()` on its existing handlers. Changes to
  `logconfig`, `logconfig_dict`, `logconfig_json` and `loglevel` were ignored
  until a full restart, which in containers meant replacing the pod. The
  existing logger now re-runs its setup on reload, so new handlers, formats
  and levels take effect while the process identity and its listeners are
  preserved, and re-running the setup no longer stacks duplicate syslog
  handlers. An invalid log configuration on reload is not fatal either: the
  error is reported on stderr, the previous working configuration is restored
  and the master keeps running with it
  ([#3353](https://github.com/benoitc/gunicorn/issues/3353)).

- **Truncated chunked bodies accepted**: RFC 9112 section 7.1.2 ends a chunked
  body with `0 CRLF CRLF`, the second CRLF being the mandatory empty trailer
  section. `ChunkedReader.parse_chunk_size()` swallowed the `NoMoreData` raised
  while scanning for it, so a body cut short right after the last chunk line was
  treated as complete instead of rejected. It now raises
  `ChunkMissingTerminator`
  ([#3382](https://github.com/benoitc/gunicorn/issues/3382),
  [#3685](https://github.com/benoitc/gunicorn/pull/3685)).

- **`--spew` crashed on dynamically generated code**: the trace hook indexed the
  2-tuple returned by `inspect.getsourcelines()` by line number rather than
  indexing the list of lines, so a frame with no `__file__` raised
  `AttributeError: 'int' object has no attribute 'rstrip'` on line 1 and
  `IndexError` beyond it. The tuple is now unpacked and offset by the source's
  starting line ([#3344](https://github.com/benoitc/gunicorn/issues/3344),
  [#3495](https://github.com/benoitc/gunicorn/pull/3495)).

- **Duplicate `Host` and `Content-Type` headers accepted**: RFC 9110 section 5.3
  allows only one of each, and a repeat cannot be merged into a list, so the
  message means different things to gunicorn and to anything downstream. Both
  are now rejected with `InvalidHeader`. The check lives in the policy hook
  shared by both parsers, so the pure-Python and fast parsers agree. Duplicate
  `Content-Length` was already rejected and is unchanged
  ([#3366](https://github.com/benoitc/gunicorn/issues/3366),
  [#3548](https://github.com/benoitc/gunicorn/pull/3548)).

- **Non-worker children reported as failed workers**: `reap_workers()` reaps
  every child through `waitpid(-1)`, including processes the kernel reparented
  onto gunicorn when it runs as PID 1 in a container, but it logged the exit
  status before checking whether the pid was ever a worker. An unrelated process
  produced `Worker (pid:N) exited with code M` and triggered alerts. More
  seriously, such a process exiting with code 3 or 4 raised `HaltServer` and shut
  the server down. Ownership is now established first: the dirty arbiter is
  reported as itself, unknown children are reaped silently at debug level, and
  only real workers can halt the server
  ([#3220](https://github.com/benoitc/gunicorn/issues/3220),
  [#3566](https://github.com/benoitc/gunicorn/pull/3566)).

- **Dirty arbiter exits were invisible on SIGCHLD**: `handle_chld()` called
  `reap_workers()` first, whose `waitpid(-1)` claimed the dirty arbiter before
  `reap_dirty_arbiter()` could identify it, so the latter always hit `ECHILD` and
  its reporting never ran. The dirty arbiter is now reaped first, and
  `reap_workers()` recognises it if it exits mid-loop.

- **Dirty arbiter returned stale responses after a worker timeout**: when a
  request reached `dirty_timeout` the arbiter answered the client with a timeout
  error but kept the worker connection open. The worker's late response was then
  the first message waiting on that socket, so the next request routed to the
  same worker received the previous request's result, and every request after it
  stayed one response behind. The connection is now closed on timeout, so the
  late answer is discarded with it
  ([#3626](https://github.com/benoitc/gunicorn/pull/3626)).

- **ASGI connection count leaked on server-initiated close**: `nr_conns` was
  only decremented in `connection_lost()`, behind a guard keyed on the same
  flag `_close_transport()` sets first. Every close the server started (a
  `Connection: close` response, a keepalive timeout, an error abort) leaked one
  count, so `ASGIWorker._shutdown()` ran the full `graceful_timeout` and warned
  about connections that were already gone. The guard now uses its own flag, so
  the decrement and the rest of the cleanup run exactly once whichever side
  closes first ([#3661](https://github.com/benoitc/gunicorn/issues/3661)).

- **Inotify reloader on cwd-relative extra files**: `reload_extra_files` entries
  with no directory part (for example `.env`) produced an empty dirname, and
  watching it raised `InotifyError` with `ENOENT`. The current directory is now
  watched as `.` ([#3377](https://github.com/benoitc/gunicorn/issues/3377),
  [#3667](https://github.com/benoitc/gunicorn/pull/3667)).

- **StatsD zero-valued metrics**: gauges, counters, histograms and timers
  reporting `0` were silently dropped because the value was tested for
  truthiness. Only `None` is skipped now
  ([#3676](https://github.com/benoitc/gunicorn/pull/3676)).

- **Spurious no-body warning from `sendfile()`**: a HEAD, 204 or 304 response
  served through `sendfile()` warned about dropped body bytes even when the
  file was empty and nothing was dropped. It now warns only when there are
  bytes to drop, matching `write()`
  ([#3684](https://github.com/benoitc/gunicorn/pull/3684)).

- **Bare `except` in the gevent websocket example**: narrowed to
  `except Exception` ([#3683](https://github.com/benoitc/gunicorn/pull/3683)).

- **ASGI `receive()` cancellation**: Let `asyncio.CancelledError` propagate
  from `BodyReceiver` instead of swallowing it and returning
  `http.disconnect`. Frameworks that cancel their disconnect listener after
  the response completes (Django) no longer see the cancel masked, so
  `request_finished` fires and `close_old_connections()` runs. Fixes idle
  database connections leaking since 25.1.0
  ([#3627](https://github.com/benoitc/gunicorn/issues/3627),
  [#3654](https://github.com/benoitc/gunicorn/pull/3654)).

- **Control socket leak on SIGHUP reload**: The control thread is now marked
  ready once its loop and server are live, and the stop paths wait on that
  readiness before scheduling shutdown. Reloads no longer leak one thread and
  its selector fd plus unix socket per worker, which eventually raised
  "too many open files"
  ([#3648](https://github.com/benoitc/gunicorn/issues/3648)).

- **WSGI body framing on HEAD/1xx/204/304**: Mirror the ASGI strip-and-warn
  behavior on the WSGI path. `Content-Length` is stripped on 1xx/204 per
  RFC 9110 section 6.4.2, body bytes are dropped for no-body responses in
  both `write()` and `sendfile()`, and a single warning is logged per request
  ([#3413](https://github.com/benoitc/gunicorn/issues/3413)).

### Refactoring

- Pass log arguments to the logger instead of pre-formatting the worker
  termination message in `Arbiter.reap_workers()`
  ([#3678](https://github.com/benoitc/gunicorn/pull/3678)).

### Changes

- **`packaging` is no longer a runtime dependency**: it was only ever imported by
  the gevent worker, to compare gevent's version. It moved to the `gevent` and
  `testing` extras, so a plain `pip install gunicorn` pulls in nothing
  ([#3643](https://github.com/benoitc/gunicorn/pull/3643)).

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.6`, which rejects duplicate
  `Host` and `Content-Type` headers in the C parser itself. Gunicorn already
  refuses them on both the WSGI and ASGI paths, so this changes nothing that is
  reachable; it moves the rejection to where the bytes are read and lets the
  ASGI corpus exercise those cases against the fast parser directly.

## 26.0.0 - 2026-05-05

### Breaking Changes

- **Eventlet worker removed**: The `eventlet` worker class has been dropped.
  Migrate to `gevent`, `gthread`, or `tornado`.

### New Features

- **ASGI Framework Compatibility Suite**: New end-to-end compatibility test
  harness covering Starlette, FastAPI, Litestar, Quart, Sanic, and BlackSheep.
  Current grid passes 438/444 tests (98%).

- **ASGI Test Suite Expansion**: 134 additional ASGI unit tests covering
  protocol semantics, lifespan, websockets, and chunked framing.

### Security

- **HTTP/1.1 Request-Target Validation** (RFC 9112 sections 3.2.3, 3.2.4):
  - Reject `authority-form` request-target outside `CONNECT`
  - Reject `asterisk-form` request-target outside `OPTIONS`
  - Reject `relative-reference` request-targets

- **Header Field Hardening** (RFC 9110):
  - Reject control characters in header field-value (section 5.5)
  - Reject forbidden trailer field-names (section 6.5.1)
  - Reject `Content-Length` list form (RFC 9112 section 6.3)

- **Request Smuggling Hardening**:
  - Tighten keepalive gate and scope `finish_body` byte cap
  - Keep `_body_receiver` alive across the keepalive smuggling gate so
    pipelined requests cannot re-enter a closed body
  - Address parser/protocol findings from a six-point WSGI/ASGI audit

- **PROXY Protocol (ASGI)**: Enforce `proxy_allow_ips` and tighten v1/v2
  parsing in the ASGI callback parser.

- **Connection Draining**: Drain the connection on close per RFC 9112
  section 9.6 to prevent reset-on-close truncation.

### Bug Fixes

- **Body Framing on HEAD/204/304**:
  - Keep `Content-Length` on HEAD and 304 responses
    ([#3621](https://github.com/benoitc/gunicorn/pull/3621))
  - Drop body framing on HEAD/204/304 even when the framework set it
  - Warn once when an ASGI app emits a body for a no-body response

- **HTTP/2 ASGI**:
  - Fix `_handle_stream_ended` to set `_body_complete` in the async HTTP/2
    handler so request bodies finalize correctly on stream end
  - Add `InvalidChunkExtension` mapping and fast-parser support in ASGI
    tests ([#3565](https://github.com/benoitc/gunicorn/pull/3565))

- **HTTP/1.1 100-Continue**: Stop adding `Transfer-Encoding: chunked` to
  100-Continue interim responses.

- **WebSocket Close Handshake** (RFC 6455):
  - Comply with the close handshake state machine
  - Close the transport after the close handshake completes
  - Fix binary send when the `text` key is `None`

- **Early Hints**: Validate headers in the `early_hints` callback to match
  `process_headers`; pass only the header name to `InvalidHeader`
  ([#3588](https://github.com/benoitc/gunicorn/pull/3588)).

- **ASGI Framework Fixes**:
  - Fix ASGI disconnect handling for Django-style apps
  - Fix Litestar request handling (use raw ASGI receive for body/headers)
  - Fix Litestar HTTP endpoints for compatibility tests
  - Fix Quart headers endpoint to normalize keys to lowercase
  - Fix Quart WebSocket close test app (missing `accept()`)
  - Fix duplicate `Transfer-Encoding` header for BlackSheep streaming

### Refactoring

- Split `BodyReceiver._closed` into separate transport and body-wait flags
  for clearer keepalive/EOF semantics.

### Changes

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.5`. Drop the last
  `python_only` test markers; the C extension is now used wherever
  available (CPython only; PyPy continues to use the Python parser).

- **Test Dependencies**: Add `h2` and `uvloop` to the `testing` extra;
  remove `eventlet`.

- **Docker Build**: Bump GitHub Actions `docker/setup-qemu-action`,
  `docker/setup-buildx-action`, `docker/login-action`,
  `docker/build-push-action`, and `docker/metadata-action` to current
  major versions.

---

## 25.3.0 - 2026-03-26

### Bug Fixes

- **HTTP/2 ASGI Body Duplication**: Fix request body being received twice in HTTP/2
  ASGI requests, causing JSON parsing errors with "Extra data" messages
  ([#3558](https://github.com/benoitc/gunicorn/issues/3558))

- **ASGI Chunked EOF Handling**: Add `finish()` method to callback parser to handle
  chunked encoding edge case where connection closes before final CRLF after zero-chunk

- **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string
  instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561))

- **Chunked Encoding**: Reject chunk extensions containing bare CR bytes per RFC 9112
  ([#3556](https://github.com/benoitc/gunicorn/discussions/3556))

### Security

- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112:
  - Reject duplicate Content-Length headers
  - Reject requests with both Content-Length and Transfer-Encoding
  - Reject chunked transfer encoding in HTTP/1.0
  - Reject stacked chunked encoding
  - Validate Transfer-Encoding values
  - Strict chunk size validation

### Changes

- **Fast HTTP Parser**: Update to gunicorn_h1c >= 0.6.2 for `asgi_headers` property
  which provides headers with lowercase names directly from the C parser

- **ASGI PROXY Protocol**: Add PROXY protocol v1/v2 support to callback parser

---

## 25.2.0 - 2026-03-24

### New Features

- **Fast HTTP Parser (gunicorn_h1c 0.6.0)**: Integrate new exception types and limit
  parameters from gunicorn_h1c 0.6.0 for both WSGI and ASGI workers
  - Requires gunicorn_h1c >= 0.6.0 for `http_parser='fast'`
  - Falls back to Python parser in `auto` mode if version not met
  - Proper HTTP status codes for limit errors (414, 431)

### Bug Fixes

- **uWSGI Async Workers**: Fix `InvalidUWSGIHeader: incomplete header` error
  when using gevent or gthread workers with uwsgi protocol behind nginx.
  ([#3552](https://github.com/benoitc/gunicorn/issues/3552),
  [PR #3554](https://github.com/benoitc/gunicorn/pull/3554))

- **FileWrapper Iterator Protocol**: Add `__iter__` and `__next__` methods to
  `FileWrapper` for full PEP 3333 compliance. Previously only supported old-style
  `__getitem__` iteration which broke code explicitly using `iter()` or `next()`.
  ([#3396](https://github.com/benoitc/gunicorn/issues/3396),
  [PR #3550](https://github.com/benoitc/gunicorn/pull/3550))

### Performance

- **ASGI HTTP Parser Optimizations**: Improve ASGI worker HTTP parsing performance
  - Read chunks in 64-byte blocks instead of 1 byte at a time for chunk size lines and trailers
  - Reuse BytesIO buffers with truncate/seek instead of creating new objects (reduces GC pressure)
  - Use `bytearray.find()` directly instead of converting to bytes first
  - Use index-based iteration for header parsing instead of `list.pop(0)` (O(1) vs O(n))

---

## 25.1.0 - 2026-02-13

### New Features

- **Control Interface (gunicornc)**: Add interactive control interface for managing
  running Gunicorn instances, similar to birdc for BIRD routing daemon
  ([PR #3505](https://github.com/benoitc/gunicorn/pull/3505))
  - Unix socket-based communication with JSON protocol
  - Interactive mode with readline support and command history
  - Commands: `show all/workers/dirty/config/stats/listeners`
  - Worker management: `worker add/remove/kill`, `dirty add/remove`
  - Server control: `reload`, `reopen`, `shutdown`
  - New settings: `--control-socket`, `--control-socket-mode`, `--no-control-socket`
  - New CLI tool: `gunicornc` for connecting to control socket
  - See [Control Interface Guide](guides/gunicornc.md) for details

- **Dirty Stash**: Add global shared state between workers via `dirty.stash`
  ([PR #3503](https://github.com/benoitc/gunicorn/pull/3503))
  - In-memory key-value store accessible by all workers
  - Supports get, set, delete, clear, keys, and has operations
  - Useful for sharing state like feature flags, rate limits, or cached data

- **Dirty Binary Protocol**: Implement efficient binary protocol for dirty arbiter IPC
  using TLV (Type-Length-Value) encoding
  ([PR #3500](https://github.com/benoitc/gunicorn/pull/3500))
  - More efficient than JSON for binary data
  - Supports all Python types: str, bytes, int, float, bool, None, list, dict
  - Better performance for large payloads

- **Dirty TTIN/TTOU Signals**: Add dynamic worker scaling for dirty arbiters
  ([PR #3504](https://github.com/benoitc/gunicorn/pull/3504))
  - Send SIGTTIN to increase dirty workers
  - Send SIGTTOU to decrease dirty workers
  - Respects minimum worker constraints from app configurations

### Changes

- **ASGI Worker**: Promoted from beta to stable
- **Dirty Arbiters**: Now marked as beta feature

### Documentation

- Fix Markdown formatting in /configure documentation

---

## 25.0.3 - 2026-02-07

### Bug Fixes

- Fix RuntimeError when StopIteration is raised inside ASGI response body
  coroutine (PEP 479 compliance)

- Fix deprecation warning for passing maxsplit as positional argument in
  `re.split()` (Python 3.13+)

---

## 25.0.2 - 2026-02-06

### Bug Fixes

- Fix ASGI concurrent request failures through nginx proxy by normalizing
  sockaddr tuples to handle both 2-tuple (IPv4) and 4-tuple (IPv6) formats
  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))

- Fix graceful disconnect handling for ASGI worker to properly handle
  client disconnects without raising exceptions
  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))

- Fix lazy import of dirty module for gevent compatibility - prevents
  import errors when concurrent.futures is imported before gevent monkey-patching
  ([PR #3483](https://github.com/benoitc/gunicorn/pull/3483))

### Changes

- Refactor: Extract `_normalize_sockaddr` utility function for consistent
  socket address handling across workers

- Add license headers to all Python source files

- Update copyright year to 2026 in LICENSE and NOTICE files

---

## 25.0.1 - 2026-02-02

### Bug Fixes

- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for
  HTTP/1.1 responses without Content-Length header. Without chunked encoding,
  clients wait for connection close to determine end-of-response.

### Changes

- Update celery_alternative example to use FastAPI with native ASGI worker and
  uvloop for async task execution

### Testing

- Add ASGI compliance test suite with Docker-based integration tests covering HTTP,
  WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI),
  HTTP/2, and concurrency scenarios

---

## 25.0.0 - 2026-02-01

### New Features

- **Dirty Arbiters**: Separate process pool for executing long-running, blocking
  operations (AI model loading, heavy computation) without blocking HTTP workers
  ([PR #3460](https://github.com/benoitc/gunicorn/pull/3460))
  - Inspired by Erlang's dirty schedulers
  - Asyncio-based with Unix socket IPC
  - Stateful workers that persist loaded resources
  - New settings: `--dirty-app`, `--dirty-workers`, `--dirty-timeout`,
    `--dirty-threads`, `--dirty-graceful-timeout`
  - Lifecycle hooks: `on_dirty_starting`, `dirty_post_fork`,
    `dirty_worker_init`, `dirty_worker_exit`

- **Per-App Worker Allocation for Dirty Arbiters**: Control how many dirty workers
  load each app for memory optimization with heavy models
  ([PR #3473](https://github.com/benoitc/gunicorn/pull/3473))
  - Set `workers` class attribute on DirtyApp (e.g., `workers = 2`)
  - Or use config format `module:class:N` (e.g., `myapp:HeavyModel:2`)
  - Requests automatically routed to workers with the target app
  - New exception `DirtyNoWorkersAvailableError` for graceful error handling
  - Example: 8 workers × 10GB model = 80GB → with `workers=2`: 20GB (75% savings)

- **HTTP/2 Support (Beta)**: Native HTTP/2 (RFC 7540) support for improved performance
  with modern clients ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))
  - Multiplexed streams over a single connection
  - Header compression (HPACK)
  - Flow control and stream prioritization
  - Works with gthread, gevent, and ASGI workers
  - New settings: `--http-protocols`, `--http2-max-concurrent-streams`,
    `--http2-initial-window-size`, `--http2-max-frame-size`, `--http2-max-header-list-size`
  - Requires SSL/TLS and h2 library: `pip install gunicorn[http2]`
  - See [HTTP/2 Guide](guides/http2.md) for details
  - New example: `examples/http2_gevent/` with Docker and tests

- **HTTP 103 Early Hints**: Support for RFC 8297 Early Hints to enable browsers to
  preload resources before the final response
  ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))
  - WSGI: `environ['wsgi.early_hints'](headers)` callback
  - ASGI: `http.response.informational` message type
  - Works with both HTTP/1.1 and HTTP/2

- **uWSGI Protocol for ASGI Worker**: The ASGI worker now supports receiving requests
  via the uWSGI binary protocol from nginx
  ([PR #3467](https://github.com/benoitc/gunicorn/pull/3467))

### Bug Fixes

- Fix HTTP/2 ALPN negotiation for gevent and eventlet workers when
  `do_handshake_on_connect` is False (the default). The TLS handshake is now
  explicitly performed before checking `selected_alpn_protocol()`.

- Fix setproctitle initialization with systemd socket activation
  ([#3465](https://github.com/benoitc/gunicorn/issues/3465))

- Fix `Expect: 100-continue` handling: ignore the header for HTTP/1.0 requests
  since 100-continue is only valid for HTTP/1.1+
  ([PR #3463](https://github.com/benoitc/gunicorn/pull/3463))

- Fix missing `_expected_100_continue` attribute in UWSGIRequest

- Disable setproctitle on macOS to prevent segfaults during process title updates

- Publish full exception traceback when the application fails to load
  ([#3462](https://github.com/benoitc/gunicorn/issues/3462))

### Deprecations

- **Eventlet Worker**: The `eventlet` worker is deprecated and will be removed in
  Gunicorn 26.0. Eventlet itself is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html).
  Please migrate to `gevent`, `gthread`, or another supported worker type.

### Changes

- Remove obsolete Makefile targets
  ([PR #3471](https://github.com/benoitc/gunicorn/pull/3471))

---

## 24.1.1 - 2026-01-24

### Bug Fixes

- Fix `forwarded_allow_ips` and `proxy_allow_ips` to remain as strings for backward
  compatibility with external tools like uvicorn. Network validation now uses strict
  mode to detect invalid CIDR notation (e.g., `192.168.1.1/24` where host bits are set)
  ([#3458](https://github.com/benoitc/gunicorn/issues/3458),
  [PR #3459](https://github.com/benoitc/gunicorn/pull/3459))

---

## 24.1.0 - 2026-01-23

### New Features

- **Official Docker Image**: Gunicorn now publishes official Docker images to GitHub
  Container Registry at `ghcr.io/benoitc/gunicorn`
  - Based on Python 3.12 slim image
  - Uses recommended worker formula (2 × CPU + 1)
  - Configurable via environment variables

- **PROXY Protocol v2 Support**: Extended PROXY protocol implementation to support
  the binary v2 format in addition to the existing text-based v1 format
  ([PR #3451](https://github.com/benoitc/gunicorn/pull/3451))
  - New `--proxy-protocol` modes: `off`, `v1`, `v2`, `auto`
  - `auto` mode (default when enabled) detects v1 or v2 automatically
  - v2 binary format is more efficient and supports additional metadata
  - Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources

- **CIDR Network Support**: `--forwarded-allow-ips` and `--proxy-allow-from` now
  accept CIDR notation (e.g., `192.168.0.0/16`) for specifying trusted networks
  ([PR #3449](https://github.com/benoitc/gunicorn/pull/3449))

- **Socket Backlog Metric**: New `gunicorn.socket.backlog` gauge metric reports
  the current socket backlog size on Linux systems
  ([PR #3450](https://github.com/benoitc/gunicorn/pull/3450))

- **InotifyReloader Enhancement**: The inotify-based reloader now watches newly
  imported modules, not just those loaded at startup
  ([PR #3447](https://github.com/benoitc/gunicorn/pull/3447))

### Bug Fixes

- Fix signal handling regression where SIGCLD alias caused "Unhandled signal: cld"
  errors on Linux when workers fail during boot
  ([#3453](https://github.com/benoitc/gunicorn/discussions/3453))

- Fix socket blocking mode on keepalive connections preventing SSL handshake
  failures with async workers
  ([PR #3452](https://github.com/benoitc/gunicorn/pull/3452))

- Use smaller buffer size in `finish_body()` for faster timeout detection on
  slow or abandoned connections
  ([PR #3453](https://github.com/benoitc/gunicorn/pull/3453))

- Handle `SSLWantReadError` in `finish_body()` to prevent worker hangs during
  SSL renegotiation
  ([PR #3448](https://github.com/benoitc/gunicorn/pull/3448))

- Log SIGTERM as info level instead of warning to reduce noise in orchestrated
  environments
  ([PR #3446](https://github.com/benoitc/gunicorn/pull/3446))

- Print exception details to stderr when worker fails to boot
  ([PR #3443](https://github.com/benoitc/gunicorn/pull/3443))

- Fix `unreader.unread()` to prepend data to buffer instead of appending
  ([PR #3442](https://github.com/benoitc/gunicorn/pull/3442))

- Prevent `RecursionError` when pickling Config objects
  ([PR #3441](https://github.com/benoitc/gunicorn/pull/3441))

- Use proper exception chaining with `raise from` in glogging.py
  ([PR #3440](https://github.com/benoitc/gunicorn/pull/3440))

---

## 24.0.0 - 2026-01-23

### New Features

- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python
  frameworks like FastAPI, Starlette, and Quart without external dependencies
  ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444))
  - HTTP/1.1 with keepalive connections
  - WebSocket support
  - Lifespan protocol for startup/shutdown hooks
  - Optional uvloop for improved performance
  - New settings: `--asgi-loop`, `--asgi-lifespan`, `--root-path`

- **uWSGI Binary Protocol**: Support for receiving requests from nginx via
  `uwsgi_pass` directive, enabling efficient binary protocol communication
  ([PR #3444](https://github.com/benoitc/gunicorn/pull/3444))
  - New settings: `--protocol uwsgi`, `--uwsgi-allow-from`

- **Documentation Migration**: Migrated documentation from Sphinx to MkDocs
  with Material theme for improved navigation and mobile experience
  ([PR #3426](https://github.com/benoitc/gunicorn/pull/3426))

### Security

- **eventlet**: Require eventlet >= 0.40.3 to address CVE-2021-21419 (websocket
  memory exhaustion) and CVE-2025-58068 (HTTP request smuggling)
  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))

- **gevent**: Require gevent >= 24.10.1 to address CVE-2023-41419 (HTTP request
  smuggling) and CVE-2024-3219 (socket.socketpair vulnerability)
  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))

- **tornado**: Require tornado >= 6.5.0 to address CVE-2025-47287 (HTTP request
  smuggling) and other security fixes
  ([PR #3445](https://github.com/benoitc/gunicorn/pull/3445))

### Changes

- Documentation now hosted at https://gunicorn.org
- Updated license configuration to PEP 639 format for uv compatibility

!!! warning "ASGI Worker Beta"
    The ASGI worker is a beta feature. While tested, the API and behavior
    may change in future releases. Please report any issues on GitHub.

---

### Content 404 (`docs/content/404.md`)

# Page Not Found

The page you're looking for doesn't exist or has moved.

<div class="quick-links" style="margin-top: 2rem;">
  <a href="/" class="quick-link">
    <strong>Home</strong>
    <span>Return to the homepage</span>
  </a>
  <a href="/quickstart/" class="quick-link">
    <strong>Quickstart</strong>
    <span>Get started with Gunicorn</span>
  </a>
  <a href="/reference/settings/" class="quick-link">
    <strong>Settings</strong>
    <span>Configuration reference</span>
  </a>
  <a href="https://github.com/benoitc/gunicorn/issues" class="quick-link">
    <strong>Report Issue</strong>
    <span>Let us know about broken links</span>
  </a>
</div>

---

### Content Asgi (`docs/content/asgi.md`)

# ASGI Worker

Gunicorn includes a native ASGI worker that enables running async Python web frameworks
like FastAPI, Starlette, and Quart without external dependencies like Uvicorn.

## Quick Start

```bash
# Install gunicorn
pip install gunicorn

# Run an ASGI application
gunicorn myapp:app --worker-class asgi --workers 4
```

For FastAPI applications:

```bash
gunicorn main:app --worker-class asgi --bind 0.0.0.0:8000
```

## Features

The ASGI worker provides:

- **HTTP/1.1** with keepalive connections
- **HTTP/2** with multiplexing and server push (requires SSL)
- **WebSocket** support for real-time applications
- **Lifespan protocol** for startup/shutdown hooks
- **Optional fast HTTP parser** via C extension for high throughput
- **Optional uvloop** for improved event loop performance
- **SSL/TLS** support
- **uWSGI protocol** for nginx `uwsgi_pass` integration

## Configuration

### Worker Class

Set the worker class to `asgi`:

```bash
gunicorn myapp:app --worker-class asgi
```

Or in a configuration file:

```python
# gunicorn.conf.py
worker_class = "asgi"
```

### Event Loop

Control which asyncio event loop implementation to use:

| Value    | Description |
|----------|-------------|
| `auto`   | Use uvloop if available, otherwise asyncio (default) |
| `asyncio`| Use Python's built-in asyncio event loop |
| `uvloop` | Use uvloop (must be installed separately) |

```bash
gunicorn myapp:app --worker-class asgi --asgi-loop uvloop
```

To use uvloop, install it first:

```bash
pip install uvloop
```

### Lifespan Protocol

The lifespan protocol lets your application run code at startup and shutdown.
This is essential for frameworks that need to initialize database connections,
caches, or background tasks.

| Value  | Description |
|--------|-------------|
| `auto` | Detect if app supports lifespan, enable if so (default) |
| `on`   | Always run lifespan protocol (fail if unsupported) |
| `off`  | Never run lifespan protocol |

```bash
gunicorn myapp:app --worker-class asgi --asgi-lifespan on
```

Example FastAPI application using lifespan:

```python
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: initialize resources
    print("Starting up...")
    yield
    # Shutdown: cleanup resources
    print("Shutting down...")

app = FastAPI(lifespan=lifespan)
```

### Root Path

When running behind a reverse proxy that mounts your application at a subpath,
set `root_path` so your application knows its mount point:

```bash
gunicorn myapp:app --worker-class asgi --root-path /api
```

This is equivalent to the `SCRIPT_NAME` in WSGI applications.

### Worker Connections

Control the maximum number of concurrent connections per worker:

```bash
gunicorn myapp:app --worker-class asgi --worker-connections 1000
```

!!! note
    Unlike sync workers, the `--threads` option has no effect on ASGI workers.
    Use `--worker-connections` to control concurrency.

## WebSocket Support

The ASGI worker supports WebSocket connections out of the box. No additional
configuration is required.

Example with Starlette:

```python
from starlette.applications import Starlette
from starlette.routing import WebSocketRoute

async def websocket_endpoint(websocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"Echo: {data}")

app = Starlette(routes=[
    WebSocketRoute("/ws", websocket_endpoint),
])
```

## Production Deployment

### With Nginx (HTTP Proxy)

```nginx
upstream gunicorn {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://gunicorn;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket support
    location /ws {
        proxy_pass http://gunicorn;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}
```

### With Nginx (uWSGI Protocol)

For better performance, you can use nginx's native uWSGI protocol support:

```bash
gunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000
```

```nginx
upstream gunicorn {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }
}
```

!!! note
    WebSocket connections are not supported when using the uWSGI protocol.
    Use HTTP proxy for WebSocket endpoints.

See [uWSGI Protocol](uwsgi.md) for more details on uWSGI protocol configuration.

### Recommended Settings

For production ASGI deployments:

```python
# gunicorn.conf.py
worker_class = "asgi"
workers = 4  # Number of worker processes
worker_connections = 1000  # Max connections per worker
keepalive = 5  # Keepalive timeout
timeout = 120  # Worker timeout
graceful_timeout = 30  # Graceful shutdown timeout

# Performance tuning
asgi_loop = "auto"  # Use uvloop if available
asgi_lifespan = "auto"  # Auto-detect lifespan support
```

## Performance

### Fast HTTP Parser

For maximum performance, install the optional `gunicorn_h1c` C extension:

```bash
pip install gunicorn[fast]
```

This provides a high-performance HTTP parser using picohttpparser with SIMD
optimizations, offering significant speedups for HTTP parsing compared to the
pure Python implementation.

The parser is automatically used when available (`--http-parser auto`), or you
can explicitly require it:

```bash
gunicorn myapp:app --worker-class asgi --http-parser fast
```

| Parser | Description |
|--------|-------------|
| `auto` | Use fast parser if available, otherwise Python (default) |
| `fast` | Require fast parser, fail if unavailable |
| `python` | Force pure Python parser |

### Performance Tips

1. **Use uvloop** for improved event loop performance:
   ```bash
   pip install uvloop
   gunicorn myapp:app --worker-class asgi --asgi-loop uvloop
   ```

2. **Install the fast parser** for optimized HTTP parsing:
   ```bash
   pip install gunicorn[fast]
   ```

3. **Tune worker count** based on CPU cores:
   ```bash
   gunicorn myapp:app --worker-class asgi --workers $(nproc)
   ```

4. **Increase connections** for I/O-bound applications:
   ```bash
   gunicorn myapp:app --worker-class asgi --worker-connections 2000
   ```

## Comparison with Other ASGI Servers

| Feature | Gunicorn ASGI | Uvicorn | Hypercorn |
|---------|---------------|---------|-----------|
| Process management | Built-in | External | Built-in |
| HTTP/2 | Yes | No | Yes |
| WebSocket | Yes | Yes | Yes |
| Lifespan | Yes | Yes | Yes |
| uvloop support | Yes | Yes | Yes |

!!! note
    HTTP/2 requires SSL/TLS and the h2 library. See [HTTP/2 Support](guides/http2.md) for details.

Gunicorn's ASGI worker provides the same process management, logging, and
configuration capabilities you're familiar with from WSGI deployments.

## Troubleshooting

### Lifespan startup failed

If you see "ASGI lifespan startup failed", your application may not properly
implement the lifespan protocol. Either fix the application or set
`--asgi-lifespan off`.

### Connection limits

If you're hitting connection limits, increase `--worker-connections` or add
more workers with `--workers`.

### Slow responses under load

Try using uvloop for better performance:

```bash
pip install uvloop
gunicorn myapp:app --worker-class asgi --asgi-loop uvloop
```

## Framework Compatibility

The ASGI worker has been tested for compatibility with major ASGI frameworks.

| Framework | HTTP Scope | HTTP Messages | WebSocket | Lifespan | Streaming | Total |
|-----------|---------|---------|---------|---------|---------|-------|
| Django + Channels | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |
| FastAPI | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |
| Starlette | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |
| Quart | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |
| Litestar | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |
| BlackSheep | 19/19 | 18/19 | 19/19 | 8/8 | 9/9 | 73/74 |

**Overall:** 438/444 tests passed (98%)

!!! note
    The compatibility test suite is located in `tests/docker/asgi_framework_compat/`.
    Run `docker compose up -d --build` followed by `pytest tests/ -v` to execute the tests.

## See Also

- [Settings Reference](reference/settings.md#asgi_loop) - All ASGI-related settings
- [Deploy](deploy.md) - General deployment guidance
- [Design](design.md) - Worker architecture overview

---

### Content Community (`docs/content/community.md`)

# Community

Connect with the project through these channels.

## Project management & discussions

Project maintenance guidelines live on the
[wiki](https://github.com/benoitc/gunicorn/wiki/Project-management).

GitHub is used for:

- [Bug reports](https://github.com/benoitc/gunicorn/issues) — search before
  opening a new issue.
- [Discussions](https://github.com/benoitc/gunicorn/discussions) — Q&A and usage
  tips.
- [Feature planning](https://github.com/benoitc/gunicorn/issues) — development
  and project management topics.

## Chat

[**Open the chat in your browser**](https://web.libera.chat/?channels=%23gunicorn)
— no account, no install, pick a nickname and you are in.

It is the `#gunicorn` channel on [Libera.Chat](https://libera.chat/), so any IRC
client works too: `irc.libera.chat:6697` over TLS.

Chat is for quick questions and hallway conversation. Nobody is guaranteed to be
watching, and nothing said there is recorded, so anything worth keeping belongs
in a [discussion](https://github.com/benoitc/gunicorn/discussions) or an
[issue](https://github.com/benoitc/gunicorn/issues).

## Issue tracking

File bugs, enhancements, and tasks in the
[GitHub issue tracker](https://github.com/benoitc/gunicorn/issues).

## Security issues

Report security vulnerabilities privately to
[`security@gunicorn.org`](mailto:security@gunicorn.org); only core developers
subscribe to this list.

## Contributing

Start with the
[contributing guide](https://github.com/benoitc/gunicorn/blob/master/CONTRIBUTING.md)
for development workflow, code style, and review expectations. New contributors
are welcome—open a draft pull request early to gather feedback.

## Funding from outside the project

A cryptocurrency token called $GUNICORN trades on
[pump.fun](https://pump.fun/coin/4xMegMRMd2TFQEXxv39vtMP1r5fFVuA7VcaSmAhLpump).
It was not created by Benoît Chesneau and is not operated by, affiliated with, or
endorsed by the Gunicorn project. Holding it gives you no ownership or governance
rights in Gunicorn, no say in its direction, and no claim on the project.

The token's creator allocated me a share of its creator rewards. These rewards
help fund my work maintaining and developing Gunicorn, and I am grateful for the
support.

It is mentioned here for transparency, because it exists and because I benefit
from it. It is not a recommendation. $GUNICORN is an independent and highly
speculative token that can lose most or all of its value, so treat it as you
would any speculative asset rather than as a way to fund this project.

If you want to support the development and maintenance of Gunicorn directly,
you can do so through [sponsorship](sponsor.md).

Other tokens using the Gunicorn name may exist. They have no connection to the
Gunicorn project or to me unless explicitly stated here.

---

### Content Configure (`docs/content/configure.md`)

<span id="configuration"></span>
# Configuration Overview

Gunicorn reads configuration from five places, in increasing order of priority:

1. Environment variables, for settings that support them.
2. Framework-specific configuration (currently Paste Deploy only).
3. A Python configuration file `gunicorn.conf.py` (default in the working directory).
4. The `GUNICORN_CMD_ARGS` environment variable.
5. Command-line arguments.

If a configuration file is provided both via `GUNICORN_CMD_ARGS` and the CLI,
only the file specified on the command line is used.

!!! note
    Print the fully resolved configuration:

    ```bash
    gunicorn --print-config APP_MODULE
    ```

    Validate configuration and exit:

    ```bash
    gunicorn --check-config APP_MODULE
    ```

    This is also a quick way to confirm that your application can start.

## Command line

Options set on the command line override framework settings and values from the
configuration file. Not every setting has a command-line flag; run

```bash
gunicorn -h
```

for the complete list. The CLI also exposes `--version`, which is not part of
the main [settings reference](reference/settings.md).

<span id="configuration_file"></span>
## Configuration file

Provide a Python file (for example `gunicorn.conf.py`). Gunicorn executes the
file on every start or reload, so any valid Python is allowed:

```python
import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
```

Every configuration key is documented in the [settings reference](reference/settings.md).

## Framework settings

At present only Paste Deploy applications expose framework-specific settings.
If you have ideas for Django or other frameworks, open an
[issue](https://github.com/benoitc/gunicorn/issues).

### Paste applications

Reference Gunicorn as the server in your INI file:

```ini
[server:main]
use = egg:gunicorn#main
host = 192.168.0.1
port = 80
workers = 2
proc_name = brim
```

Gunicorn merges any recognised parameters into the base configuration. Values
from the configuration file and command line still override these defaults.

---

### Content Custom (`docs/content/custom.md`)

<span id="custom"></span>
# Custom Application

!!! info "Added in 19.0"
    Use Gunicorn as part of your own WSGI application by subclassing
    `gunicorn.app.base.BaseApplication`.



Example: create a tiny WSGI app and load it with a custom application:

```text
--8<-- "examples/standalone_app.py"
```



## Using server hooks

Provide hooks through configuration, just like a standard Gunicorn deployment.
For example, a `pre_fork` hook:

```python
def pre_fork(server, worker):
    print(f"pre-fork server {server} worker {worker}", file=sys.stderr)

if __name__ == "__main__":
    options = {
        "bind": "127.0.0.1:8080",
        "workers": number_of_workers(),
        "pre_fork": pre_fork,
    }
```

## Direct usage of existing WSGI apps

Run Gunicorn from Python to serve a WSGI application instance at runtime—useful
for rolling deploys or packaging with PEX. Gunicorn exposes
`gunicorn.app.wsgiapp`, which accepts any WSGI app (for example a Flask or
Django instance). Assuming your package is `exampleapi` and the application is
`app`:

```bash
python -m gunicorn.app.wsgiapp exampleapi:app
```

All CLI flags and configuration files still apply:

```bash
# Custom parameters
python -m gunicorn.app.wsgiapp exampleapi:app --bind=0.0.0.0:8081 --workers=4
# Using a config file
python -m gunicorn.app.wsgiapp exampleapi:app -c config.py
```

For PEX builds use `-c gunicorn` at build time so the packaged app accepts the
entry point at runtime:

```bash
pex . -v -c gunicorn -o compiledapp.pex
./compiledapp.pex exampleapi:app -c gunicorn_config.py
```

---

### Content Deploy (`docs/content/deploy.md`)

# Deploying Gunicorn

We strongly recommend running Gunicorn behind a proxy server.

## Nginx configuration

Although many HTTP proxies exist, we recommend [Nginx](https://nginx.org/).
When using the default synchronous workers you must ensure the proxy buffers
slow clients; otherwise Gunicorn becomes vulnerable to denial-of-service
attacks. Use [Hey](https://github.com/rakyll/hey) to verify proxy behaviour.

An example configuration for fast clients with Nginx
([source](https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf)):

```nginx title="nginx.conf"
--8<-- "examples/nginx.conf"
```



To support streaming requests/responses or patterns such as Comet, long
polling, or WebSockets, disable proxy buffering and run Gunicorn with an async
worker class:

```nginx
location @proxy_to_app {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_buffering off;

    proxy_pass http://app_server;
}
```

To ignore aborted requests (for example, health checks that close connections
prematurely) enable
[`proxy_ignore_client_abort`](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_client_abort):

```nginx
proxy_ignore_client_abort on;
```

!!! note
    The default value for `proxy_ignore_client_abort` is `off`. If it remains off
    Nginx logs will report error 499 and Gunicorn may log `Ignoring EPIPE` when the
    log level is `debug`.



Pass protocol information to Gunicorn so applications can generate correct
URLs. Add this header to your `location` block:

```nginx
proxy_set_header X-Forwarded-Proto $scheme;
```

If Nginx runs on a different host, tell Gunicorn which proxies are trusted so it
accepts the `X-Forwarded-*` headers:

```bash
gunicorn -w 3 --forwarded-allow-ips="10.170.3.217,10.170.3.220" test:app
```

When all traffic comes from trusted proxies (for example Heroku) you can set
`--forwarded-allow-ips='*'`. This is **dangerous** if untrusted clients can
reach Gunicorn directly, because forged headers could make your application
serve secure content over plain HTTP.

Gunicorn 19 changed the handling of `REMOTE_ADDR` to conform to
[RFC 3875](https://www.rfc-editor.org/rfc/rfc3875), meaning it now records the
proxy IP rather than the upstream client. To log the real client address, set
[`access_log_format`](reference/settings.md#access_log_format) to include `X-Forwarded-For`:

```text
%({x-forwarded-for}i)s %(l.md)s %(u.md)s %(t.md)s "%(r.md)s" %(s.md)s %(b.md)s "%(f.md)s" "%(a.md)s"
```

When binding Gunicorn to a UNIX socket `REMOTE_ADDR` will be empty.

## PROXY Protocol

The [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
allows load balancers and reverse proxies to pass original client connection
information (IP address, port) to backend servers. This is especially useful
when TLS termination happens at the proxy layer.

Gunicorn supports both PROXY protocol v1 (text format) and v2 (binary format).

### Configuration

Enable PROXY protocol with the `--proxy-protocol` option:

```bash
# Auto-detect v1 or v2 (recommended)
gunicorn --proxy-protocol auto app:app

# Force v1 only (text format)
gunicorn --proxy-protocol v1 app:app

# Force v2 only (binary format, more efficient)
gunicorn --proxy-protocol v2 app:app
```

Using `--proxy-protocol` without a value is equivalent to `auto`.

!!! warning "Security"
    Only enable PROXY protocol when Gunicorn is behind a trusted proxy that sends
    PROXY headers. Configure [`--proxy-allow-from`](reference/settings.md#proxy_allow_ips)
    to restrict which IPs can send PROXY protocol headers.

### HAProxy

HAProxy can send PROXY protocol headers to backends. Example configuration:

```haproxy
frontend https_front
    bind *:443 ssl crt /etc/ssl/certs/site.pem
    default_backend gunicorn_back

backend gunicorn_back
    # Send PROXY protocol v2 (binary, more efficient)
    server gunicorn 127.0.0.1:8000 send-proxy-v2

    # Or use v1 (text format)
    # server gunicorn 127.0.0.1:8000 send-proxy
```

Start Gunicorn to accept PROXY protocol:

```bash
gunicorn -b 127.0.0.1:8000 --proxy-protocol v2 --proxy-allow-from 127.0.0.1 app:app
```

### stunnel

[stunnel](https://www.stunnel.org/) can terminate TLS and forward connections
with PROXY protocol headers:

```ini
# /etc/stunnel/stunnel.conf
[https]
accept = 443
connect = 127.0.0.1:8000
cert = /etc/ssl/certs/stunnel.pem
key = /etc/ssl/certs/stunnel.key
protocol = proxy
```

The `protocol = proxy` directive tells stunnel to prepend PROXY protocol v1
headers to forwarded connections.

### AWS/ELB

AWS Network Load Balancers (NLB) and Application Load Balancers (ALB) support
PROXY protocol v2. Enable it in the target group settings, then configure
Gunicorn:

```bash
gunicorn --proxy-protocol v2 --proxy-allow-from '*' app:app
```

!!! note
    When using `--proxy-allow-from '*'` ensure Gunicorn is not directly
    accessible from the internet—only through the load balancer.

## Using virtual environments

Install Gunicorn inside your project
[virtual environment](https://pypi.python.org/pypi/virtualenv) to keep versions
isolated:

```bash
mkdir ~/venvs/
virtualenv ~/venvs/webapp
source ~/venvs/webapp/bin/activate
pip install gunicorn
deactivate
```

Force installation into the active virtual environment with `--ignore-installed`:

```bash
source ~/venvs/webapp/bin/activate
pip install -I gunicorn
```

## Monitoring

!!! note
    Do not enable Gunicorn's daemon mode when using process monitors. These
    supervisors expect to manage the direct child process.



### Gaffer

Use [Gaffer](https://gaffer.readthedocs.io/) with *gafferd* to manage Gunicorn:

```ini
[process:gunicorn]
cmd = gunicorn -w 3 test:app
cwd = /path/to/project
```

Create a `Procfile` if you prefer:

```procfile
gunicorn = gunicorn -w 3 test:app
```

Start Gunicorn via Gaffer:

```bash
gaffer start
```

Or load it into a running *gafferd* instance:

```bash
gaffer load
```

### runit

[runit](http://smarden.org/runit/) is a popular supervisor. A sample service
script (see the
[full example](https://github.com/benoitc/gunicorn/blob/master/examples/gunicorn_rc)):

```bash
#!/bin/sh

GUNICORN=/usr/local/bin/gunicorn
ROOT=/path/to/project
PID=/var/run/gunicorn.pid

APP=main:application

if [ -f $PID ]; then rm $PID; fi

cd $ROOT
exec $GUNICORN -c $ROOT/gunicorn.conf.py --pid=$PID $APP
```

Save as `/etc/sv/<app_name>/run`, make it executable, and symlink into
`/etc/service/<app_name>`. runit will then supervise Gunicorn.

### Supervisor

[Supervisor](http://supervisord.org/) configuration example (adapted from
[examples/supervisor.conf](https://github.com/benoitc/gunicorn/blob/master/examples/supervisor.conf)):

```ini
[program:gunicorn]
command=/path/to/gunicorn main:application -c /path/to/gunicorn.conf.py
directory=/path/to/project
user=nobody
autostart=true
autorestart=true
redirect_stderr=true
```

### Upstart

Sample Upstart config (logs go to `/var/log/upstart/myapp.log`):

```upstart
# /etc/init/myapp.conf

description "myapp"

start on (filesystem.md)
stop on runlevel [016]

respawn
setuid nobody
setgid nogroup
chdir /path/to/app/directory

exec /path/to/virtualenv/bin/gunicorn myapp:app
```

### systemd

[systemd](https://www.freedesktop.org/wiki/Software/systemd/) can create a UNIX
socket and launch Gunicorn on demand.

Service file:

```ini
# /etc/systemd/system/gunicorn.service

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
NotifyAccess=main
User=someuser
Group=someuser
WorkingDirectory=/home/someuser/applicationroot
ExecStart=/usr/bin/gunicorn applicationname.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

`Type=notify` lets Gunicorn report readiness to systemd. If the service should
run under a transient user consider adding `DynamicUser=true`. Tighten
permissions further with `ProtectSystem=strict` if the app permits.

Socket activation file:

```ini
# /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
```

Enable and start the socket so it begins listening immediately and on reboot:

```bash
systemctl enable --now gunicorn.socket
```

Test connectivity from the nginx user (Debian defaults to `www-data`):

```bash
sudo -u www-data curl --unix-socket /run/gunicorn.sock http
```

!!! note
    Use `systemctl show --value -p MainPID gunicorn.service` to retrieve the main
    process ID or `systemctl kill -s HUP gunicorn.service` to send signals.



Configure Nginx to proxy to the new socket:

```nginx
user www-data;
...
http {
    server {
        listen          8000;
        server_name     127.0.0.1;
        location / {
            proxy_pass http://unix:/run/gunicorn.sock;
        }
    }
}
...
```

!!! note
    Adjust `listen` and `server_name` for production (typically port 80 and your
    site's domain).



Ensure nginx starts automatically:

```bash
systemctl enable nginx.service
systemctl start nginx
```

Browse to <http://127.0.0.1:8000/> to verify Gunicorn + Nginx + systemd.

## Logging

Configure logging through the CLI flags described in the
[settings documentation](reference/settings.md#logging) or via a
[logging configuration file](https://github.com/benoitc/gunicorn/blob/master/examples/logging.conf).
Rotate logs with `logrotate` by sending `SIGUSR1`:

```bash
kill -USR1 $(cat /var/run/gunicorn.pid)
```

!!! note
    If you override the `LOGGING` dictionary, set `disable_existing_loggers` to
    `False` so Gunicorn's loggers remain active.



!!! warning
    Gunicorn's error log should capture Gunicorn-related messages only. Route your
    application logs separately.

---

### Content Design (`docs/content/design.md`)

<span id="design"></span>
# Design

A brief look at Gunicorn's architecture.

## Server Model

Gunicorn uses a **pre-fork worker model**: an arbiter process manages worker
processes, while the workers handle requests and responses. The arbiter never
touches individual client sockets.

<div class="pillars" markdown>

<div class="pillar" markdown>
<div class="pillar__icon">⚖️</div>

### Arbiter

Orchestrates the worker pool. Listens for signals (`TTIN`, `TTOU`, `CHLD`,
`HUP`) to adjust workers, restart them on failure, or reload configuration.
</div>

<div class="pillar" markdown>
<div class="pillar__icon">⚙️</div>

### Worker Pool

Each worker handles requests independently. Worker types determine
concurrency model: sync, threaded, or async via greenlets/asyncio.
</div>

<div class="pillar" markdown>
<div class="pillar__icon">📡</div>

### Signal Communication

`TTIN`/`TTOU` adjust worker count. `CHLD` triggers restart of crashed
workers. `HUP` reloads configuration. See [Signals](signals.md).
</div>

</div>

## Worker Types

Choose a worker type based on your application's needs.

=== "Sync"

    The **default** worker. Handles one request at a time per worker.

    - Simple and predictable
    - Errors affect only the current request
    - No keep-alive support (connections close after response)
    - Requires a buffering proxy (nginx, HAProxy) for production

    ```bash
    gunicorn myapp:app
    ```

=== "Gthread"

    Threaded worker with a **thread pool** per worker process.

    - Supports keep-alive connections
    - Good balance of concurrency and simplicity
    - Threads share memory (lower footprint than workers)
    - Idle connections close after keepalive timeout

    ```bash
    gunicorn myapp:app -k gthread --threads 4
    ```

=== "ASGI"

    Native **asyncio** support for modern async frameworks.

    - For FastAPI, Starlette, Quart, and other ASGI apps
    - Full async/await support
    - See the [ASGI Guide](asgi.md) for details

    ```bash
    gunicorn myapp:app -k uvicorn.workers.UvicornWorker
    ```

=== "Gevent"

    **Greenlet-based** async worker using [Gevent](http://www.gevent.org/).

    - Handles thousands of concurrent connections
    - Supports keep-alive, WebSockets, long-polling
    - May require patches for some libraries (e.g., `psycogreen` for Psycopg)
    - Not compatible with code that relies on blocking behavior

    ```bash
    gunicorn myapp:app -k gevent --worker-connections 1000
    ```

=== "Tornado"

    Worker for [Tornado](https://www.tornadoweb.org/) applications.

    - Designed for Tornado's async framework
    - Can serve WSGI apps, but not recommended for that use case
    - Use when running native Tornado applications

    ```bash
    gunicorn myapp:app -k tornado
    ```

## Comparison

| Worker | Concurrency Model | Keep-Alive | Best For |
|--------|-------------------|------------|----------|
| `sync` | 1 request/worker | ❌ | CPU-bound apps behind a proxy |
| `gthread` | Thread pool | ✅ | Mixed workloads, moderate concurrency |
| ASGI workers | AsyncIO | ✅ | Modern async frameworks (FastAPI, etc.) |
| `gevent` | Greenlets | ✅ | I/O-bound, WebSockets, streaming |
| `tornado` | Tornado IOLoop | ✅ | Native Tornado applications |

!!! tip "Quick Decision Guide"

    - **Simple app behind nginx?** → `sync` (default)
    - **Need keep-alive or moderate concurrency?** → `gthread`
    - **WebSockets, streaming, long-polling?** → `gevent` or ASGI worker
    - **FastAPI, Starlette, or async framework?** → ASGI worker

## When to Use Async Workers

Synchronous workers assume your app is CPU or network bound and avoids
indefinite blocking operations. Use async workers when you have:

- Long blocking calls (external APIs, slow databases)
- Direct internet traffic without a buffering proxy
- Streaming request/response bodies
- Long polling or Comet patterns
- WebSockets

!!! info "Testing Slow Clients"

    Tools like [Hey](https://github.com/rakyll/hey) can simulate slow responses
    to test how your configuration handles them.

## Scaling

### How Many Workers?

!!! warning "Don't Over-Scale"

    Workers ≠ clients. Gunicorn typically needs only **4–12 workers** to handle
    heavy traffic. Too many workers waste resources and can reduce throughput.

Start with this formula and adjust under load:

```
workers = (2 × CPU cores) + 1
```

Use `TTIN`/`TTOU` signals to adjust the worker count at runtime.

### How Many Threads?

With the `gthread` worker, you can combine workers and threads:

```bash
gunicorn myapp:app -k gthread --workers 4 --threads 2
```

!!! info "Threads vs Workers"

    - **Threads** share memory → lower footprint
    - **Workers** isolate failures → better fault tolerance
    - Combine both for the best of both worlds

Threads can extend request time beyond the worker timeout while still
notifying the arbiter. The optimal mix depends on your runtime (CPython vs
PyPy) and workload.

## Configuration Examples

```bash
# Sync (default) - simple apps behind nginx
gunicorn myapp:app

# Gthread - keep-alive and thread concurrency
gunicorn myapp:app -k gthread --workers 4 --threads 4

# Gevent - high concurrency for I/O-bound apps
gunicorn myapp:app -k gevent --workers 4 --worker-connections 1000

# ASGI - FastAPI/Starlette with Uvicorn worker
gunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4
```

<span id="asyncio-workers"></span>

!!! note "Third-Party AsyncIO Workers"

    For asyncio frameworks, you can also use third-party workers. See the
    [aiohttp deployment guide](https://docs.aiohttp.org/en/stable/deployment.html#nginx-gunicorn)
    for examples.

---

### Content Dirty (`docs/content/dirty.md`)

---
title: Dirty Arbiters
menu:
    guides:
        weight: 10
---

# Dirty Arbiters

!!! warning "Beta Feature"
    Dirty Arbiters is a beta feature introduced in Gunicorn 25.0.0. While it
    has been tested, the API and behavior may change in future releases. Please
    report any issues on [GitHub](https://github.com/benoitc/gunicorn/issues).

Dirty Arbiters provide a separate process pool for executing long-running,
blocking operations (AI model loading, heavy computation) without blocking HTTP
workers. This feature is inspired by Erlang's dirty schedulers.

## Overview

Traditional Gunicorn workers are designed to handle HTTP requests quickly.
Long-running operations like loading ML models or performing heavy computation
can block these workers, reducing the server's ability to handle concurrent
requests.

Dirty Arbiters solve this by providing:

- **Separate worker pool** - Completely separate from HTTP workers, can be
  killed/restarted independently
- **Stateful workers** - Loaded resources persist in dirty worker memory
- **Message-passing IPC** - Communication via Unix sockets with binary TLV
  protocol
- **Explicit API** - Clear `execute()` calls (no hidden IPC)
- **Asyncio-based** - Clean concurrent handling with streaming support

## Design Philosophy

Dirty Arbiters follow several key design principles:

### Separate Process Hierarchy

Unlike threads or in-process pools, Dirty Arbiters use a fully separate process
tree:

- **Isolation** - A crash or memory leak in a dirty worker cannot affect HTTP
  workers
- **Independent lifecycle** - Dirty workers can be killed/restarted without
  affecting request handling
- **Resource accounting** - OS-level memory limits can be applied per-process
- **Clean shutdown** - Each process tree can be signaled and terminated
  independently

### Erlang Inspiration

The name and concept come from Erlang's "dirty schedulers" - special schedulers
that handle operations that would block normal schedulers. In Erlang, dirty
schedulers run NIFs (Native Implemented Functions) that can't yield. Similarly,
Gunicorn's Dirty Arbiters handle Python operations that would block HTTP
workers.

### Why Asyncio

The Dirty Arbiter uses asyncio for its core loop rather than the main arbiter's
select-based approach:

- **Non-blocking IPC** - Can handle many concurrent client connections
  efficiently
- **Concurrent request routing** - Multiple requests can be dispatched to
  workers simultaneously
- **Streaming support** - Native async generators for streaming responses
- **Clean signal handling** - Signals integrate cleanly via
  `loop.add_signal_handler()`

### Stateful Applications

Traditional WSGI apps are request-scoped - they're invoked per-request and
don't maintain state between requests. Dirty apps are different:

- **Long-lived** - Apps persist in worker memory for the worker's lifetime
- **Pre-loaded resources** - Models, connections, and caches stay loaded
- **Explicit state management** - Apps control their own lifecycle via `init()`
  and `close()`

This makes dirty apps ideal for ML inference, where loading a model once and
reusing it for many requests is essential.

## Architecture

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Process Relationships

| Component | Parent | Communication |
|-----------|--------|---------------|
| Main Arbiter | init/systemd | Signals from OS |
| HTTP Workers | Main Arbiter | Pipes, signals |
| Dirty Arbiter | Main Arbiter | Signals, exit status |
| Dirty Workers | Dirty Arbiter | Unix socket, signals, WorkerTmp |

## Configuration

Add these settings to your Gunicorn configuration file or command line:

```python
# gunicorn.conf.py
dirty_apps = [
    "myapp.ml:MLApp",
    "myapp.images:ImageApp",
]
dirty_workers = 2          # Number of dirty workers
dirty_timeout = 300        # Task timeout in seconds
dirty_threads = 1          # Threads per worker
dirty_graceful_timeout = 30  # Shutdown timeout
```

Or via command line:

```bash
gunicorn myapp:app \
    --dirty-app myapp.ml:MLApp \
    --dirty-app myapp.images:ImageApp \
    --dirty-workers 2 \
    --dirty-timeout 300
```

### Configuration Options

| Setting | Default | Description |
|---------|---------|-------------|
| `dirty_apps` | `[]` | List of dirty app import paths |
| `dirty_workers` | `0` | Number of dirty workers (0 = disabled) |
| `dirty_timeout` | `300` | Task timeout in seconds |
| `dirty_threads` | `1` | Threads per dirty worker |
| `dirty_graceful_timeout` | `30` | Graceful shutdown timeout |

## Per-App Worker Allocation

By default, all dirty workers load all configured apps. For apps that consume
significant memory (like large ML models), you can limit how many workers load
a specific app.

### Why Per-App Allocation?

Consider a scenario with a 10GB ML model and 8 dirty workers:

- **Default behavior**: 8 workers × 10GB = 80GB RAM
- **With `workers=2`**: 2 workers × 10GB = 20GB RAM (75% savings)

Requests for the limited app are routed only to workers that have it loaded.

### Configuration Methods

**Method 1: Class Attribute**

Set the `workers` attribute on your DirtyApp class:

```python
from gunicorn.dirty import DirtyApp

class HeavyModelApp(DirtyApp):
    workers = 2  # Only 2 workers will load this app

    def init(self):
        self.model = load_10gb_model()

    def predict(self, data):
        return self.model.predict(data)

    def close(self):
        pass
```

**Method 2: Config Override**

Use the `module:class:N` format in your config:

```python
# gunicorn.conf.py
dirty_apps = [
    "myapp.light:LightApp",           # All workers (default)
    "myapp.heavy:HeavyModelApp:2",    # Only 2 workers
    "myapp.single:SingletonApp:1",    # Only 1 worker
]
dirty_workers = 4
```

Config overrides take precedence over class attributes.

### Worker Distribution

When workers spawn, apps are assigned based on their limits:

```
Example with dirty_workers=4:
  - LightApp (workers=None):  Loaded on workers 1, 2, 3, 4
  - HeavyModelApp (workers=2): Loaded on workers 1, 2
  - SingletonApp (workers=1):  Loaded on worker 1

Worker 1: [LightApp, HeavyModelApp, SingletonApp]
Worker 2: [LightApp, HeavyModelApp]
Worker 3: [LightApp]
Worker 4: [LightApp]
```

### Request Routing

Requests are automatically routed to workers that have the target app:

```python
client = get_dirty_client()

# Goes to any of 4 workers (round-robin)
client.execute("myapp.light:LightApp", "action")

# Goes to worker 1 or 2 only (round-robin between those)
client.execute("myapp.heavy:HeavyModelApp", "predict", data)

# Always goes to worker 1
client.execute("myapp.single:SingletonApp", "process")
```

### Error Handling

If no workers have the requested app loaded, a `DirtyNoWorkersAvailableError`
is raised:

```python
from gunicorn.dirty import get_dirty_client
from gunicorn.dirty.errors import DirtyNoWorkersAvailableError

def my_view(request):
    client = get_dirty_client()
    try:
        result = client.execute("myapp.heavy:HeavyModelApp", "predict", data)
    except DirtyNoWorkersAvailableError as e:
        # All workers with this app are down or app not configured
        return {"error": "Service temporarily unavailable", "app": e.app_path}
```

### Worker Crash Recovery

When a worker crashes, its replacement gets the **same apps** as the dead
worker:

```
Timeline:
  t=0: Worker 1 crashes (had HeavyModelApp)
  t=1: Arbiter detects crash, queues respawn
  t=2: New Worker 5 spawns with same apps as Worker 1
  t=3: HeavyModelApp still available on Worker 2 during gap
```

This ensures:

- No memory redistribution on existing workers
- Predictable replacement behavior
- The heavy model is only loaded on the new worker

### Best Practices

1. **Set realistic limits** - Don't set `workers=1` unless truly necessary
   (single point of failure)
2. **Monitor memory** - Track per-worker memory to tune allocation
3. **Handle unavailability** - Catch `DirtyNoWorkersAvailableError` gracefully
4. **Use class attributes for app-specific limits** - Makes the limit part of
   the app definition
5. **Use config for deployment-specific overrides** - Different limits for dev
   vs prod

## Creating a Dirty App

Dirty apps inherit from `DirtyApp` and implement three methods:

```python
# myapp/dirty.py
from gunicorn.dirty import DirtyApp

class MLApp(DirtyApp):
    """Dirty application for ML workloads."""

    def __init__(self):
        self.models = {}

    def init(self):
        """Called once at dirty worker startup."""
        # Pre-load commonly used models
        self.models['default'] = self._load_model('base-model')

    def __call__(self, action, *args, **kwargs):
        """Dispatch to action methods."""
        method = getattr(self, action, None)
        if method is None:
            raise ValueError(f"Unknown action: {action}")
        return method(*args, **kwargs)

    def load_model(self, name):
        """Load a model into memory."""
        if name not in self.models:
            self.models[name] = self._load_model(name)
        return {"loaded": True, "name": name}

    def inference(self, model_name, input_text):
        """Run inference on loaded model."""
        model = self.models.get(model_name)
        if not model:
            raise ValueError(f"Model not loaded: {model_name}")
        return model.predict(input_text)

    def _load_model(self, name):
        import torch
        model = torch.load(f"models/{name}.pt")
        return model

    def close(self):
        """Cleanup on shutdown."""
        for model in self.models.values():
            del model
```

### DirtyApp Interface

| Method/Attribute | Description |
|------------------|-------------|
| `workers` | Class attribute. Number of workers to load this app (`None` = all workers). |
| `init()` | Called once when dirty worker starts, after instantiation. Load resources here. |
| `__call__(action, *args, **kwargs)` | Handle requests from HTTP workers. |
| `close()` | Called when dirty worker shuts down. Cleanup resources. |

### Initialization Sequence

When a dirty worker starts, initialization happens in this order:

1. **Fork** - Worker process is forked from dirty arbiter
2. **`dirty_post_fork(arbiter, worker)`** - Hook called immediately after fork
3. **App instantiation** - Each dirty app class is instantiated (`__init__`)
4. **`app.init()`** - Called for each app after instantiation (load models,
   resources)
5. **`dirty_worker_init(worker)`** - Hook called after ALL apps are initialized
6. **Run loop** - Worker starts accepting requests from HTTP workers

This means:

- Use `__init__` for basic setup (initialize empty containers, store config)
- Use `init()` for heavy loading (ML models, database connections, large files)
- The `dirty_worker_init` hook fires only after all apps have completed their
  `init()` calls

## Using from HTTP Workers

### Sync Workers (sync, gthread)

```python
from gunicorn.dirty import get_dirty_client

def my_view(request):
    client = get_dirty_client()

    # Load a model
    client.execute("myapp.ml:MLApp", "load_model", "gpt-4")

    # Run inference
    result = client.execute(
        "myapp.ml:MLApp",
        "inference",
        "gpt-4",
        input_text=request.data
    )
    return result
```

### Async Workers (ASGI)

```python
from gunicorn.dirty import get_dirty_client_async

async def my_view(request):
    client = await get_dirty_client_async()

    # Non-blocking execution
    await client.execute_async("myapp.ml:MLApp", "load_model", "gpt-4")

    result = await client.execute_async(
        "myapp.ml:MLApp",
        "inference",
        "gpt-4",
        input_text=request.data
    )
    return result
```

## Streaming

Dirty Arbiters support streaming responses for use cases like LLM token
generation, where data is produced incrementally. This enables real-time
delivery of results without waiting for complete execution.

### Streaming with Generators

Any dirty app action that returns a generator (sync or async) automatically
streams chunks to the client:

```python
# myapp/llm.py
from gunicorn.dirty import DirtyApp

class LLMApp(DirtyApp):
    def init(self):
        from transformers import pipeline
        self.generator = pipeline("text-generation", model="gpt2")

    def generate(self, prompt):
        """Sync streaming - yields tokens."""
        for token in self.generator(prompt, stream=True):
            yield token["generated_text"]

    async def generate_async(self, prompt):
        """Async streaming - yields tokens."""
        import openai
        client = openai.AsyncOpenAI()
        stream = await client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            stream=True
        )
        async for chunk in stream:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content

    def close(self):
        pass
```

### Client Streaming API

Use `stream()` for sync workers and `stream_async()` for async workers:

**Sync Workers (sync, gthread):**

```python
from gunicorn.dirty import get_dirty_client

def generate_view(request):
    client = get_dirty_client()

    def generate_response():
        for chunk in client.stream("myapp.llm:LLMApp", "generate", request.prompt):
            yield chunk

    return StreamingResponse(generate_response())
```

**Async Workers (ASGI):**

```python
from gunicorn.dirty import get_dirty_client_async

async def generate_view(request):
    client = await get_dirty_client_async()

    async def generate_response():
        async for chunk in client.stream_async("myapp.llm:LLMApp", "generate", request.prompt):
            yield chunk

    return StreamingResponse(generate_response())
```

### Streaming Protocol

Streaming uses a simple protocol with three message types:

1. **Chunk** (`type: "chunk"`) - Contains partial data
2. **End** (`type: "end"`) - Signals stream completion
3. **Error** (`type: "error"`) - Signals error during streaming

Example message flow:
```
Client -> Arbiter -> Worker: request
Worker -> Arbiter -> Client: chunk (data: "Hello")
Worker -> Arbiter -> Client: chunk (data: " ")
Worker -> Arbiter -> Client: chunk (data: "World")
Worker -> Arbiter -> Client: end
```

## Binary Protocol

The dirty worker IPC uses a binary protocol inspired by OpenBSD msgctl/msgsnd
for efficient data transfer. This eliminates base64 encoding overhead for
binary data like images, audio, or model weights.

### Header Format (16 bytes)

```
+--------+--------+--------+--------+--------+--------+--------+--------+
|  Magic (2B)     | Ver(1) | MType  |        Payload Length (4B)        |
+--------+--------+--------+--------+--------+--------+--------+--------+
|                       Request ID (8 bytes)                            |
+--------+--------+--------+--------+--------+--------+--------+--------+
```

- **Magic**: `0x47 0x44` ("GD" for Gunicorn Dirty)
- **Version**: `0x01`
- **MType**: Message type (`0x01`=REQUEST, `0x02`=RESPONSE, `0x03`=ERROR,
  `0x04`=CHUNK, `0x05`=END)
- **Length**: Payload size (big-endian uint32, max 64MB)
- **Request ID**: uint64 identifier

### TLV Payload Encoding

Payloads use Type-Length-Value encoding:

| Type | Code | Description |
|------|------|-------------|
| None | `0x00` | No value bytes |
| Bool | `0x01` | 1 byte (0x00/0x01) |
| Int64 | `0x05` | 8 bytes big-endian signed |
| Float64 | `0x06` | 8 bytes IEEE 754 |
| Bytes | `0x10` | 4-byte length + raw bytes |
| String | `0x11` | 4-byte length + UTF-8 |
| List | `0x20` | 4-byte count + elements |
| Dict | `0x21` | 4-byte count + key-value pairs |

### Binary Data Benefits

The binary protocol allows passing raw bytes directly without encoding:

```python
# Image processing with binary data
def resize(self, image_data, width, height):
    """Resize an image - image_data is raw bytes."""
    img = Image.open(io.BytesIO(image_data))
    resized = img.resize((width, height))
    buffer = io.BytesIO()
    resized.save(buffer, format='PNG')
    return buffer.getvalue()  # Returns raw bytes

# Called from HTTP worker
thumbnail = client.execute(
    "myapp.images:ImageApp",
    "thumbnail",
    raw_image_bytes,  # No base64 encoding needed
    size=256
)
```

### Error Handling in Streams

Errors during streaming are delivered as error messages:

```python
def generate_view(request):
    client = get_dirty_client()

    try:
        for chunk in client.stream("myapp.llm:LLMApp", "generate", prompt):
            yield chunk
    except DirtyError as e:
        # Error occurred mid-stream
        yield f"\n[Error: {e.message}]"
```

### Best Practices for Streaming

1. **Use async generators for I/O-bound streaming** - e.g., API calls to
   external services
2. **Use sync generators for CPU-bound streaming** - e.g., local model
   inference
3. **Yield frequently** - Heartbeats are sent during streaming to keep workers
   alive
4. **Keep chunks small** - Smaller chunks provide better perceived latency
5. **Handle client disconnection** - Streams continue even if client
   disconnects; design accordingly

## Stash (Shared State via Message Passing)

Stash provides shared state between dirty workers, similar to Erlang's ETS
(Erlang Term Storage). Workers remain fully isolated - all state access goes
through message passing to the arbiter.

### Architecture

```
                    +------------------+
                    |   Dirty Arbiter  |
                    |                  |
                    |  stash_tables:   |
                    |    sessions: {}  |
                    |    cache: {}     |
                    +--------+---------+
                             |
              Unix Socket IPC (message passing)
                             |
         +-------------------+-------------------+
         |                   |                   |
   +-----v-----+       +-----v-----+       +-----v-----+
   |  Worker 1 |       |  Worker 2 |       |  Worker 3 |
   |           |       |           |       |           |
   | (isolated)|       | (isolated)|       | (isolated)|
   +-----------+       +-----------+       +-----------+

   Workers have NO shared memory.
   All stash operations are IPC messages to arbiter.
```

### How It Works

1. Worker calls `stash.put("sessions", "user:1", data)`
2. Worker sends message to arbiter via Unix socket
3. Arbiter stores data in its memory (`self.stash_tables`)
4. Arbiter sends response back to worker
5. Worker receives confirmation

This is **not** shared memory - workers remain fully isolated. The arbiter acts
as a centralized store that workers communicate with via message passing. This
matches Erlang's model where ETS tables are owned by a process.

### Basic Usage

```python
from gunicorn.dirty import stash

# Store a value (table auto-created)
# This sends a message to arbiter, which stores it
stash.put("sessions", "user:123", {"name": "Alice", "role": "admin"})

# Retrieve a value
# This sends a request to arbiter, which returns the value
user = stash.get("sessions", "user:123")

# Delete a key
stash.delete("sessions", "user:123")

# Check existence
if stash.exists("sessions", "user:123"):
    print("Session exists")

# List keys with pattern matching
keys = stash.keys("sessions", pattern="user:*")
```

### Dict-like Interface

For more Pythonic access, use the table interface:

```python
from gunicorn.dirty import stash

# Get a table reference
sessions = stash.table("sessions")

# Dict-like operations (each is an IPC message)
sessions["user:123"] = {"name": "Alice"}
user = sessions["user:123"]
del sessions["user:123"]

# Iteration
for key in sessions:
    print(key, sessions[key])

# Length
count = len(sessions)
```

### Table Management

```python
from gunicorn.dirty import stash

# Explicit table creation (idempotent)
stash.ensure("cache")

# Get table info
info = stash.info("sessions")
print(f"Table has {info['size']} entries")

# Clear all entries in a table
stash.clear("sessions")

# Delete entire table
stash.delete_table("sessions")

# List all tables
tables = stash.tables()
```

### Using Stash in DirtyApp

Declare tables your app uses with the `stashes` class attribute:

```python
from gunicorn.dirty import DirtyApp, stash

class SessionApp(DirtyApp):
    # Tables declared here are auto-created on startup
    stashes = ["sessions", "counters"]

    def init(self):
        # Initialize counter if needed
        if not stash.exists("counters", "requests"):
            stash.put("counters", "requests", 0)

    def login(self, user_id, user_data):
        """Store session - any worker can read it via arbiter."""
        stash.put("sessions", f"user:{user_id}", {
            "data": user_data,
            "logged_in_at": time.time(),
        })
        self._increment_counter()
        return {"status": "ok"}

    def get_session(self, user_id):
        """Get session - request goes to arbiter."""
        return stash.get("sessions", f"user:{user_id}")

    def _increment_counter(self):
        """Increment global counter via arbiter."""
        current = stash.get("counters", "requests", 0)
        stash.put("counters", "requests", current + 1)

    def close(self):
        pass
```

### API Reference

| Function | Description |
|----------|-------------|
| `stash.put(table, key, value)` | Store a value (table auto-created) |
| `stash.get(table, key, default=None)` | Retrieve a value |
| `stash.delete(table, key)` | Delete a key, returns True if deleted |
| `stash.exists(table, key=None)` | Check if table/key exists |
| `stash.keys(table, pattern=None)` | List keys, optional glob pattern |
| `stash.clear(table)` | Delete all entries in table |
| `stash.info(table)` | Get table info (size, etc.) |
| `stash.ensure(table)` | Create table if not exists |
| `stash.delete_table(table)` | Delete entire table |
| `stash.tables()` | List all table names |
| `stash.table(name)` | Get dict-like interface |

### Patterns and Use Cases

**Session Storage:**
```python
# Store session on login (worker 1)
stash.put("sessions", f"user:{user_id}", session_data)

# Check session on request (may be worker 2)
session = stash.get("sessions", f"user:{user_id}")
if session is None:
    raise AuthError("Not logged in")
```

**Shared Cache:**
```python
def get_expensive_result(key):
    # Check cache first (via arbiter)
    cached = stash.get("cache", key)
    if cached is not None:
        return cached

    # Compute and cache
    result = expensive_computation()
    stash.put("cache", key, result)
    return result
```

**Global Counters:**
```python
def increment_counter(name):
    # Note: not atomic - two workers could read same value
    current = stash.get("counters", name, 0)
    stash.put("counters", name, current + 1)
    return current + 1
```

**Feature Flags:**
```python
# Set flag (from admin endpoint)
stash.put("flags", "new_feature", True)

# Check flag (from any worker)
if stash.get("flags", "new_feature", False):
    enable_new_feature()
```

### Error Handling

```python
from gunicorn.dirty.stash import (
    StashError,
    StashTableNotFoundError,
    StashKeyNotFoundError,
)

try:
    info = stash.info("nonexistent")
except StashTableNotFoundError as e:
    print(f"Table not found: {e.table_name}")

# Using get() with default avoids KeyNotFoundError
value = stash.get("table", "key", default="fallback")
```

### Best Practices

1. **Use descriptive table names** - `user_sessions`, `ml_cache`, not `data`
2. **Use key prefixes** - `user:123`, `cache:model:v1` for organization
3. **Handle missing data** - Always provide defaults or check existence
4. **Don't store large data** - Each access is an IPC round-trip
5. **Remember it's ephemeral** - Data is lost on arbiter restart

### Advantages

- **Worker isolation** - Workers remain fully isolated; no shared memory bugs
- **Simple API** - Dict-like interface, no locking required
- **Binary support** - Efficiently stores bytes (images, model weights)
- **Pattern matching** - `keys(pattern="user:*")` for querying
- **Zero setup** - Works automatically with dirty workers
- **Table-based** - Organize data into logical namespaces

### Limitations

- **No persistence** - Data lives only in arbiter memory
- **No transactions** - No atomic read-modify-write operations
- **No TTL** - Entries don't expire automatically
- **IPC overhead** - Each operation is a network round-trip
- **Single arbiter** - Not distributed across multiple machines

For persistent or distributed state, use Redis, PostgreSQL, or similar external
systems.

### Flask Example

```python
from flask import Flask, Response
from gunicorn.dirty import get_dirty_client

app = Flask(__name__)

@app.route("/chat", methods=["POST"])
def chat():
    prompt = request.json.get("prompt")
    client = get_dirty_client()

    def stream():
        for token in client.stream("myapp.llm:LLMApp", "generate", prompt):
            yield f"data: {token}\n\n"

    return Response(stream(), content_type="text/event-stream")
```

### FastAPI Example

```python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from gunicorn.dirty import get_dirty_client_async

app = FastAPI()

@app.post("/chat")
async def chat(prompt: str):
    client = await get_dirty_client_async()

    async def stream():
        async for token in client.stream_async("myapp.llm:LLMApp", "generate", prompt):
            yield f"data: {token}\n\n"

    return StreamingResponse(stream(), media_type="text/event-stream")
```

## Lifecycle Hooks

Dirty Arbiters provide hooks for customization:

```python
# gunicorn.conf.py

def on_dirty_starting(arbiter):
    """Called just before the dirty arbiter starts."""
    print("Dirty arbiter starting...")

def dirty_post_fork(arbiter, worker):
    """Called just after a dirty worker is forked."""
    print(f"Dirty worker {worker.pid} forked")

def dirty_worker_init(worker):
    """Called after a dirty worker initializes all apps."""
    print(f"Dirty worker {worker.pid} initialized")

def dirty_worker_exit(arbiter, worker):
    """Called when a dirty worker exits."""
    print(f"Dirty worker {worker.pid} exiting")

on_dirty_starting = on_dirty_starting
dirty_post_fork = dirty_post_fork
dirty_worker_init = dirty_worker_init
dirty_worker_exit = dirty_worker_exit
```

## Signal Handling

Dirty Arbiters integrate with the main arbiter's signal handling. Signals are
forwarded from the main arbiter to the dirty arbiter, which then propagates
them to workers.

### Signal Flow

```
  Main Arbiter                    Dirty Arbiter                 Dirty Workers
       |                                |                             |
  SIGTERM/SIGHUP/SIGUSR1 ------>  signal_handler()                    |
       |                                |                             |
       |                          call_soon_threadsafe()              |
       |                                |                             |
       |                          handle_signal()                     |
       |                                |                             |
       |                                +------> os.kill(worker, sig) |
       |                                                              |
```

### Signal Reference

| Signal | At Dirty Arbiter | At Dirty Workers | Notes |
|--------|-----------------|------------------|-------|
| `SIGTERM` | Sets `self.alive = False`, waits for graceful shutdown | Exits after completing current request | Graceful shutdown with timeout |
| `SIGQUIT` | Immediate exit via `sys.exit(0)` | Killed immediately | Fast shutdown, no cleanup |
| `SIGHUP` | Kills all workers, spawns new ones | Exits immediately | Hot reload of workers |
| `SIGUSR1` | Reopens log files, forwards to workers | Reopens log files | Log rotation support |
| `SIGTTIN` | Increases worker count by 1 | N/A | Dynamic scaling up |
| `SIGTTOU` | Decreases worker count by 1 | N/A | Dynamic scaling down |
| `SIGCHLD` | Handled by event loop, triggers reap | N/A | Worker death detection |
| `SIGINT` | Same as SIGTERM | Same as SIGTERM | Ctrl-C handling |

### Dynamic Scaling with TTIN/TTOU

You can dynamically scale the number of dirty workers at runtime using signals,
without restarting gunicorn:

```bash
# Find the dirty arbiter process
ps aux | grep dirty-arbiter
# Or use the PID file (location depends on your app name)
cat /tmp/gunicorn-dirty-myapp.pid

# Increase dirty workers by 1
kill -TTIN <dirty-arbiter-pid>

# Decrease dirty workers by 1
kill -TTOU <dirty-arbiter-pid>
```

**Minimum Worker Constraint:** The dirty arbiter will not decrease below the
minimum number of workers required by your app configurations. For example, if
you have an app with `workers = 3`, you cannot scale below 3 dirty workers.
When this limit is reached, a warning is logged:

```
WARNING: SIGTTOU: Cannot decrease below 3 workers (required by app specs)
```

**Use Cases:**

- **Burst handling** - Scale up when you anticipate heavy load
- **Cost optimization** - Scale down during low-traffic periods
- **Recovery** - Scale up if workers are busy with long-running tasks

### Forwarded Signals

The main arbiter forwards these signals to the dirty arbiter process:

- **SIGTERM** - Graceful shutdown of entire process tree
- **SIGHUP** - Worker reload (main arbiter reloads HTTP workers, dirty arbiter
  reloads dirty workers)
- **SIGUSR1** - Log rotation across all processes

### Async Signal Handling

The dirty arbiter uses asyncio's signal integration for safe handling in the
event loop:

```python
# Signals are registered with the event loop
loop.add_signal_handler(signal.SIGTERM, self.signal_handler, signal.SIGTERM)

def signal_handler(self, sig):
    # Use call_soon_threadsafe for thread-safe event loop integration
    self.loop.call_soon_threadsafe(self.handle_signal, sig)
```

This pattern ensures signals don't interrupt asyncio operations mid-execution,
preventing race conditions and partial state updates.

## Liveness and Health Monitoring

Dirty Arbiters implement multiple layers of health monitoring to ensure workers
remain responsive and orphaned processes are cleaned up.

### Heartbeat Mechanism

Each dirty worker maintains a "worker tmp" file whose mtime serves as a
heartbeat:

```
Worker Lifecycle:
  1. Worker spawns, creates WorkerTmp file
  2. Worker touches file every (dirty_timeout / 2) seconds
  3. Arbiter checks all worker mtimes every 1 second
  4. If mtime > dirty_timeout seconds old, worker is killed
```

This file-based heartbeat has several advantages:

- **OS-level tracking** - No IPC required, works even if worker is stuck in C
  code
- **Crash detection** - Arbiter notices immediately when worker stops updating
- **Graceful recovery** - Worker killed with SIGKILL, arbiter spawns
  replacement

### Timeout Detection

The arbiter's monitoring loop checks worker health every second:

```python
# Pseudocode for worker monitoring
for worker in self.workers:
    mtime = worker.tmp.last_update()
    if time.time() - mtime > self.dirty_timeout:
        log.warning(f"Worker {worker.pid} timed out, killing")
        os.kill(worker.pid, signal.SIGKILL)
```

When a worker is killed:

1. `SIGCHLD` is delivered to the arbiter
2. Arbiter reaps the worker process
3. `dirty_worker_exit` hook is called
4. A new worker is spawned to maintain `dirty_workers` count

### Parent Death Detection

Dirty arbiters monitor their parent process (the main arbiter) to detect
orphaning:

```python
# In the dirty arbiter's main loop
if os.getppid() != self.parent_pid:
    log.info("Parent died, shutting down")
    self.alive = False
```

This check runs every iteration of the event loop (typically sub-millisecond).
When parent death is detected:

1. Arbiter sets `self.alive = False`
2. All workers are sent SIGTERM
3. Arbiter waits for graceful shutdown (up to `dirty_graceful_timeout`)
4. Remaining workers are sent SIGKILL
5. Arbiter exits

### Orphan Cleanup

To handle edge cases where the dirty arbiter itself crashes, a well-known PID
file is used:

**PID file location**: `/tmp/gunicorn_dirty_<main_arbiter_pid>.pid`

On startup, the dirty arbiter:

1. Checks if PID file exists
2. If yes, reads the old PID and attempts to kill it (`SIGTERM`)
3. Waits briefly for cleanup
4. Writes its own PID to the file
5. On exit, removes the PID file

This ensures that if a dirty arbiter crashes and the main arbiter restarts it,
the old orphaned process is terminated.

### Respawn Behavior

| Component | Respawn Trigger | Respawn Behavior |
|-----------|-----------------|------------------|
| Dirty Worker | Exit, timeout, or crash | Immediate respawn to maintain `dirty_workers` count |
| Dirty Arbiter | Exit or crash | Main arbiter respawns if not shutting down |

The dirty arbiter maintains a target worker count and continuously spawns
workers until the target is reached:

```python
while len(self.workers) < self.num_workers:
    self.spawn_worker()
```

### Monitoring Recommendations

For production deployments, consider:

1. **Log monitoring** - Watch for "Worker timed out" messages indicating hung
   workers
2. **Process monitoring** - Use systemd or supervisord to monitor the main
   arbiter
3. **Metrics** - Track respawn frequency to detect unstable workers

```bash
# Check for recent worker timeouts
grep "Worker.*timed out" /var/log/gunicorn.log | tail -20

# Monitor process tree
watch -n 1 'pstree -p $(cat gunicorn.pid)'
```

## Error Handling

The dirty client raises specific exceptions:

```python
from gunicorn.dirty.errors import (
    DirtyError,
    DirtyTimeoutError,
    DirtyConnectionError,
    DirtyAppError,
    DirtyAppNotFoundError,
    DirtyNoWorkersAvailableError,
)

try:
    result = client.execute("myapp.ml:MLApp", "inference", "model", data)
except DirtyTimeoutError:
    # Operation timed out
    pass
except DirtyAppNotFoundError:
    # App not loaded in dirty workers
    pass
except DirtyNoWorkersAvailableError as e:
    # No workers have this app (all crashed or app limited to 0 workers)
    print(f"No workers for app: {e.app_path}")
except DirtyAppError as e:
    # Error during app execution
    print(f"App error: {e.message}, traceback: {e.traceback}")
except DirtyConnectionError:
    # Connection to dirty arbiter failed
    pass
```

## Best Practices

1. **Pre-load commonly used resources** in `init()` to avoid cold starts
2. **Set appropriate timeouts** based on your workload
3. **Handle errors gracefully** - dirty workers may restart
4. **Use meaningful action names** for easier debugging
5. **Keep responses serializable** - results are passed via binary IPC
   (supports bytes directly)

## Monitoring

Monitor dirty workers using standard process monitoring:

```bash
# Check dirty arbiter and workers
ps aux | grep "dirty"

# View logs
tail -f gunicorn.log | grep dirty
```

## Example: Image Processing

```python
# myapp/images.py
from gunicorn.dirty import DirtyApp
from PIL import Image
import io

class ImageApp(DirtyApp):
    def init(self):
        # Pre-import heavy libraries
        import cv2
        self.cv2 = cv2

    def resize(self, image_data, width, height):
        """Resize an image."""
        img = Image.open(io.BytesIO(image_data))
        resized = img.resize((width, height))
        buffer = io.BytesIO()
        resized.save(buffer, format='PNG')
        return buffer.getvalue()

    def thumbnail(self, image_data, size=128):
        """Create a thumbnail."""
        img = Image.open(io.BytesIO(image_data))
        img.thumbnail((size, size))
        buffer = io.BytesIO()
        img.save(buffer, format='JPEG')
        return buffer.getvalue()

    def close(self):
        pass
```

Usage:

```python
from gunicorn.dirty import get_dirty_client

def upload_image(request):
    client = get_dirty_client()

    # Create thumbnail in dirty worker
    thumbnail = client.execute(
        "myapp.images:ImageApp",
        "thumbnail",
        request.files['image'].read(),
        size=256
    )

    return save_thumbnail(thumbnail)
```

## Complete Examples

For full working examples with Docker deployment, see:

- [Embedding Service
  Example](https://github.com/benoitc/gunicorn/tree/master/examples/embedding_service)
  - FastAPI-based text embedding API using sentence-transformers with dirty
  workers for ML model management.
- [Streaming Chat
  Example](https://github.com/benoitc/gunicorn/tree/master/examples/streaming_chat)
  - Simulated LLM chat with token-by-token SSE streaming, demonstrating dirty
  worker generators and real-time response delivery.

---

### Content Faq (`docs/content/faq.md`)

<span id="faq"></span>
# FAQ

## WSGI bits

### How do I set `SCRIPT_NAME`?

By default `SCRIPT_NAME` is an empty string. Set it via an environment variable
or HTTP header. Because the header contains an underscore it is only accepted
from trusted forwarders listed in [`forwarded_allow_ips`](reference/settings.md#forwarded_allow_ips).

!!! note
    If your application should appear under a subfolder, `SCRIPT_NAME` typically
    starts with a single leading slash and no trailing slash.



## Server stuff

### How do I reload my application in Gunicorn?

Send `HUP` to the master process for a graceful reload:

```bash
kill -HUP masterpid
```

### How might I test a proxy configuration?

Use [Hey](https://github.com/rakyll/hey) to confirm that your proxy buffers
responses correctly for synchronous workers:

```bash
hey -n 10000 -c 100 http://127.0.0.1:5000/
```

That benchmark issues 10,000 requests with a concurrency of 100.

### How can I name processes?

Install [setproctitle](https://pypi.python.org/pypi/setproctitle) to give
Gunicorn processes meaningful names in tools such as `ps` and `top`. This helps
when running multiple Gunicorn instances. See the
[`proc_name`](reference/settings.md#proc_name) setting for details.

### Why is there no HTTP keep-alive?

The default sync workers target Nginx, which uses HTTP/1.0 for upstream
connections. If you need to serve unbuffered internet traffic directly, pick an
async worker instead.

## Worker processes

### How do I know which type of worker to use?

Read the [design guide](design.md) for guidance on worker types.

### What types of workers are available?

See the [`worker_class`](reference/settings.md#worker_class) configuration reference.

### How can I figure out the best number of worker processes?

Follow the recommendations for tuning the [`number of workers`](design.md#how-many-workers).

### How can I change the number of workers dynamically?

Send `TTIN` or `TTOU` to the master process:

```bash
kill -TTIN $masterpid  # increment workers
kill -TTOU $masterpid  # decrement workers
```

### Does Gunicorn suffer from the thundering herd problem?

Potentially, when many sleeping handlers wake simultaneously but only one takes
the request. There is ongoing work to mitigate this
([issue #792](https://github.com/benoitc/gunicorn/issues/792)). Monitor load if
you use large numbers of workers or threads.

### Why don't I see logs in the console?

Gunicorn 19.0 disabled console logging by default. Use `--log-file=-` to stream
logs to stdout. Console logging returned in 19.2.

## Kernel parameters

High-concurrency deployments may need kernel tuning. These Linux-oriented tips
apply to any network service.

### How can I increase the maximum number of file descriptors?

Raise the per-process limit (remember sockets count as files). Running `sudo
ulimit` is ineffective—switch to root, adjust the limit, then launch Gunicorn.
Consider managing limits via systemd service units or init scripts.

### How can I increase the maximum socket backlog?

Increase the queue of pending connections:

```bash
sudo sysctl -w net.core.somaxconn="2048"
```

### How can I disable the use of `sendfile()`?

Pass `--no-sendfile` or set the `SENDFILE=0` environment variable.

## Troubleshooting

### Django reports `ImproperlyConfigured`

Asynchronous workers may break `django.core.urlresolvers.reverse`. Use
`reverse_lazy` instead.

### How do I avoid blocking in `os.fchmod`?

Gunicorn's heartbeat touches temporary files. On disk-backed filesystems (for
example `/tmp` on some distributions) `os.fchmod` can block if I/O stalls or the
filesystem fills up. Mount a `tmpfs` and point `--worker-tmp-dir` to it.

Check whether `/tmp` is RAM-backed:

```bash
df /tmp
```

If not, create a new `tmpfs` mount:

```bash
sudo cp /etc/fstab /etc/fstab.orig
sudo mkdir /mem
echo 'tmpfs       /mem tmpfs defaults,size=64m,mode=1777,noatime,comment=for-gunicorn 0 0' | sudo tee -a /etc/fstab
sudo mount /mem
```

Verify the result:

```bash
df /mem
```

Then start Gunicorn with `--worker-tmp-dir /mem`.

### Why are workers silently killed?

If a worker vanishes without logs, check for `SIGKILL`. Reverse proxies may show
`502` responses while Gunicorn logs only new worker startups (for example,
`[INFO] Booting worker`). A common culprit is the OOM killer in cgroups-limited
environments.

Inspect kernel logs:

```bash
dmesg | grep gunicorn
```

If you see messages similar to `Memory cgroup out of memory ... Killed process
(gunicorn.md)`, raise memory limits or adjust OOM behaviour.

---

### Content Install (`docs/content/install.md`)

# Installation

!!! note
    Gunicorn requires **Python 3.12 or newer**.

## Quick Install

=== "pip"

    ```bash
    pip install gunicorn
    ```

=== "pipx"

    ```bash
    pipx install gunicorn
    ```

=== "Docker"

    ```bash
    docker pull ghcr.io/benoitc/gunicorn:latest
    docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app
    ```

    See the [Docker guide](guides/docker.md) for production configurations.

=== "System Packages"

    **Debian/Ubuntu:**
    ```bash
    sudo apt-get update
    sudo apt-get install gunicorn
    ```

    **Fedora:**
    ```bash
    sudo dnf install python3-gunicorn
    ```

    **Arch Linux:**
    ```bash
    sudo pacman -S gunicorn
    ```

    !!! warning
        System packages may lag behind the latest release. For production,
        prefer pip installation in a virtual environment.

## Virtual Environment (Recommended)

Always install Gunicorn inside a virtual environment to isolate dependencies:

```bash
# Create virtual environment
python -m venv venv

# Activate it
source venv/bin/activate  # Linux/macOS
# or: venv\Scripts\activate  # Windows

# Install gunicorn
pip install gunicorn
```

## From Source

Install the latest development version from GitHub:

```bash
pip install git+https://github.com/benoitc/gunicorn.git
```

Upgrade to the latest commit:

```bash
pip install -U git+https://github.com/benoitc/gunicorn.git
```

## Extra Packages

Gunicorn provides optional extras for additional worker types and features.
Install them with pip's bracket syntax:

```bash
pip install gunicorn[gevent,setproctitle]
```

### Worker Types

| Extra | Description |
|-------|-------------|
| `gunicorn[gevent]` | Gevent-based greenlet workers |
| `gunicorn[gthread]` | Threaded workers |
| `gunicorn[tornado]` | Tornado-based workers (not recommended) |

See the [design docs](design.md) for guidance on choosing worker types.

### Utilities

| Extra | Description |
|-------|-------------|
| `gunicorn[setproctitle]` | Set process name in `ps`/`top` output |

!!! tip
    If running multiple Gunicorn instances, use `setproctitle` with the
    [`proc_name`](reference/settings.md#proc_name) setting to distinguish them.

## Async Workers

For applications using async I/O patterns, install the appropriate greenlet
library:

=== "Gevent"

    ```bash
    pip install gunicorn[gevent]
    ```

    Run with:
    ```bash
    gunicorn app:app --worker-class gevent
    ```

=== "ASGI (asyncio)"

    No extra installation required:

    ```bash
    gunicorn app:app --worker-class asgi
    ```

    For better performance, install uvloop:
    ```bash
    pip install uvloop
    gunicorn app:app --worker-class asgi --asgi-loop uvloop
    ```

!!! note
    Greenlet-based workers require the Python development headers. On Ubuntu:
    `sudo apt-get install python3-dev`

## Verify Installation

Check the installed version:

```bash
gunicorn --version
```

Test with a simple application:

```bash
echo 'def app(e, s): s("200 OK", []); return [b"OK"]' > test_app.py
gunicorn test_app:app
# Visit http://127.0.0.1:8000
```

## Next Steps

- [Quickstart](quickstart.md) - Get running in 5 minutes
- [Run](run.md) - CLI usage and framework integration
- [Configure](configure.md) - Configuration options

---

### Content Instrumentation (`docs/content/instrumentation.md`)

<span id="instrumentation"></span>
# Instrumentation

!!! info "Added in 19.1"
    Gunicorn exposes optional instrumentation for the arbiter and workers using the
    statsD protocol over UDP. The `gunicorn.instrument.statsd` module turns
    Gunicorn into a statsD client.



UDP keeps Gunicorn isolated from slow statsD consumers, so metrics collection
does not impact request handling.

Tell Gunicorn where the statsD server is located:

```bash
gunicorn --statsd-host=localhost:8125 --statsd-prefix=service.app ...
```

The `Statsd` logger subclasses `gunicorn.glogging.Logger` and tracks:

- `gunicorn.requests` &mdash; request rate per second
- `gunicorn.request.duration` &mdash; request duration histogram (milliseconds.md)
- `gunicorn.workers` &mdash; number of workers managed by the arbiter (gauge.md)
- `gunicorn.log.critical` &mdash; rate of critical log messages
- `gunicorn.log.error` &mdash; rate of error log messages
- `gunicorn.log.warning` &mdash; rate of warning log messages
- `gunicorn.log.exception` &mdash; rate of exceptional log messages

See the [`statsd_host`](reference/settings.md#statsd_host) setting for additional options.

[statsD](https://github.com/etsy/statsd)

---

### Content News (`docs/content/news.md`)

<span id="news"></span>
# Changelog

## 26.2.2 - 2026-09-06

### Bug Fixes

- **ASGI worker closed HTTP/1.1 connections without saying so**: when the
  worker would not reuse a connection (the client sent `Connection: close`,
  keepalive is disabled, the worker is shutting down, or the response trips
  `max_requests`) it closed the socket without a `Connection: close` header, so
  a client treated the response as persistent and reused a socket the server
  had already closed. The header is now sent whenever the connection will close
  ([#3726](https://github.com/benoitc/gunicorn/pull/3726)).

## 26.2.1 - 2026-09-05

### Bug Fixes

- **ASGI worker dropped a pipelined request on keepalive**: a second request
  sent on a kept-alive connection while the worker was finishing the first was
  left in the parser buffer, then discarded when the parser was reset between
  requests, so the connection hung until the client timed out. The buffered
  request is now recovered and served. Most visible with clients that pipeline
  or with frameworks that respond with `Transfer-Encoding: chunked`.

- **ASGI worker returned 502 for the uWSGI protocol**: with
  `--worker-class asgi --protocol uwsgi` behind nginx `uwsgi_pass`, every
  request failed with `Invalid uWSGI header: incomplete header`. The worker
  set up the HTTP/1 parser instead of a uWSGI reader, so inbound bytes never
  reached the uWSGI handler. The connection now starts a reader for the uWSGI
  protocol, builds the request body for the app, and sets `raw_path`.

- **Chunked framing lines were read without a bound**: a chunk-size line or
  trailer section that never terminates was reread in full on every socket read
  with no size limit, so one request could keep a worker busy without ever
  reaching the application. These lines are now bounded by the existing request
  limits and scanned incrementally, and a malformed chunked body answers
  `400 Bad Request` with one log line instead of a traceback.

- **HTTP/2 follow-up to the review**: a bad request now resets its own
  stream instead of the connection; the request line and field limits and
  the method token rule apply to HTTP/2 requests; forbidden trailers are
  dropped; `Expect: 100-continue` is answered; `RST_STREAM` floods close the
  connection with `ENHANCE_YOUR_CALM`; outbound frames respect the peer's
  `SETTINGS_MAX_FRAME_SIZE`; a malformed `HTTP2-Settings` header refuses the
  h2c upgrade before any `101` goes out and an oversized upgrade body is not
  upgraded; `http2_initial_window_size` and `http2_max_concurrent_streams` are
  bounded at startup, `http2_max_header_list_size = 0` means unlimited as
  documented, `h3` is no longer accepted in `http_protocols`, and `h2` without
  the h2 package is a configuration error. On `gthread` and `gevent` the
  h2c-upgraded stream is cleaned up like any other, requests queued behind a
  body read are served before the `max_requests` `GOAWAY`, which names the
  last stream served, peer protocol errors are logged at debug, and responses
  carry `Server` and `Date`. On the ASGI worker `send()` raises an `OSError`
  once the peer is gone (ASGI 2.4), trailers announced on
  `http.response.start` are sent after the body, no-body statuses drop their
  framing headers, the body wait is bounded by `timeout`, write backpressure
  reaches `writer.drain()`, and an idle connection closes after `keepalive`.
  h2spec runs against every HTTP/2 worker in the docker suites.

- **HTTP/2 request bodies were buffered in full before dispatch**: DATA
  frames were kept in two buffers, copied twice more when the request was
  built, and flow control credit went back to the peer as each frame arrived,
  so a client that never sent END_STREAM could grow a worker without the
  application being called. A request is now dispatched on its headers and
  `wsgi.input` (or ASGI `receive()`) pulls the body from the stream as it
  arrives, returning window credit only for what the application has read.
  A peer can have no more than the receive window in flight per stream. A
  body the application leaves unread is cut off with `RST_STREAM(NO_ERROR)`
  once the response is sent. On the ASGI worker, streams on a connection are
  served concurrently. Only listeners with `h2` in `http_protocols` were
  affected.
- **HTTP/2 review fixes**: a review of the HTTP/2 support found these, all
  fixed on every h2-capable worker unless noted. A graceful `GOAWAY` from the
  peer is honoured wherever it lands in a read: established streams finish,
  later ones are refused, and no private h2 state is touched. Waiting for
  send credit is bounded by `timeout` instead of a fixed five seconds; a
  peer that stops reading gets `RST_STREAM(CANCEL)` and the application is
  stopped rather than told the response succeeded. An application error
  after the response headers went out resets the stream instead of sending a
  second `HEADERS` block, which corrupted the HPACK table for every later
  response. An empty final body chunk now ends the stream. On `gthread` and
  `gevent`, frames read while waiting for credit are handled in order, an
  idle connection is closed after `keepalive` and a body that stalls for
  `timeout` is cancelled, so a peer can no longer pin a thread, and streams
  the peer resets before they are served are dropped so `HEADERS+RST_STREAM`
  floods cannot grow the worker. On the ASGI worker, `receive()` after the
  body blocks until the peer goes away instead of spinning the event loop, a
  reader paused under backpressure is resumed, and in-flight streams get the
  disconnect grace period before they are cancelled.

- Worker timeout used to dump a 500 response onto a body that had already
  started, because `sys.exit()` from the abort handler is a `BaseException`.
  Once headers are out we just close the connection
  ([#3410](https://github.com/benoitc/gunicorn/issues/3410)).

## 26.2.0 - 2026-08-24

### New Features

- **Cleartext HTTP/2 (h2c)**: `http2_cleartext` accepts `prior-knowledge`,
  `upgrade`, `both` or `off` (the default). With `prior-knowledge`, a connection
  opening with the HTTP/2 preface is served as HTTP/2, which is what a
  TLS-terminating proxy in front of gunicorn needs to avoid dropping to HTTP/1.1
  upstream. `upgrade` honours an HTTP/1.1 `Upgrade: h2c` request. Both work on
  the gthread, gevent and ASGI workers. Only peers in `forwarded_allow_ips` are
  considered; anything else from such a peer is refused with 400 rather than
  silently downgraded. Each mechanism is enabled separately, so turning one on
  does not turn the other on
  ([#3489](https://github.com/benoitc/gunicorn/issues/3489),
  [#3663](https://github.com/benoitc/gunicorn/pull/3663),
  [#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Security

- **HTTP/2 bypassed the header policy**: `HTTP2Request` built its headers
  straight from the stream, so nothing the HTTP/1 path enforces applied over
  HTTP/2: the underscore and `header_map` policy, duplicate `Host` and
  `Content-Type`, control characters in values, and the `forwarded_allow_ips`
  trust gate. An untrusted client could set `SCRIPT_NAME` and forge `HTTP_*`
  entries in the WSGI environ, and decide `wsgi.url_scheme` through `:scheme`.
  The policy now lives on a mixin both request classes share, and the scheme is
  derived from the transport
  ([#3705](https://github.com/benoitc/gunicorn/pull/3705)).

### Bug Fixes

- **WSGI HTTP/2 responses were buffered whole**: both WSGI workers collected the
  entire body in memory before sending anything. They write through an
  `HTTP2Response` now, so the body leaves as it is produced
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709)).

- **No-body responses carried a body over HTTP/2**: HEAD, 204 and 304 sent
  application body bytes on all three workers, while the HTTP/1 path had always
  dropped them per RFC 9110. They no longer do
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709),
  [#3710](https://github.com/benoitc/gunicorn/pull/3710)).

- **Events lost during flow-control waits**: while blocked waiting for a
  WINDOW_UPDATE, both HTTP/2 connections read from the socket and discarded
  every event that was not a stream reset or connection termination, losing
  requests and body data outright. They are queued for the main loop now
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709),
  [#3710](https://github.com/benoitc/gunicorn/pull/3710)).

- **`sendfile()` on HTTP/2**: it was guarded by `cfg.is_ssl`, which covered
  HTTP/2 only for as long as HTTP/2 implied TLS. It is refused on HTTP/2
  responses directly, so cleartext cannot bypass HTTP/2 framing
  ([#3709](https://github.com/benoitc/gunicorn/pull/3709)).

- **Request bodies dropped on `Upgrade` requests**: on the ASGI worker with the
  fast parser, any request carrying an `Upgrade` header reached the application
  with an empty body, whatever the header's value and with HTTP/2 switched off.
  The parser treated the header as meaning the rest of the connection was no
  longer HTTP/1, so it never read the body it had just been told the length of.
  Fixed in `gunicorn_h1c` 0.6.9, which the requirement below now pins
  ([#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Changes

- **Fast HTTP Parser**: require `gunicorn_h1c >= 0.6.9`, which adds
  `remaining()` for recovering bytes pipelined behind a completed message, and
  stops an `Upgrade` header from suppressing body parsing
  ([#3711](https://github.com/benoitc/gunicorn/pull/3711)).

### Documentation

- Describe `http_parser` in its own terms rather than as an ASGI-only setting;
  it applies to the WSGI workers too. Thanks to
  [@methane](https://github.com/methane)
  ([#3704](https://github.com/benoitc/gunicorn/pull/3704)).

- Correct the default control socket path in the `gunicorn.c` guide. Thanks to
  [@cormier](https://github.com/cormier)
  ([#3703](https://github.com/benoitc/gunicorn/pull/3703)).

- State the supported Python version as 3.10+ in the README, matching
  `requires-python`. Thanks to [@Rotzbua](https://github.com/Rotzbua)
  ([#3712](https://github.com/benoitc/gunicorn/pull/3712)).

- Point CONTRIBUTING at the mkdocs settings reference instead of the retired
  Sphinx path. Thanks to [@melbinjp](https://github.com/melbinjp)
  ([#3690](https://github.com/benoitc/gunicorn/pull/3690)).

- Fix the sponsor logo path on the sponsor page
  ([#3700](https://github.com/benoitc/gunicorn/pull/3700)).

## 26.1.0 - 2026-08-18

### New Features

- **Glob patterns in `reload_extra_files`**: entries containing `*`, `?` or `[`
  are treated as patterns, so `ui/*/config.json` watches every view's config
  without listing them one by one. Patterns are re-expanded on every reload
  check rather than once at startup, so a file created later starts being
  watched without restarting gunicorn, and `**` recurses. A pattern matching
  nothing warns instead of failing, since with live expansion it may match later
  ([#1643](https://github.com/benoitc/gunicorn/issues/1643),
  [#3662](https://github.com/benoitc/gunicorn/pull/3662)).

### Security

- **Dependency floors raised past known advisories**: every declared floor was
  checked against the advisory database. `tornado`, `h2`, `setuptools` and
  `pymdown-extensions` permitted vulnerable versions and now require the first
  clean release; `pytest` and `httpx` were unpinned and now carry floors. The
  `tornado` example pinned `tornado<6`, which was both the source of several
  advisories and older than the `>=6.5.0` the tornado worker needs, so the
  example could not run as pinned.

### Bug Fixes

- **SIGHUP did not reload the logger configuration**: `Arbiter.reload()`
  re-read the configuration file but kept using the logger built at startup,
  calling only `reopen_files()` on its existing handlers. Changes to
  `logconfig`, `logconfig_dict`, `logconfig_json` and `loglevel` were ignored
  until a full restart, which in containers meant replacing the pod. The
  existing logger now re-runs its setup on reload, so new handlers, formats
  and levels take effect while the process identity and its listeners are
  preserved, and re-running the setup no longer stacks duplicate syslog
  handlers. An invalid log configuration on reload is not fatal either: the
  error is reported on stderr, the previous working configuration is restored
  and the master keeps running with it
  ([#3353](https://github.com/benoitc/gunicorn/issues/3353)).

- **Truncated chunked bodies accepted**: RFC 9112 section 7.1.2 ends a chunked
  body with `0 CRLF CRLF`, the second CRLF being the mandatory empty trailer
  section. `ChunkedReader.parse_chunk_size()` swallowed the `NoMoreData` raised
  while scanning for it, so a body cut short right after the last chunk line was
  treated as complete instead of rejected. It now raises
  `ChunkMissingTerminator`
  ([#3382](https://github.com/benoitc/gunicorn/issues/3382),
  [#3685](https://github.com/benoitc/gunicorn/pull/3685)).

- **`--spew` crashed on dynamically generated code**: the trace hook indexed the
  2-tuple returned by `inspect.getsourcelines()` by line number rather than
  indexing the list of lines, so a frame with no `__file__` raised
  `AttributeError: 'int' object has no attribute 'rstrip'` on line 1 and
  `IndexError` beyond it. The tuple is now unpacked and offset by the source's
  starting line ([#3344](https://github.com/benoitc/gunicorn/issues/3344),
  [#3495](https://github.com/benoitc/gunicorn/pull/3495)).

- **Duplicate `Host` and `Content-Type` headers accepted**: RFC 9110 section 5.3
  allows only one of each, and a repeat cannot be merged into a list, so the
  message means different things to gunicorn and to anything downstream. Both
  are now rejected with `InvalidHeader`. The check lives in the policy hook
  shared by both parsers, so the pure-Python and fast parsers agree. Duplicate
  `Content-Length` was already rejected and is unchanged
  ([#3366](https://github.com/benoitc/gunicorn/issues/3366),
  [#3548](https://github.com/benoitc/gunicorn/pull/3548)).

- **Non-worker children reported as failed workers**: `reap_workers()` reaps
  every child through `waitpid(-1)`, including processes the kernel reparented
  onto gunicorn when it runs as PID 1 in a container, but it logged the exit
  status before checking whether the pid was ever a worker. An unrelated process
  produced `Worker (pid:N) exited with code M` and triggered alerts. More
  seriously, such a process exiting with code 3 or 4 raised `HaltServer` and shut
  the server down. Ownership is now established first: the dirty arbiter is
  reported as itself, unknown children are reaped silently at debug level, and
  only real workers can halt the server
  ([#3220](https://github.com/benoitc/gunicorn/issues/3220),
  [#3566](https://github.com/benoitc/gunicorn/pull/3566)).

- **Dirty arbiter exits were invisible on SIGCHLD**: `handle_chld()` called
  `reap_workers()` first, whose `waitpid(-1)` claimed the dirty arbiter before
  `reap_dirty_arbiter()` could identify it, so the latter always hit `ECHILD` and
  its reporting never ran. The dirty arbiter is now reaped first, and
  `reap_workers()` recognises it if it exits mid-loop.

- **Dirty arbiter returned stale responses after a worker timeout**: when a
  request reached `dirty_timeout` the arbiter answered the client with a timeout
  error but kept the worker connection open. The worker's late response was then
  the first message waiting on that socket, so the next request routed to the
  same worker received the previous request's result, and every request after it
  stayed one response behind. The connection is now closed on timeout, so the
  late answer is discarded with it
  ([#3626](https://github.com/benoitc/gunicorn/pull/3626)).

- **ASGI connection count leaked on server-initiated close**: `nr_conns` was
  only decremented in `connection_lost()`, behind a guard keyed on the same
  flag `_close_transport()` sets first. Every close the server started (a
  `Connection: close` response, a keepalive timeout, an error abort) leaked one
  count, so `ASGIWorker._shutdown()` ran the full `graceful_timeout` and warned
  about connections that were already gone. The guard now uses its own flag, so
  the decrement and the rest of the cleanup run exactly once whichever side
  closes first ([#3661](https://github.com/benoitc/gunicorn/issues/3661)).

- **Inotify reloader on cwd-relative extra files**: `reload_extra_files` entries
  with no directory part (for example `.env`) produced an empty dirname, and
  watching it raised `InotifyError` with `ENOENT`. The current directory is now
  watched as `.` ([#3377](https://github.com/benoitc/gunicorn/issues/3377),
  [#3667](https://github.com/benoitc/gunicorn/pull/3667)).

- **StatsD zero-valued metrics**: gauges, counters, histograms and timers
  reporting `0` were silently dropped because the value was tested for
  truthiness. Only `None` is skipped now
  ([#3676](https://github.com/benoitc/gunicorn/pull/3676)).

- **Spurious no-body warning from `sendfile()`**: a HEAD, 204 or 304 response
  served through `sendfile()` warned about dropped body bytes even when the
  file was empty and nothing was dropped. It now warns only when there are
  bytes to drop, matching `write()`
  ([#3684](https://github.com/benoitc/gunicorn/pull/3684)).

- **Bare `except` in the gevent websocket example**: narrowed to
  `except Exception` ([#3683](https://github.com/benoitc/gunicorn/pull/3683)).

- **ASGI `receive()` cancellation**: Let `asyncio.CancelledError` propagate
  from `BodyReceiver` instead of swallowing it and returning
  `http.disconnect`. Frameworks that cancel their disconnect listener after
  the response completes (Django) no longer see the cancel masked, so
  `request_finished` fires and `close_old_connections()` runs. Fixes idle
  database connections leaking since 25.1.0
  ([#3627](https://github.com/benoitc/gunicorn/issues/3627),
  [#3654](https://github.com/benoitc/gunicorn/pull/3654)).

- **Control socket leak on SIGHUP reload**: The control thread is now marked
  ready once its loop and server are live, and the stop paths wait on that
  readiness before scheduling shutdown. Reloads no longer leak one thread and
  its selector fd plus unix socket per worker, which eventually raised
  "too many open files"
  ([#3648](https://github.com/benoitc/gunicorn/issues/3648)).

- **WSGI body framing on HEAD/1xx/204/304**: Mirror the ASGI strip-and-warn
  behavior on the WSGI path. `Content-Length` is stripped on 1xx/204 per
  RFC 9110 section 6.4.2, body bytes are dropped for no-body responses in
  both `write()` and `sendfile()`, and a single warning is logged per request
  ([#3413](https://github.com/benoitc/gunicorn/issues/3413)).

### Refactoring

- Pass log arguments to the logger instead of pre-formatting the worker
  termination message in `Arbiter.reap_workers()`
  ([#3678](https://github.com/benoitc/gunicorn/pull/3678)).

### Changes

- **`packaging` is no longer a runtime dependency**: it was only ever imported by
  the gevent worker, to compare gevent's version. It moved to the `gevent` and
  `testing` extras, so a plain `pip install gunicorn` pulls in nothing
  ([#3643](https://github.com/benoitc/gunicorn/pull/3643)).

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.6`, which rejects duplicate
  `Host` and `Content-Type` headers in the C parser itself. Gunicorn already
  refuses them on both the WSGI and ASGI paths, so this changes nothing that is
  reachable; it moves the rejection to where the bytes are read and lets the
  ASGI corpus exercise those cases against the fast parser directly.

## 26.0.0 - 2026-05-05

### Breaking Changes

- **Eventlet worker removed**: The `eventlet` worker class has been dropped.
  Migrate to `gevent`, `gthread`, or `tornado`.

### New Features

- **ASGI Framework Compatibility Suite**: New end-to-end compatibility test
  harness covering Starlette, FastAPI, Litestar, Quart, Sanic, and BlackSheep.
  Current grid passes 438/444 tests (98%).

- **ASGI Test Suite Expansion**: 134 additional ASGI unit tests covering
  protocol semantics, lifespan, websockets, and chunked framing.

### Security

- **HTTP/1.1 Request-Target Validation** (RFC 9112 sections 3.2.3, 3.2.4):
  - Reject `authority-form` request-target outside `CONNECT`
  - Reject `asterisk-form` request-target outside `OPTIONS`
  - Reject `relative-reference` request-targets

- **Header Field Hardening** (RFC 9110):
  - Reject control characters in header field-value (section 5.5)
  - Reject forbidden trailer field-names (section 6.5.1)
  - Reject `Content-Length` list form (RFC 9112 section 6.3)

- **Request Smuggling Hardening**:
  - Tighten keepalive gate and scope `finish_body` byte cap
  - Keep `_body_receiver` alive across the keepalive smuggling gate so
    pipelined requests cannot re-enter a closed body
  - Address parser/protocol findings from a six-point WSGI/ASGI audit

- **PROXY Protocol (ASGI)**: Enforce `proxy_allow_ips` and tighten v1/v2
  parsing in the ASGI callback parser.

- **Connection Draining**: Drain the connection on close per RFC 9112
  section 9.6 to prevent reset-on-close truncation.

### Bug Fixes

- **Body Framing on HEAD/204/304**:
  - Keep `Content-Length` on HEAD and 304 responses
    ([#3621](https://github.com/benoitc/gunicorn/pull/3621))
  - Drop body framing on HEAD/204/304 even when the framework set it
  - Warn once when an ASGI app emits a body for a no-body response

- **HTTP/2 ASGI**:
  - Fix `_handle_stream_ended` to set `_body_complete` in the async HTTP/2
    handler so request bodies finalize correctly on stream end
  - Add `InvalidChunkExtension` mapping and fast-parser support in ASGI
    tests ([#3565](https://github.com/benoitc/gunicorn/pull/3565))

- **HTTP/1.1 100-Continue**: Stop adding `Transfer-Encoding: chunked` to
  100-Continue interim responses.

- **WebSocket Close Handshake** (RFC 6455):
  - Comply with the close handshake state machine
  - Close the transport after the close handshake completes
  - Fix binary send when the `text` key is `None`

- **Early Hints**: Validate headers in the `early_hints` callback to match
  `process_headers`; pass only the header name to `InvalidHeader`
  ([#3588](https://github.com/benoitc/gunicorn/pull/3588)).

- **ASGI Framework Fixes**:
  - Fix ASGI disconnect handling for Django-style apps
  - Fix Litestar request handling (use raw ASGI receive for body/headers)
  - Fix Litestar HTTP endpoints for compatibility tests
  - Fix Quart headers endpoint to normalize keys to lowercase
  - Fix Quart WebSocket close test app (missing `accept()`)
  - Fix duplicate `Transfer-Encoding` header for BlackSheep streaming

### Refactoring

- Split `BodyReceiver._closed` into separate transport and body-wait flags
  for clearer keepalive/EOF semantics.

### Changes

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.5`. Drop the last
  `python_only` test markers; the C extension is now used wherever
  available (CPython only; PyPy continues to use the Python parser).

- **Test Dependencies**: Add `h2` and `uvloop` to the `testing` extra;
  remove `eventlet`.

- **Docker Build**: Bump GitHub Actions `docker/setup-qemu-action`,
  `docker/setup-buildx-action`, `docker/login-action`,
  `docker/build-push-action`, and `docker/metadata-action` to current
  major versions.

---

## 25.3.0 - 2026-03-26

### Bug Fixes

- **HTTP/2 ASGI Body Duplication**: Fix request body being received twice in HTTP/2
  ASGI requests, causing JSON parsing errors with "Extra data" messages
  ([#3558](https://github.com/benoitc/gunicorn/issues/3558))

- **ASGI Chunked EOF Handling**: Add `finish()` method to callback parser to handle
  chunked encoding edge case where connection closes before final CRLF after zero-chunk

- **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string
  instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561))

- **Chunked Encoding**: Reject chunk extensions containing bare CR bytes per RFC 9112
  ([#3556](https://github.com/benoitc/gunicorn/discussions/3556))

- **Request Line Limit**: Fix `--limit-request-line 0` to mean unlimited as documented,
  instead of using default maximum. Works with both Python and fast C parser.
  ([#3563](https://github.com/benoitc/gunicorn/issues/3563))

### Security

- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112:
  - Reject duplicate Content-Length headers
  - Reject requests with both Content-Length and Transfer-Encoding
  - Reject chunked transfer encoding in HTTP/1.0
  - Reject stacked chunked encoding
  - Validate Transfer-Encoding values
  - Strict chunk size validation

### Changes

- **Fast HTTP Parser**: Update to gunicorn_h1c >= 0.6.3 for `asgi_headers` property
  and `InvalidChunkExtension` validation for bare CR rejection

- **ASGI PROXY Protocol**: Add PROXY protocol v1/v2 support to callback parser

- **Docker Images**: Update to Python 3.14

---

## 25.2.0 - 2026-03-24

### New Features

- **Fast HTTP Parser (gunicorn_h1c 0.6.0)**: Integrate new exception types and limit
  parameters from gunicorn_h1c 0.6.0 for both WSGI and ASGI workers
  - Requires gunicorn_h1c >= 0.6.0 for `http_parser='fast'`
  - Falls back to Python parser in `auto` mode if version not met
  - Proper HTTP status codes for limit errors (414, 431)

### Bug Fixes

- **uWSGI Async Workers**: Fix `InvalidUWSGIHeader: incomplete header` error
  when using gevent or gthread workers with uwsgi protocol behind nginx.
  ([#3552](https://github.com/benoitc/gunicorn/issues/3552),
  [PR #3554](https://github.com/benoitc/gunicorn/pull/3554))

- **FileWrapper Iterator Protocol**: Add `__iter__` and `__next__` methods to
  `FileWrapper` for full PEP 3333 compliance. Previously only supported old-style
  `__getitem__` iteration which broke code explicitly using `iter()` or `next()`.
  ([#3396](https://github.com/benoitc/gunicorn/issues/3396),
  [PR #3550](https://github.com/benoitc/gunicorn/pull/3550))

### Performance

- **ASGI HTTP Parser Optimizations**: Improve ASGI worker HTTP parsing performance
  - Callback-based parsing with direct `bytearray` buffer operations
  - Use `bytearray.find()` directly instead of converting to bytes first
  - Use index-based iteration for header parsing instead of `list.pop(0)` (O(1) vs O(n))

---

## 25.1.0 - 2026-02-13

### New Features

- **Control Interface (gunicornc)**: Add interactive control interface for managing
  running Gunicorn instances, similar to birdc for BIRD routing daemon
  ([PR #3505](https://github.com/benoitc/gunicorn/pull/3505))
  - Unix socket-based communication with JSON protocol
  - Interactive mode with readline support and command history
  - Commands: `show all/workers/dirty/config/stats/listeners`
  - Worker management: `worker add/remove/kill`, `dirty add/remove`
  - Server control: `reload`, `reopen`, `shutdown`
  - New settings: `--control-socket`, `--control-socket-mode`, `--no-control-socket`
  - New CLI tool: `gunicornc` for connecting to control socket
  - See [Control Interface Guide](guides/gunicornc.md) for details

- **Dirty Stash**: Add global shared state between workers via `dirty.stash`
  ([PR #3503](https://github.com/benoitc/gunicorn/pull/3503))
  - In-memory key-value store accessible by all workers
  - Supports get, set, delete, clear, keys, and has operations
  - Useful for sharing state like feature flags, rate limits, or cached data

- **Dirty Binary Protocol**: Implement efficient binary protocol for dirty arbiter IPC
  using TLV (Type-Length-Value) encoding
  ([PR #3500](https://github.com/benoitc/gunicorn/pull/3500))
  - More efficient than JSON for binary data
  - Supports all Python types: str, bytes, int, float, bool, None, list, dict
  - Better performance for large payloads

- **Dirty TTIN/TTOU Signals**: Add dynamic worker scaling for dirty arbiters
  ([PR #3504](https://github.com/benoitc/gunicorn/pull/3504))
  - Send SIGTTIN to increase dirty workers
  - Send SIGTTOU to decrease dirty workers
  - Respects minimum worker constraints from app configurations

### Changes

- **ASGI Worker**: Promoted from beta to stable
- **Dirty Arbiters**: Now marked as beta feature

### Documentation

- Fix Markdown formatting in /configure documentation

---

## 25.0.3 - 2026-02-07

### Bug Fixes

- Fix RuntimeError when StopIteration is raised inside ASGI response body
  coroutine (PEP 479 compliance)

- Fix deprecation warning for passing maxsplit as positional argument in
  `re.split()` (Python 3.13+)

---

## 25.0.2 - 2026-02-06

### Bug Fixes

- Fix ASGI concurrent request failures through nginx proxy by normalizing
  sockaddr tuples to handle both 2-tuple (IPv4) and 4-tuple (IPv6) formats
  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))

- Fix graceful disconnect handling for ASGI worker to properly handle
  client disconnects without raising exceptions
  ([PR #3485](https://github.com/benoitc/gunicorn/pull/3485))

- Fix lazy import of dirty module for gevent compatibility - prevents
  import errors when concurrent.futures is imported before gevent monkey-patching
  ([PR #3483](https://github.com/benoitc/gunicorn/pull/3483))

### Changes

- Refactor: Extract `_normalize_sockaddr` utility function for consistent
  socket address handling across workers

- Add license headers to all Python source files

- Update copyright year to 2026 in LICENSE and NOTICE files

---

## 25.0.1 - 2026-02-02

### Bug Fixes

- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for
  HTTP/1.1 responses without Content-Length header. Without chunked encoding,
  clients wait for connection close to determine end-of-response.

### Changes

- Update celery_alternative example to use FastAPI with native ASGI worker and
  uvloop for async task execution

### Testing

- Add ASGI compliance test suite with Docker-based integration tests covering HTTP,
  WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI),
  HTTP/2, and concurrency scenarios

---

## 25.0.0 - 2026-02-01

### New Features

- **Dirty Arbiters**: Separate process pool for executing long-running, blocking
  operations (AI model loading, heavy computation) without blocking HTTP workers
  ([PR #3460](https://github.com/benoitc/gunicorn/pull/3460))
  - Inspired by Erlang's dirty schedulers
  - Asyncio-based with Unix socket IPC
  - Stateful workers that persist loaded resources
  - New settings: `--dirty-app`, `--dirty-workers`, `--dirty-timeout`,
    `--dirty-threads`, `--dirty-graceful-timeout`
  - Lifecycle hooks: `on_dirty_starting`, `dirty_post_fork`,
    `dirty_worker_init`, `dirty_worker_exit`

- **Per-App Worker Allocation for Dirty Arbiters**: Control how many dirty workers
  load each app for memory optimization with heavy models
  ([PR #3473](https://github.com/benoitc/gunicorn/pull/3473))
  - Set `workers` class attribute on DirtyApp (e.g., `workers = 2`)
  - Or use config format `module:class:N` (e.g., `myapp:HeavyModel:2`)
  - Requests automatically routed to workers with the target app
  - New exception `DirtyNoWorkersAvailableError` for graceful error handling
  - Example: 8 workers × 10GB model = 80GB → with `workers=2`: 20GB (75% savings)

- **HTTP/2 Support (Beta)**: Native HTTP/2 (RFC 7540) support for improved performance
  with modern clients ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))
  - Multiplexed streams over a single connection
  - Header compression (HPACK)
  - Flow control and stream prioritization
  - Works with gthread, gevent, and ASGI workers
  - New settings: `--http-protocols`, `--http2-max-concurrent-streams`,
    `--http2-initial-window-size`, `--http2-max-frame-size`, `--http2-max-header-list-size`
  - Requires SSL/TLS and h2 library: `pip install gunicorn[http2]`
  - See [HTTP/2 Guide](guides/http2.md) for details
  - New example: `examples/http2_gevent/` with Docker and tests

- **HTTP 103 Early Hints**: Support for RFC 8297 Early Hints to enable browsers to
  preload resources before the final response
  ([PR #3468](https://github.com/benoitc/gunicorn/pull/3468))
  - WSGI: `environ['wsgi.early_hints'](headers)` callback
  - ASGI: `http.response.informational` message type
  - Works with both HTTP/1.1 and HTTP/2

- **uWSGI Protocol for ASGI Worker**: The ASGI worker now supports receiving requests
  via the uWSGI binary protocol from nginx
  ([PR #3467](https://github.com/benoitc/gunicorn/pull/3467))

### Bug Fixes

- Fix HTTP/2 ALPN negotiation for the gevent worker when
  `do_handshake_on_connect` is False (the default). The TLS handshake is now
  explicitly performed before checking `selected_alpn_protocol()`.

- Fix setproctitle initialization with systemd socket activation
  ([#3465](https://github.com/benoitc/gunicorn/issues/3465))

- Fix `Expect: 100-continue` handling: ignore the header for HTTP/1.0 requests
  since 100-continue is only valid for HTTP/1.1+
  ([PR #3463](https://github.com/benoitc/gunicorn/pull/3463))

- Fix missing `_expected_100_continue` attribute in UWSGIRequest

- Disable setproctitle on macOS to prevent segfaults during process title updates

- Publish full exception traceback when the application fails to load
  ([#3462](https://github.com/benoitc/gunicorn/issues/3462))

- Fix ASGI: quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM

### Removals

- **Eventlet Worker**: The `eventlet` worker has been removed.  Eventlet itself
  is [no longer actively maintained](https://eventlet.readthedocs.io/en/latest/asyncio/migration.html);
  the worker was deprecated in 25.x and is now gone.  Migrate to `gevent`,
  `gthread`, or one of the ASGI workers.

### Changes

- Remove obsolete Makefile targets
  ([PR #3471](https://github.com/benoitc/gunicorn/pull/3471))

---

## 24.1.1 - 2026-01-24

### Bug Fixes

- Fix `forwarded_allow_ips` and `proxy_allow_ips` to remain as strings for backward
  compatibility with external tools like uvicorn. Network validation now uses strict
  mode to detect invalid CIDR notation (e.g., `192.168.1.1/24` where host bits are set)
  ([#3458](https://github.com/benoitc/gunicorn/issues/3458),
  [PR #3459](https://github.com/benoitc/gunicorn/pull/3459))

---

## 24.1.0 - 2026-01-23

### New Features

- **Official Docker Image**: Gunicorn now publishes official Docker images to GitHub
  Container Registry at `ghcr.io/benoitc/gunicorn`
  - Based on Python 3.12 slim image
  - Uses recommended worker formula (2 × CPU + 1)
  - Configurable via environment variables

- **PROXY Protocol v2 Support**: Extended PROXY protocol implementation to support
  the binary v2 format in addition to the existing text-based v1 format
  - New `--proxy-protocol` modes: `off`, `v1`, `v2`, `auto`
  - Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources

- **CIDR Network Support**: `--forwarded-allow-ips` and `--proxy-allow-from` now
  accept CIDR notation (e.g., `192.168.0.0/16`) for specifying trusted networks

- **Socket Backlog Metric**: New `gunicorn.socket.backlog` gauge metric reports
  the current socket backlog size on Linux systems

- **InotifyReloader Enhancement**: The inotify-based reloader now watches newly
  imported modules, not just those loaded at startup

### Bug Fixes

- Fix signal handling regression where SIGCLD alias caused errors on Linux
- Fix socket blocking mode on keepalive connections with async workers
- Handle `SSLWantReadError` in `finish_body()` to prevent worker hangs
- Log SIGTERM as info level instead of warning
- Print exception details to stderr when worker fails to boot
- Fix `unreader.unread()` to prepend data to buffer instead of appending
- Prevent `RecursionError` when pickling Config objects

---

## 24.0.0 - 2026-01-23

### New Features

- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python
  frameworks like FastAPI, Starlette, and Quart without external dependencies
  - HTTP/1.1 with keepalive connections
  - WebSocket support
  - Lifespan protocol for startup/shutdown hooks
  - Optional uvloop for improved performance

- **uWSGI Binary Protocol**: Support for receiving requests from nginx via
  `uwsgi_pass` directive

- **Documentation Migration**: Migrated to MkDocs with Material theme

### Security

- **gevent**: Require gevent >= 24.10.1 (CVE-2023-41419, CVE-2024-3219)
- **tornado**: Require tornado >= 6.5.0 (CVE-2025-47287)

---

## 23.0.0 - 2024-08-10

- minor docs fixes ([PR #3217](https://github.com/benoitc/gunicorn/pull/3217), [PR #3089](https://github.com/benoitc/gunicorn/pull/3089), [PR #3167](https://github.com/benoitc/gunicorn/pull/3167))
- worker_class parameter accepts a class ([PR #3079](https://github.com/benoitc/gunicorn/pull/3079))
- fix deadlock if request terminated during chunked parsing ([PR #2688](https://github.com/benoitc/gunicorn/pull/2688))
- permit receiving Transfer-Encodings: compress, deflate, gzip ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))
- permit Transfer-Encoding headers specifying multiple encodings. note: no parameters, still ([PR #3261](https://github.com/benoitc/gunicorn/pull/3261))
- sdist generation now explicitly excludes sphinx build folder ([PR #3257](https://github.com/benoitc/gunicorn/pull/3257))
- decode bytes-typed status (as can be passed by gevent) as utf-8 instead of raising `TypeError` ([PR #2336](https://github.com/benoitc/gunicorn/pull/2336))
- raise correct Exception when encounting invalid chunked requests ([PR #3258](https://github.com/benoitc/gunicorn/pull/3258))
- the SCRIPT_NAME and PATH_INFO headers, when received from allowed forwarders, are no longer restricted for containing an underscore ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))
- include IPv6 loopback address ``[::1]`` in default for [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) and [proxy-allow-ips](reference/settings.md#proxy_allow_ips) ([PR #3192](https://github.com/benoitc/gunicorn/pull/3192))

!!! note
    - The SCRIPT_NAME change mitigates a regression that appeared first in the 22.0.0 release
    - Review your [forwarded-allow-ips](reference/settings.md#forwarded_allow_ips) setting if you are still not seeing the SCRIPT_NAME transmitted
    - Review your [forwarder-headers](reference/settings.md#forwarder_headers) setting if you are missing headers after upgrading from a version prior to 22.0.0


### Breaking changes

- refuse requests where the uri field is empty ([PR #3255](https://github.com/benoitc/gunicorn/pull/3255))
- refuse requests with invalid CR/LR/NUL in heade field values ([PR #3253](https://github.com/benoitc/gunicorn/pull/3253))
- remove temporary ``--tolerate-dangerous-framing`` switch from 22.0 ([PR #3260](https://github.com/benoitc/gunicorn/pull/3260))
- If any of the breaking changes affect you, be aware that now refused requests can post a security problem, especially so in setups involving request pipe-lining and/or proxies.

## 22.0.0 - 2024-04-17

- use `utime` to notify workers liveness
- migrate setup to pyproject.toml
- fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors)
- parsing additional requests is no longer attempted past unsupported request framing
- on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits)
- requests conflicting configured or passed SCRIPT_NAME now produce a verbose error
- Trailer fields are no longer inspected for headers indicating secure scheme
- support Python 3.12

### Breaking changes

- minimum version is Python 3.7
- the limitations on valid characters in the HTTP method have been bounded to Internet Standards
- requests specifying unsupported transfer coding (order.md) are refused by default (rare.md)
- HTTP methods are no longer casefolded by default (IANA method registry contains none affected)
- HTTP methods containing the number sign (#) are no longer accepted by default (rare.md)
- HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported)
- HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted
- HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software
- HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits)
- requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling)
- empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies)


### Security

- fix CVE-2024-1135

## History

- [2026](2026-news.md)
- [2024](2024-news.md)
- [2023](2023-news.md)
- [2021](2021-news.md)
- [2020](2020-news.md)
- [2019](2019-news.md)
- [2018](2018-news.md)
- [2017](2017-news.md)
- [2016](2016-news.md)
- [2015](2015-news.md)
- [2014](2014-news.md)
- [2013](2013-news.md)
- [2012](2012-news.md)
- [2011](2011-news.md)
- [2010](2010-news.md)

---

### Content Roadmap (`docs/content/roadmap.md`)

# Roadmap

Where gunicorn is going, and what is being worked on now. This is priority
order, not a schedule: nothing here carries a date, and items move when
circumstances or funding change.

Gunicorn is maintained in unpaid time. That is the constraint behind every
ordering decision on this page. See [Support Gunicorn](sponsor.md) for how that
changes.

## Now

The current focus, roughly in order.

**Release stabilization.** Working through open reports and pull requests,
reviewing contributions, and getting fixes into releases. This is the bulk of
the work and the part that goes undone when there is no time.

**Security.** Responding to reported vulnerabilities, and keeping the HTTP
parsers aligned with RFC 9110 and RFC 9112 on request framing, header syntax
and smuggling defenses.

**HTTP/2.** Taking what shipped as beta toward something dependable, including
`h2c` support across the HTTP/2-capable workers.

**ASGI.** Tracking the spec as FastAPI, Starlette, Quart and the rest move,
with the compatibility suite as the check on that.

**Python compatibility.** Keeping up with new Python releases.

**Documentation.** Keeping the guides and the settings reference current.

## Next

Larger pieces of work that are planned, or begun but not landed.

**Serving AI workloads** (resource-aware, lifecycle-aware workers). Dirty
arbiters already give heavy applications such as ML models their own process
pools, with per-app worker allocation and streaming responses. The next step is
to make those workers resource-aware and lifecycle-aware without turning
Gunicorn into an inference engine.

Applications will be able to request operator-defined resource slots, initially
targeting GPU-backed workloads, while Gunicorn manages allocation, readiness,
backpressure and graceful worker replacement. Resource slots are virtual
allocation tokens: the application and its inference engine remain responsible
for the physical device, model memory, batching and parallelism.

The goal is for Gunicorn to complement engines such as vLLM, SGLang, llama.cpp
or PyTorch by managing their application lifecycle cleanly, rather than
replacing them.

**HTTP/3.** QUIC support, alongside finishing HTTP/2. The protocol is already
reserved in the configuration and does nothing yet.

**FastCGI.** A FastCGI responder alongside the existing
[uWSGI protocol](uwsgi.md) support, so gunicorn can sit behind nginx or Apache
over FCGI instead of HTTP proxying. A working branch exists in
[#3466](https://github.com/benoitc/gunicorn/pull/3466); it needs finishing and
review.

None of these are settled. If you have a use case, a design opinion or a
constraint any of them would break, say so in
[Ideas](https://github.com/benoitc/gunicorn/discussions/categories/ideas) —
that is what shapes the order and the shape of the work.

## Not planned

**Windows support.** Gunicorn is a pre-fork UNIX server. The process model is
the product, and it does not port.

## Influencing this

The ordering is not fixed. Things that move it:

- A clear bug report with a reproduction moves the fix up.
- A pull request with tests moves the feature up further.
- [Sponsorship](sponsor.md) converts evenings into days, which is what
  determines how much of the "Next" section happens at all.

If something important to you is missing here, open an
[Idea](https://github.com/benoitc/gunicorn/discussions/categories/ideas), or
come [ask in chat](https://web.libera.chat/?channels=%23gunicorn).

---

### Content Run (`docs/content/run.md`)

# Running Gunicorn

You can run Gunicorn directly from the command line or integrate it with
popular frameworks like Django, Pyramid, or TurboGears. For deployment
patterns see the [deployment guide](deploy.md).

## Commands

After installation you have access to the `gunicorn` executable.

<span id="gunicorn-cmd"></span>
### `gunicorn`

Basic usage:

```bash
gunicorn [OPTIONS] [WSGI_APP]
```

`WSGI_APP` follows the pattern `MODULE_NAME:VARIABLE_NAME`. The module can be a
full dotted path. The variable refers to a WSGI callable defined in that
module.

!!! info "Changed in 20.1.0"
    `WSGI_APP` can be omitted when defined in a [configuration file](configure.md).



Example test application:

```python
def app(environ, start_response):
    """Simplest possible application object"""
    data = b"Hello, World!\n"
    status = "200 OK"
    response_headers = [
        ("Content-type", "text/plain"),
        ("Content-Length", str(len(data)))
    ]
    start_response(status, response_headers)
    return iter([data])
```

Run it with:

```bash
gunicorn --workers=2 test:app
```

You can also expose a factory function that returns the application:

```python
def create_app():
    app = FrameworkApp()
    ...
    return app
```

```bash
gunicorn --workers=2 'test:create_app()'
```

Passing positional and keyword arguments is supported but prefer
configuration files or environment variables for anything beyond quick tests.

#### Commonly used arguments

- `-c CONFIG`, `--config CONFIG` &mdash; configuration file (`PATH`, `file:PATH`, or
  `python:MODULE_NAME`).
- `-b BIND`, `--bind BIND` &mdash; socket to bind (host, host:port, `fd://FD`,
  or `unix:PATH`).
- `-w WORKERS`, `--workers WORKERS` &mdash; number of worker processes, typically
  two to four per CPU core. See the [FAQ](faq.md) for tuning tips.
- `-k WORKERCLASS`, `--worker-class WORKERCLASS` &mdash; worker type (`sync`,
  `gevent`, `tornado`, `gthread`). Read the
  [settings entry](reference/settings.md#worker_class) before switching classes.
- `-n APP_NAME`, `--name APP_NAME` &mdash; set the process name (requires
  [`setproctitle`](https://pypi.python.org/pypi/setproctitle)).

You can pass any setting via the environment variable
`GUNICORN_CMD_ARGS`. See the [configuration guide](configure.md) and
[settings reference](reference/settings.md) for details.

## Integration

Gunicorn integrates cleanly with Django and Paste Deploy applications.

### Django

Gunicorn looks for a WSGI callable named `application`. A typical invocation is:

```bash
gunicorn myproject.wsgi
```

!!! note
    Ensure your project is on `PYTHONPATH`. The easiest way is to run this command
    from the directory containing `manage.py`.



Set environment variables with `--env` and add your project to `PYTHONPATH`
if needed:

```bash
gunicorn --env DJANGO_SETTINGS_MODULE=myproject.settings myproject.wsgi
```

See [`raw_env`](reference/settings.md#raw_env) and [`pythonpath`](reference/settings.md#pythonpath) for
more options.

### Paste Deployment

Frameworks such as Pyramid and TurboGears often rely on Paste Deployment
configuration. You can use Gunicorn in two ways.

#### As a Paste server runner

Let your framework command (for example `pserve` or `gearbox`) load Gunicorn by
configuring it as the server:

```ini
[server:main]
use = egg:gunicorn#main
host = 127.0.0.1
port = 8080
workers = 3
```

This approach is quick to set up but Gunicorn cannot control how the
application loads. Options like [`reload`](reference/settings.md#reload) will be ignored and
hot upgrades are unavailable. Features such as daemon mode may conflict with
what your framework already provides. Prefer running those features through the
framework (for example `pserve --reload`). Advanced configuration is still
possible by pointing the `config` key at a Gunicorn configuration file.

#### Using Gunicorn's Paste support

Use the [`paste`](reference/settings.md#paste) option to load a Paste configuration directly
with the Gunicorn CLI. This unlocks Gunicorn's reloader and hot code upgrades,
while still letting Paste define the application object.

```bash
gunicorn --paste development.ini -b :8080 --chdir /path/to/project
```

Select a different application section by appending the name:

```bash
gunicorn --paste development.ini#admin -b :8080 --chdir /path/to/project
```

In both modes Gunicorn will honor any Paste `loggers` configuration unless you
override it with Gunicorn-specific [logging settings](reference/settings.md#logging).

---

### Content Signals (`docs/content/signals.md`)

<span id="signals"></span>
# Signal Handling

A quick reference to the signals handled by Gunicorn. This includes the signals
used internally to coordinate with worker processes.

## Master process

- `QUIT`, `INT` &mdash; quick shutdown.
- `TERM` &mdash; graceful shutdown; waits for workers to finish requests up to
  [`graceful_timeout`](reference/settings.md#graceful_timeout).
- `HUP` &mdash; reload configuration, spawn new workers, and gracefully stop old
  ones. If the app is not preloaded (see [`preload_app`](reference/settings.md#preload_app))
  the application code is reloaded too.
- `TTIN` &mdash; increase worker count by one.
- `TTOU` &mdash; decrease worker count by one.
- `USR1` &mdash; reopen log files.
- `USR2` &mdash; perform a binary upgrade. Send `TERM` to the old master afterwards
  to stop it. This also reloads preloaded applications (see
  [binary upgrades](#binary-upgrade)).
- `WINCH` &mdash; gracefully stop workers when Gunicorn runs as a daemon.

## Worker process

Workers rarely need direct signalling—if the master stays alive it will respawn
workers automatically.

- `QUIT`, `INT` &mdash; quick shutdown.
- `TERM` &mdash; graceful shutdown.
- `USR1` &mdash; reopen log files.

## Reload the configuration

Use `HUP` to reload Gunicorn on the fly:

```text
2013-06-29 06:26:55 [20682] [INFO] Handling signal: hup
2013-06-29 06:26:55 [20682] [INFO] Hang up: Master
2013-06-29 06:26:55 [20703] [INFO] Booting worker with pid: 20703
2013-06-29 06:26:55 [20702] [INFO] Booting worker with pid: 20702
2013-06-29 06:26:55 [20688] [INFO] Worker exiting (pid: 20688)
2013-06-29 06:26:55 [20687] [INFO] Worker exiting (pid: 20687)
2013-06-29 06:26:55 [20689] [INFO] Worker exiting (pid: 20689)
2013-06-29 06:26:55 [20704] [INFO] Booting worker with pid: 20704
```

Gunicorn reloads its settings, starts new workers, and gracefully shuts down the
previous ones. If the app is not preloaded it reloads the application module as
well.

<span id="binary-upgrade"></span>
## Upgrading to a new binary on the fly

!!! info "Changed in 19.6.0"
    PID files now follow the pattern `<name>.pid.2` instead of `<name>.pid.oldbin`.



You can replace the Gunicorn binary without downtime. Incoming requests remain
served and preloaded applications reload.

1. Replace the old binary and send `USR2` to the master. Gunicorn starts a new
   master whose PID file ends with `.2` and spawns new workers.

   ```text
   PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND
   20844 benoitc   20   0 54808  11m 3352 S   0.0  0.1   0:00.36 gunicorn: master [test:app]
   20849 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]
   20850 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]
   20851 benoitc   20   0 54808 9.9m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]
   20854 benoitc   20   0 55748  12m 3348 S   0.0  0.2   0:00.35 gunicorn: master [test:app]
   20859 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]
   20860 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.00 gunicorn: worker [test:app]
   20861 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]
   ```

2. Send `WINCH` to the old master to gracefully stop its workers.

You can still roll back while the old master keeps its listen sockets:

1. Send `HUP` to the old master to restart its workers without reloading the
   config file.
2. Send `TERM` to the new master to shut down its workers gracefully.
3. Send `QUIT` to the new master to force it to exit.

If the new workers linger, send `KILL` after the new master quits.

To complete the upgrade, send `TERM` to the old master so only the new server
continues running:

```text
PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND
20854 benoitc   20   0 55748  12m 3348 S   0.0  0.2   0:00.45 gunicorn: master [test:app]
20859 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]
20860 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.02 gunicorn: worker [test:app]
20861 benoitc   20   0 55748  11m 1500 S   0.0  0.1   0:00.01 gunicorn: worker [test:app]
```

---

### Content Sponsor (`docs/content/sponsor.md`)

# Support Gunicorn

Gunicorn has been serving Python web applications since 2010. It's downloaded
millions of times per month and runs in production at companies of all sizes.

No company pays for gunicorn's maintenance. The work happens in unpaid time, and
that is what sets how fast it moves. Sponsorship changes that directly: it buys
days that would otherwise go to client work.

## What your sponsorship funds

Where the time goes, in priority order:

- **Release stabilization**: working through the open reports and pull requests,
  reviewing contributions and getting the fixes people are waiting on into a
  release. This is the bulk of it, and it is the part that goes undone when
  there is no time
- **Security**: responding to reported vulnerabilities
- **HTTP/2**: h2c support across the HTTP/2-capable workers, and hardening what
  shipped as beta
- **ASGI**: tracking the spec as FastAPI, Starlette and Quart move
- **Python compatibility**: keeping up with new Python releases
- **Documentation**: keeping the guides and the settings reference current

Two larger pieces of new work, neither started:

- **Serving AI workloads**: dirty arbiters already give a model its own process
  pool, with per-app worker allocation and streaming responses. The work ahead
  is what a model server needs on top of that: batching requests so a single
  forward pass serves many of them, backpressure when the queue outgrows the
  workers, and pinning workers to specific GPUs
- **HTTP/3**: QUIC support, alongside finishing HTTP/2. The protocol is already
  reserved in the configuration and does nothing yet

This is what gets worked on, in the order it gets worked on. It is not a
schedule, and nothing here carries a date.

Sustained sponsorship funds full-time months rather than evenings. When it does,
what it paid for goes in the [release notes](news.md), so you can see what your
money bought.

## How to support

### Donate

<p>
  <a href="https://github.com/sponsors/benoitc"><img src="https://img.shields.io/badge/GitHub_Sponsors-❤-ea4aaa?style=for-the-badge&logo=github" alt="GitHub Sponsors"></a>
  <a href="https://checkout.revolut.com/pay/74cf6fe1-358e-4880-b800-3f0936ce94be"><img src="https://img.shields.io/badge/Revolut-Donate-191c20?style=for-the-badge" alt="Revolut"></a>
</p>

- **[GitHub Sponsors](https://github.com/sponsors/benoitc)**: monthly or one-time
- **[Revolut](https://checkout.revolut.com/pay/74cf6fe1-358e-4880-b800-3f0936ce94be)**: direct donations, individuals and companies

### Corporate sponsorship

If gunicorn is part of your infrastructure:

- **Recurring sponsorship** through [GitHub Sponsors](https://github.com/sponsors/benoitc)
- **Sponsored support contracts** for priority bug fixes and feature requests
- **Logo placement** on this site and in the README

## Sponsors

<a href="https://enki-multimedia.eu" target="_blank" rel="noopener">
  <img src="../assets/enki-multimedia.svg" alt="Enki Multimedia" style="height: 50px;" />
</a>

---

### Content Uwsgi (`docs/content/uwsgi.md`)

# uWSGI Protocol

Gunicorn supports the uWSGI binary protocol, allowing it to receive requests from
nginx using the `uwsgi_pass` directive. This provides efficient communication
between nginx and Gunicorn without HTTP overhead.

Both **WSGI** and **ASGI** workers support the uWSGI protocol.

!!! note
    This is the **uWSGI binary protocol**, not the uWSGI server. Gunicorn
    implements the protocol to receive requests from nginx, similar to how
    the uWSGI server would.

## Quick Start

Enable uWSGI protocol support:

```bash
# WSGI application
gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000

# ASGI application
gunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000
```

Configure nginx to forward requests:

```nginx
upstream gunicorn {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }
}
```

## Why Use uWSGI Protocol?

The uWSGI binary protocol offers several advantages over HTTP proxying:

- **Lower overhead** - Binary format is more compact than HTTP headers
- **Better integration** - nginx's native uwsgi module is highly optimized
- **Simpler configuration** - No need to reconstruct HTTP headers

## Configuration

### Protocol Setting

Switch from HTTP to uWSGI protocol:

```bash
gunicorn myapp:app --protocol uwsgi
```

Or in a configuration file:

```python
# gunicorn.conf.py
protocol = "uwsgi"
```

### Allowed IPs

By default, uWSGI protocol requests are only accepted from localhost
(`127.0.0.1` and `::1`). This prevents unauthorized hosts from sending
requests directly to Gunicorn.

To allow additional IPs:

```bash
gunicorn myapp:app --protocol uwsgi --uwsgi-allow-from 10.0.0.1,10.0.0.2
```

To allow all IPs (not recommended for production):

```bash
gunicorn myapp:app --protocol uwsgi --uwsgi-allow-from '*'
```

!!! warning
    Only allow IPs from trusted sources. The uWSGI protocol does not provide
    authentication, so anyone who can connect can send requests.

!!! note
    UNIX socket connections are always allowed regardless of this setting.

### Using UNIX Sockets

For better performance and security, use UNIX sockets instead of TCP:

```bash
gunicorn myapp:app --protocol uwsgi --bind unix:/run/gunicorn.sock
```

Nginx configuration:

```nginx
upstream gunicorn {
    server unix:/run/gunicorn.sock;
}

server {
    listen 80;

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }
}
```

## Nginx Configuration

### Basic Setup

Create or verify the `uwsgi_params` file exists (usually at `/etc/nginx/uwsgi_params`):

```nginx
uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;
```

### With SSL Termination

When nginx handles SSL and forwards to Gunicorn:

```nginx
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
        uwsgi_param HTTPS on;
    }
}
```

### Load Balancing

Distribute requests across multiple Gunicorn instances:

```nginx
upstream gunicorn {
    least_conn;
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

server {
    listen 80;

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }
}
```

### Static Files

Serve static files directly from nginx:

```nginx
server {
    listen 80;

    location /static/ {
        alias /path/to/static/;
    }

    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }
}
```

## Protocol Details

The uWSGI protocol uses a compact binary format:

| Bytes | Field | Description |
|-------|-------|-------------|
| 0 | modifier1 | Packet type (0 = WSGI request) |
| 1-2 | datasize | Size of vars block (little-endian) |
| 3 | modifier2 | Additional flags (usually 0) |

After the header, the vars block contains CGI-style key-value pairs:

```
[2-byte key_size][key][2-byte val_size][value]...
```

Standard CGI variables like `REQUEST_METHOD`, `PATH_INFO`, and `QUERY_STRING`
are extracted from this block to construct the WSGI environ.

## Combining with HTTP

You can run Gunicorn with both HTTP and uWSGI protocol support by running
separate instances:

```bash
# HTTP for direct access
gunicorn myapp:app --bind 127.0.0.1:8080

# uWSGI for nginx
gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000
```

## Troubleshooting

### ForbiddenUWSGIRequest Error

If you see "Forbidden uWSGI request from IP", the connecting IP is not in
the allowed list. Either:

1. Add the IP to `--uwsgi-allow-from`
2. Use UNIX sockets instead
3. Ensure nginx is connecting from an allowed IP

### Invalid uWSGI Header

This usually means:

1. HTTP traffic is being sent to a uWSGI endpoint
2. The packet is malformed or truncated
3. Network issues caused data corruption

Verify that nginx is using `uwsgi_pass` (not `proxy_pass`) and that the
`uwsgi_params` file is being included.

### Headers Missing

If certain headers aren't reaching your application, verify they're included
in `uwsgi_params`. Custom headers should be passed as:

```nginx
uwsgi_param HTTP_X_CUSTOM_HEADER $http_x_custom_header;
```

## See Also

- [Settings Reference](reference/settings.md#protocol) - Protocol and uWSGI settings
- [Deploy](deploy.md) - General deployment guidance
- [Design](design.md) - Worker architecture overview

---

### Content Guides Docker (`docs/content/guides/docker.md`)

# Docker Deployment

Running Gunicorn in Docker containers is the most common deployment pattern
for modern Python applications. This guide covers best practices for
containerizing Gunicorn applications.

## Official Docker Image

Gunicorn provides an official Docker image on GitHub Container Registry:

```bash
docker pull ghcr.io/benoitc/gunicorn:latest
```

### Quick Start

Mount your application directory and run:

```bash
docker run -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app
```

### Running in Background

Use `-d` (detached mode) to run the container in the background:

```bash
# Start in background
docker run -d --name myapp -p 8000:8000 -v $(pwd):/app ghcr.io/benoitc/gunicorn app:app

# View logs
docker logs myapp

# Follow logs in real-time
docker logs -f myapp

# Stop the container
docker stop myapp

# Start it again
docker start myapp

# Remove the container
docker rm myapp
```

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `GUNICORN_BIND` | Full bind address | `0.0.0.0:8000` |
| `GUNICORN_HOST` | Bind host | `0.0.0.0` |
| `GUNICORN_PORT` | Bind port | `8000` |
| `GUNICORN_WORKERS` | Number of workers | `(2 * CPU) + 1` |
| `GUNICORN_ARGS` | Additional arguments | (none) |

### With Configuration

```bash
docker run -p 9000:9000 -v $(pwd):/app \
  -e GUNICORN_PORT=9000 \
  -e GUNICORN_WORKERS=4 \
  -e GUNICORN_ARGS="--timeout 120 --access-logfile -" \
  ghcr.io/benoitc/gunicorn app:app
```

### As Base Image (Recommended for Production)

```dockerfile
FROM ghcr.io/benoitc/gunicorn:24.1.0

# Install app dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY --chown=gunicorn:gunicorn . .

CMD ["myapp:app", "--workers", "4"]
```

### With Docker Compose

```yaml
services:
  web:
    image: ghcr.io/benoitc/gunicorn:latest
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app
    command: ["myapp:app", "--workers", "4"]
```

### Available Tags

- `ghcr.io/benoitc/gunicorn:latest` - Latest release
- `ghcr.io/benoitc/gunicorn:24.1.0` - Specific version
- `ghcr.io/benoitc/gunicorn:24.1` - Minor version
- `ghcr.io/benoitc/gunicorn:24` - Major version

## Building Your Own Image

For more control, build a custom image using the patterns below.

## Basic Dockerfile

```dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .

# Run gunicorn
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
```

Build and run:

```bash
docker build -t myapp .
docker run -p 8000:8000 myapp
```

## Production Configuration

### Environment Variables

Use environment variables for configuration:

```dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Configuration via environment
ENV GUNICORN_WORKERS=4
ENV GUNICORN_BIND=0.0.0.0:8000

CMD gunicorn app:app \
    --workers ${GUNICORN_WORKERS} \
    --bind ${GUNICORN_BIND}
```

Or use `GUNICORN_CMD_ARGS`:

```dockerfile
ENV GUNICORN_CMD_ARGS="--workers=4 --bind=0.0.0.0:8000"
CMD ["gunicorn", "app:app"]
```

### Worker Count

In containers, determine workers based on available CPU:

```python
# gunicorn.conf.py
import multiprocessing

workers = multiprocessing.cpu_count() * 2 + 1
bind = "0.0.0.0:8000"
```

Or let Kubernetes/Docker limit CPU and calculate accordingly:

```bash
# At runtime
gunicorn app:app --workers $(( 2 * $(nproc) + 1 ))
```

### Non-Root User

Run as a non-root user for security:

```dockerfile
FROM python:3.12-slim

# Create non-root user
RUN useradd --create-home appuser
WORKDIR /home/appuser/app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

USER appuser

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
```

### Health Checks

Add a health check endpoint and Docker health check:

```dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1
```

## Multi-Stage Build

Reduce image size with multi-stage builds:

```dockerfile
# Build stage
FROM python:3.12 AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

# Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Copy wheels and install
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels

COPY . .

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
```

## Docker Compose

Example `docker-compose.yml`:

```yaml
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://db:5432/myapp
    depends_on:
      - db
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 512M

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_PASSWORD=secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - web

volumes:
  postgres_data:
```

## Kubernetes Deployment

Example Kubernetes deployment:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8000
        env:
        - name: GUNICORN_WORKERS
          value: "4"
        resources:
          limits:
            cpu: "1"
            memory: "512Mi"
          requests:
            cpu: "500m"
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8000
```

## Graceful Shutdown

Gunicorn handles `SIGTERM` gracefully by default. Configure the timeout:

```dockerfile
CMD ["gunicorn", "app:app", \
     "--bind", "0.0.0.0:8000", \
     "--graceful-timeout", "30", \
     "--timeout", "120"]
```

Match Docker's stop timeout:

```yaml
# docker-compose.yml
services:
  web:
    stop_grace_period: 30s
```

## Logging

Log to stdout/stderr for Docker log collection:

```python
# gunicorn.conf.py
accesslog = "-"
errorlog = "-"
loglevel = "info"
```

Use JSON logging for log aggregation:

```python
# gunicorn.conf.py
import json
import datetime

class JsonFormatter:
    def format(self, record):
        return json.dumps({
            "timestamp": datetime.datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
        })

logconfig_dict = {
    "version": 1,
    "formatters": {
        "json": {"()": JsonFormatter}
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "json",
            "stream": "ext://sys.stdout"
        }
    },
    "root": {
        "handlers": ["console"],
        "level": "INFO"
    }
}
```

## Troubleshooting

### Worker Timeout

If workers are killed with `[CRITICAL] WORKER TIMEOUT`, increase the timeout:

```bash
gunicorn app:app --timeout 120
```

Or investigate slow requests in your application.

### Out of Memory

If containers are OOM-killed:

1. Reduce worker count
2. Use `--max-requests` to restart workers periodically
3. Increase container memory limits

```bash
gunicorn app:app --workers 2 --max-requests 1000 --max-requests-jitter 100
```

### Connection Reset

If you see connection resets, ensure:

1. Load balancer health checks match your `/health` endpoint
2. Graceful timeout is sufficient for in-flight requests
3. Keepalive settings match between Gunicorn and upstream proxy

## See Also

- [Deploy](../deploy.md) - General deployment patterns
- [Settings](../reference/settings.md) - All configuration options

---

### Content Guides Gunicornc (`docs/content/guides/gunicornc.md`)

---
title: Control Interface (gunicornc)
menu:
    guides:
        weight: 15
---

# Control Interface (gunicornc)

Gunicorn provides a control interface similar to [birdc](https://bird.network.cz/?get_doc&v=20&f=bird-3.html) for the BIRD routing daemon. This allows you to inspect and manage a running Gunicorn instance via a Unix socket.

## Overview

The control interface consists of two parts:

1. **Control Socket Server** - Runs in the arbiter process, accepts commands via Unix socket
2. **gunicornc CLI** - Interactive client that connects to the control socket

## Quick Start

### Start Gunicorn with Control Socket

By default, Gunicorn creates the control socket at `$XDG_RUNTIME_DIR/gunicorn.ctl`. If `XDG_RUNTIME_DIR` is unset or does not point to an existing directory, it falls back to `$HOME/.gunicorn/gunicorn.ctl`:

```bash
gunicorn -w 4 myapp:app
```

Or specify a custom path:

```bash
gunicorn --control-socket /tmp/myapp.ctl -w 4 myapp:app
```

### Connect with gunicornc

```bash
# Connect to default socket (./gunicorn.ctl)
gunicornc

# Connect to custom socket
gunicornc -s /tmp/myapp.ctl

# Run a single command
gunicornc -c "show workers"

# Output as JSON (for scripting)
gunicornc -c "show stats" -j
```

## Interactive Mode

When run without the `-c` flag, gunicornc enters interactive mode with readline support:

```
$ gunicornc
Connected to gunicorn.ctl
Type 'help' for available commands, 'quit' to exit.

gunicorn> show workers
PID        AGE    BOOTED   LAST_BEAT
----------------------------------------
12345      1      yes      0.2s ago
12346      2      yes      0.1s ago
12347      3      yes      0.3s ago

Total: 3 workers

gunicorn> worker add 2
{
  "added": 2,
  "previous": 3,
  "total": 5
}

gunicorn> quit
```

## Commands

### Show Commands

| Command | Description |
|---------|-------------|
| `show all` | Overview of all processes (arbiter, web workers, dirty workers) |
| `show workers` | List HTTP workers with status |
| `show dirty` | List dirty workers and apps |
| `show config` | Show current effective configuration |
| `show stats` | Show server statistics |
| `show listeners` | Show bound sockets |
| `help` | Show available commands |

### Worker Management

| Command | Description |
|---------|-------------|
| `worker add [N]` | Spawn N workers (default 1) |
| `worker remove [N]` | Remove N workers (default 1) |
| `worker kill <PID>` | Gracefully terminate specific worker |

### Dirty Worker Management

| Command | Description |
|---------|-------------|
| `dirty add [N]` | Spawn N dirty workers (default 1) |
| `dirty remove [N]` | Remove N dirty workers (default 1) |

!!! note "Per-App Worker Limits"
    When using `dirty add`, workers only load apps that haven't reached their
    worker limits. If all apps are at their limits, no new workers will be spawned.
    The response will include a `reason` field explaining this.

### Server Control

| Command | Description |
|---------|-------------|
| `reload` | Graceful reload (equivalent to SIGHUP) |
| `reopen` | Reopen log files (equivalent to SIGUSR1) |
| `shutdown [graceful\|quick]` | Shutdown server (SIGTERM or SIGINT) |

## Example Session

```
$ gunicornc
Connected to gunicorn.ctl
Type 'help' for available commands, 'quit' to exit.

gunicorn> show all
ARBITER (master)
  PID: 12345

WEB WORKERS (4)
  PID        AGE    BOOTED   LAST_BEAT
  --------------------------------------
  12346      1      yes      0.05s ago
  12347      2      yes      0.04s ago
  12348      3      yes      0.03s ago
  12349      4      yes      0.02s ago

DIRTY ARBITER
  PID: 12350

DIRTY WORKERS (2)
  PID        AGE    APPS
  --------------------------------------------------
  12351      1      MLModel
                    ImageProcessor
  12352      2      MLModel

gunicorn> show stats
Uptime:           2h 15m 30s
PID:              12345
Workers current:  4
Workers target:   4
Workers spawned:  6
Workers killed:   2
Reloads:          1

gunicorn> worker add
{
  "added": 1,
  "previous": 4,
  "total": 5
}

gunicorn> dirty add 1
{
  "success": true,
  "operation": "add",
  "requested": 1,
  "spawned": 1,
  "total_workers": 3,
  "target_workers": 3
}

gunicorn> quit
```

## Configuration

### Settings

| Setting | CLI Flag | Default | Description |
|---------|----------|---------|-------------|
| `control_socket` | `--control-socket` | `gunicorn.ctl` | Unix socket path |
| `control_socket_mode` | `--control-socket-mode` | `0o600` | Socket file permissions |
| `control_socket_disable` | `--no-control-socket` | `False` | Disable control socket |

### Example Configuration

```python
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4

# Control socket settings
control_socket = "/var/run/gunicorn/myapp.ctl"
control_socket_mode = 0o660  # Allow group access
```

## Scripting

Use the `-j` flag for JSON output when scripting:

```bash
#!/bin/bash

# Get current worker count
workers=$(gunicornc -c "show stats" -j | jq -r '.workers_current')
echo "Current workers: $workers"

# Scale up if needed
if [ "$workers" -lt 8 ]; then
    gunicornc -c "worker add $((8 - workers))"
fi
```

## Security

The control socket uses filesystem permissions for access control:

- **Default mode**: `0o600` (owner only)
- **No authentication**: Relies on filesystem permissions
- **Unix socket only**: No TCP/remote access

To allow group access:

```python
control_socket_mode = 0o660
```

To disable the control socket entirely:

```bash
gunicorn --no-control-socket myapp:app
```

## Protocol

The control interface uses a JSON-based protocol with length-prefixed framing:

```
+----------------+------------------+
| Length (4B BE) |  JSON Payload    |
+----------------+------------------+
```

### Request Format

```json
{
  "id": 1,
  "command": "show workers"
}
```

### Response Format

```json
{
  "id": 1,
  "status": "ok",
  "data": { ... }
}
```

### Error Response

```json
{
  "id": 1,
  "status": "error",
  "error": "Unknown command: foo"
}
```

## Troubleshooting

### Cannot connect to socket

```
Error: Connection refused
```

- Check that Gunicorn is running
- Verify the socket path is correct
- Check socket file permissions

### Permission denied

```
Error: Permission denied
```

- Check that you have read/write access to the socket file
- The socket is created with mode `0o600` by default (owner only)

### Socket not found

```
Error: No such file or directory
```

- By default, Gunicorn creates the socket at `$XDG_RUNTIME_DIR/gunicorn.ctl` or falls back to `$HOME/.gunicorn/gunicorn.ctl`.
- Use an absolute path with `--control-socket /path/to/socket.ctl`
- Check if `--no-control-socket` was specified

---

### Content Guides Http2 (`docs/content/guides/http2.md`)

# HTTP/2 Support

!!! warning "Beta Feature"
    HTTP/2 support is a beta feature introduced in Gunicorn 25.0.0. While it has been tested,
    the API and behavior may change in future releases. Please report any issues on
    [GitHub](https://github.com/benoitc/gunicorn/issues).

Gunicorn supports HTTP/2 (RFC 7540) for improved performance with modern clients.
HTTP/2 provides multiplexed streams, header compression, and other optimizations
over HTTP/1.1.

## Quick Start

```bash
# Install gunicorn with HTTP/2 support
pip install gunicorn[http2]

# Run with HTTP/2 enabled (requires SSL)
gunicorn myapp:app \
    --worker-class gthread \
    --threads 4 \
    --certfile server.crt \
    --keyfile server.key \
    --http-protocols h2,h1
```

## Requirements

HTTP/2 support requires:

- **SSL/TLS**: HTTP/2 uses ALPN (Application-Layer Protocol Negotiation) which
  requires an encrypted connection (see [Cleartext HTTP/2](#cleartext-http2-h2c)
  for the proxy-only exception)
- **h2 library**: Install with `pip install gunicorn[http2]` or `pip install h2`
- **Compatible worker**: gthread, gevent, or ASGI workers

## Configuration

### Enable HTTP/2

Enable HTTP/2 by setting the `--http-protocols` option:

```bash
gunicorn myapp:app --http-protocols h2,h1
```

Or in a configuration file:

```python
# gunicorn.conf.py
http_protocols = "h2, h1"
```

The order matters for ALPN negotiation - protocols are tried in order of preference.

| Protocol | Description |
|----------|-------------|
| `h2`     | HTTP/2 over TLS |
| `h1`     | HTTP/1.1 (fallback) |

!!! note
    Always include `h1` as a fallback for clients that don't support HTTP/2.

### SSL/TLS Configuration

HTTP/2 requires SSL/TLS. Configure certificates:

```bash
gunicorn myapp:app \
    --certfile /path/to/server.crt \
    --keyfile /path/to/server.key \
    --http-protocols h2,h1
```

Or in a configuration file:

```python
# gunicorn.conf.py
certfile = "/path/to/server.crt"
keyfile = "/path/to/server.key"
http_protocols = "h2, h1"
```

### Cleartext HTTP/2 (h2c)

When TLS is terminated in front of Gunicorn - Cloud Run, fly.io, a
service-mesh sidecar - the proxy can speak HTTP/2 to the upstream over
plain TCP. Enable it with `--http2-cleartext`, on the `gthread`, `gevent`
or `asgi` worker:

```bash
gunicorn myapp:app -k gthread --http-protocols h2,h1 \
    --http2-cleartext prior-knowledge
```

The value selects which mechanism is accepted: `prior-knowledge`,
`upgrade`, `both`, or `off` (the default). Each mechanism is enabled
separately, so turning one on does not turn the other on.

h2c is only accepted from peers in `forwarded_allow_ips` - the same
trust list used for forwarded headers, which in this deployment shape is
exactly the TLS-terminating proxy. A trusted peer that opens a
connection with the HTTP/2 connection preface is served HTTP/2; anything
else from a trusted peer (an HTTP/1.x request, a malformed or stalled
preface) is rejected with a 400 rather than silently downgraded, so a
misconfigured proxy fails loudly instead of quietly losing HTTP/2.
Untrusted peers are served HTTP/1.1 exactly as if the flag were off.

Both mechanisms are served by the `gthread`, `gevent` and `asgi`
workers. Note that with `prior-knowledge` alone a trusted peer sending
anything else is refused with 400, while with `upgrade` or `both` an
ordinary HTTP/1 request is served normally, because that is how an
upgrade starts.

On the `gthread` and `gevent` workers, streams on an HTTP/2 connection are
handled one after another, so a slow handler holds up the other streams on
that connection; the `asgi` worker serves them concurrently. Request bodies
are pulled from the connection as the application reads them and response
bodies are streamed as the application produces them, so neither sits in
worker memory. A stream that makes no progress for `timeout` seconds is
cancelled, and a connection with nothing open is closed after `keepalive`.

As on HTTP/1, responses that cannot carry a body (HEAD, 1xx, 204, 304)
send none, and `sendfile()` does not apply to HTTP/2 responses: raw file
bytes would bypass HTTP/2 framing.

Test prior knowledge with:

```bash
curl --http2-prior-knowledge http://localhost:8000/
```

!!! warning
    h2c is cleartext. Only enable it behind a trusted, TLS-terminating
    proxy. Never expose an h2c port directly to the internet.

### HTTP/2 Settings

Fine-tune HTTP/2 behavior with these settings:

| Setting | Default | Description |
|---------|---------|-------------|
| `http2_max_concurrent_streams` | 100 | Maximum concurrent streams per connection |
| `http2_initial_window_size` | 65535 | Initial flow control window size (bytes) |
| `http2_max_frame_size` | 16384 | Maximum frame size (bytes) |
| `http2_max_header_list_size` | 65536 | Maximum header list size (bytes) |

Example configuration:

```python
# gunicorn.conf.py
http_protocols = "h2, h1"
http2_max_concurrent_streams = 200
http2_initial_window_size = 1048576  # 1MB
```

## Worker Compatibility

Not all workers support HTTP/2:

| Worker | HTTP/2 Support | Notes |
|--------|----------------|-------|
| `sync` | No | Single-threaded, cannot multiplex streams |
| `gthread` | Yes | Recommended for HTTP/2 |
| `gevent` | Yes | Requires gevent |
| `asgi` | Yes | For async frameworks |
| `tornado` | No | Tornado handles its own protocol |

If you use the sync or tornado worker with HTTP/2 enabled, Gunicorn will log a
warning and fall back to HTTP/1.1.

### Recommended: gthread Worker

For HTTP/2, the gthread worker is recommended:

```bash
gunicorn myapp:app \
    --worker-class gthread \
    --threads 4 \
    --workers 2 \
    --http-protocols h2,h1 \
    --certfile server.crt \
    --keyfile server.key
```

## HTTP 103 Early Hints

Gunicorn supports HTTP 103 Early Hints (RFC 8297), allowing servers to send
resource hints before the final response. This enables browsers to preload
CSS, JavaScript, and other assets in parallel.

### WSGI Applications

Use the `wsgi.early_hints` callback in your WSGI application:

```python
def app(environ, start_response):
    # Send early hints if available
    if 'wsgi.early_hints' in environ:
        environ['wsgi.early_hints']([
            ('Link', '</style.css>; rel=preload; as=style'),
            ('Link', '</app.js>; rel=preload; as=script'),
        ])

    # Continue with the actual response
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<html>...</html>']
```

### ASGI Applications

Use the `http.response.informational` message type:

```python
async def app(scope, receive, send):
    # Send early hints
    await send({
        "type": "http.response.informational",
        "status": 103,
        "headers": [
            (b"link", b"</style.css>; rel=preload; as=style"),
            (b"link", b"</app.js>; rel=preload; as=script"),
        ],
    })

    # Send the actual response
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [(b"content-type", b"text/html")],
    })
    await send({
        "type": "http.response.body",
        "body": b"<html>...</html>",
    })
```

!!! note
    Early hints are only sent to HTTP/1.1+ clients. HTTP/1.0 clients silently
    ignore the callback since they don't support 1xx responses.

## Stream Priority

HTTP/2 allows clients to indicate the relative priority of streams using PRIORITY frames
(RFC 7540 Section 5.3). Gunicorn tracks stream priorities and exposes them to both
WSGI and ASGI applications.

### Accessing Priority in WSGI

Priority information is available in the WSGI environ for HTTP/2 requests:

```python
def app(environ, start_response):
    # Access stream priority (HTTP/2 only)
    weight = environ.get('gunicorn.http2.priority_weight')
    depends_on = environ.get('gunicorn.http2.priority_depends_on')

    if weight is not None:
        # This is an HTTP/2 request with priority info
        # Higher weight = client considers this more important
        print(f"Request priority: weight={weight}, depends_on={depends_on}")

    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'OK']
```

| Environ Key | Range | Default | Description |
|-------------|-------|---------|-------------|
| `gunicorn.http2.priority_weight` | 1-256 | 16 | Higher weight = more resources |
| `gunicorn.http2.priority_depends_on` | Stream ID | 0 | Parent stream (0 = root) |

### Accessing Priority in ASGI

For ASGI applications, priority is available in the scope's `extensions` dict:

```python
async def app(scope, receive, send):
    if scope["type"] == "http":
        # Check for HTTP/2 priority extension
        extensions = scope.get("extensions", {})
        priority = extensions.get("http.response.priority")

        if priority:
            weight = priority["weight"]        # 1-256
            depends_on = priority["depends_on"]  # Parent stream ID
            print(f"Request priority: weight={weight}, depends_on={depends_on}")

        await send({
            "type": "http.response.start",
            "status": 200,
            "headers": [(b"content-type", b"text/plain")],
        })
        await send({
            "type": "http.response.body",
            "body": b"OK",
        })
```

| Extension Key | Field | Range | Default | Description |
|---------------|-------|-------|---------|-------------|
| `http.response.priority` | `weight` | 1-256 | 16 | Higher weight = more resources |
| `http.response.priority` | `depends_on` | Stream ID | 0 | Parent stream (0 = root) |

!!! note
    Stream priority is advisory. Applications can use it for scheduling decisions,
    but Gunicorn does not enforce priority-based request ordering. Priority
    information is only present for HTTP/2 requests.

## Response Trailers

HTTP/2 supports trailing headers (trailers) sent after the response body.
This is commonly used for gRPC status codes, checksums, and timing information.

### WSGI Applications

For WSGI applications, use the `gunicorn.http2.send_trailers` callback in the environ:

```python
def app(environ, start_response):
    # Get trailer callback (HTTP/2 only)
    send_trailers = environ.get('gunicorn.http2.send_trailers')

    # Announce trailers in response headers
    headers = [
        ('Content-Type', 'application/grpc'),
        ('Trailer', 'grpc-status, grpc-message'),
    ]
    start_response('200 OK', headers)

    # Yield response body
    yield b'response data'

    # Send trailers after body (if available)
    if send_trailers:
        send_trailers([
            ('grpc-status', '0'),
            ('grpc-message', 'OK'),
        ])
```

### ASGI Applications

For ASGI applications, use the `http.response.trailers` extension:

```python
async def app(scope, receive, send):
    # Send response with trailers flag
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [
            (b"content-type", b"application/grpc"),
            (b"trailer", b"grpc-status, grpc-message"),
        ],
    })

    # Send body
    await send({
        "type": "http.response.body",
        "body": b"response data",
        "more_body": False,
    })

    # Send trailers (HTTP/2 only)
    if "http.response.trailers" in scope.get("extensions", {}):
        await send({
            "type": "http.response.trailers",
            "headers": [
                (b"grpc-status", b"0"),
                (b"grpc-message", b"OK"),
            ],
        })
```

### Trailer Rules (RFC 7540)

- Trailers MUST NOT include pseudo-headers (`:status`, `:path`, etc.)
- Announce trailers using the `Trailer` response header
- Trailers are only available in HTTP/2 (HTTP/1.1 chunked encoding not supported)

### Common Use Cases

| Use Case | Trailer Headers |
|----------|-----------------|
| gRPC | `grpc-status`, `grpc-message` |
| Checksums | `Content-MD5`, `Digest` |
| Timing | `Server-Timing` |
| Signatures | `Signature` |

## Production Deployment

### With Nginx

Configure nginx to proxy HTTP/2 connections to Gunicorn:

```nginx
upstream gunicorn {
    server 127.0.0.1:8443;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate /path/to/server.crt;
    ssl_certificate_key /path/to/server.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Forward 103 Early Hints (requires nginx 1.29+)
    location / {
        proxy_pass https://gunicorn;
        proxy_http_version 1.1;
        proxy_ssl_verify off;

        early_hints $http2;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

!!! note
    For nginx to forward 103 Early Hints from upstream, you need nginx 1.29+
    and the [`early_hints`](https://nginx.org/en/docs/http/ngx_http_core_module.html#early_hints) directive.

### Direct TLS Termination

For simpler deployments, Gunicorn can terminate TLS directly:

```python
# gunicorn.conf.py
bind = "0.0.0.0:443"
worker_class = "gthread"
threads = 4
workers = 4

# SSL
certfile = "/etc/letsencrypt/live/example.com/fullchain.pem"
keyfile = "/etc/letsencrypt/live/example.com/privkey.pem"

# HTTP/2
http_protocols = "h2, h1"
http2_max_concurrent_streams = 100
```

### Recommended Settings

For production HTTP/2 deployments:

```python
# gunicorn.conf.py
worker_class = "gthread"
workers = 4
threads = 4
keepalive = 120  # HTTP/2 connections are long-lived

# SSL/TLS
certfile = "/path/to/server.crt"
keyfile = "/path/to/server.key"
# TLS 1.2 is already the floor of the default context; ssl_version is
# ignored. Use the ssl_context hook to tighten ciphers if you need to.

# HTTP/2
http_protocols = "h2, h1"
http2_max_concurrent_streams = 100
http2_initial_window_size = 65535
```

## Troubleshooting

### HTTP/2 not negotiated

If clients fall back to HTTP/1.1:

1. Verify SSL is configured correctly
2. Check that `h2` is in `--http-protocols`
3. Ensure the h2 library is installed: `pip install h2`
4. Verify ALPN support: `openssl s_client -alpn h2 -connect host:port`

### Worker doesn't support HTTP/2

If you see "HTTP/2 is not supported by the sync worker":

```bash
# Switch to gthread worker
gunicorn myapp:app --worker-class gthread --threads 4
```

### Connection errors with large requests

Increase flow control window sizes:

```python
http2_initial_window_size = 1048576  # 1MB
http2_max_frame_size = 32768  # 32KB
```

### Too many concurrent streams

If clients report stream limit errors:

```python
http2_max_concurrent_streams = 200  # Increase from default 100
```

## Performance Tuning

HTTP/2 performance depends on proper tuning of both Gunicorn and system settings.
This section covers different tuning profiles and their trade-offs.

### Tuning Profiles

#### Conservative (Default)

Best for: Low to moderate traffic, memory-constrained environments.

```python
# gunicorn.conf.py - Conservative profile
workers = 2
worker_class = "gthread"
threads = 4

http2_max_concurrent_streams = 100
http2_initial_window_size = 65535      # 64KB
http2_max_frame_size = 16384           # 16KB
```

| Pros | Cons |
|------|------|
| Low memory footprint | Limited throughput at high concurrency |
| Safe defaults per RFC | More round-trips for large transfers |
| Works on constrained systems | May bottleneck at ~10K req/s |

#### Balanced

Best for: Moderate traffic, general production use.

```python
# gunicorn.conf.py - Balanced profile
workers = 4
worker_class = "gthread"
threads = 4
backlog = 2048

http2_max_concurrent_streams = 128
http2_initial_window_size = 262144     # 256KB
http2_max_frame_size = 16384           # 16KB
```

| Pros | Cons |
|------|------|
| Good throughput (15K+ req/s) | More memory per connection |
| Handles traffic spikes | Requires more CPU |
| Good balance of resources | |

#### High Concurrency

Best for: High traffic APIs, microservices, load testing.

```python
# gunicorn.conf.py - High concurrency profile
workers = 4
worker_class = "gthread"
threads = 8
backlog = 2048
worker_connections = 10000

http2_max_concurrent_streams = 256
http2_initial_window_size = 1048576    # 1MB
http2_max_frame_size = 32768           # 32KB
```

| Pros | Cons |
|------|------|
| High throughput (20K+ req/s) | Higher memory usage (~4x conservative) |
| Handles 1000s of clients | Requires system tuning |
| Better large transfer performance | May overwhelm downstream services |

### Setting Trade-offs

#### `http2_max_concurrent_streams`

Controls how many simultaneous streams a client can open per connection.

| Value | Memory | Throughput | Use Case |
|-------|--------|------------|----------|
| 50-100 | Low | Moderate | APIs with small payloads |
| 128-256 | Medium | High | General web applications |
| 500+ | High | Very High | Streaming, real-time apps |

!!! warning
    Very high values (500+) can lead to resource exhaustion under attack.
    Use with rate limiting.

#### `http2_initial_window_size`

Flow control window size determines how much data can be sent before waiting for acknowledgment.

| Value | Memory | Latency | Use Case |
|-------|--------|---------|----------|
| 65535 (64KB) | Low | Higher for large transfers | Default, memory-constrained |
| 262144 (256KB) | Medium | Balanced | General use |
| 1048576 (1MB) | High | Lower for large transfers | Large file transfers, streaming |

!!! note
    Larger windows improve throughput for large responses but increase memory
    usage per stream. Calculate: `max_streams × window_size × connections`.

#### `http2_max_frame_size`

Maximum size of individual HTTP/2 frames.

| Value | Memory | Efficiency | Use Case |
|-------|--------|------------|----------|
| 16384 (16KB) | Low | More frames for large data | Default, RFC minimum |
| 32768 (32KB) | Medium | Balanced | General use |
| 65536 (64KB) | Higher | Fewer frames | Large payloads |

### System Tuning (Linux)

For high concurrency (1000+ clients), tune these kernel parameters:

```bash
# /etc/sysctl.conf or /etc/sysctl.d/99-gunicorn.conf

# Increase socket backlog for burst connections
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Increase network queue size
net.core.netdev_max_backlog = 65535

# Expand ephemeral port range
net.ipv4.ip_local_port_range = 1024 65535

# Allow reuse of TIME_WAIT sockets
net.ipv4.tcp_tw_reuse = 1

# Increase max open files system-wide
fs.file-max = 2097152
```

Apply with: `sudo sysctl -p`

Also increase file descriptor limits:

```bash
# /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
```

### Docker Tuning

For Docker deployments, add these to your container or compose file:

```yaml
# docker-compose.yml
services:
  gunicorn:
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    sysctls:
      net.core.somaxconn: 65535
```

Or in Dockerfile:

```dockerfile
# Increase file descriptor limit
RUN ulimit -n 65535
```

### Benchmark Results

Reference benchmarks using h2load with 4 Gunicorn workers in Docker (Apple M4 Pro):

| Profile | Clients | Streams | Requests/sec | Latency (mean) |
|---------|---------|---------|--------------|----------------|
| Conservative | 100 | 10 | 11,700 | 69ms |
| Conservative | 1000 | 10 | 12,750 | 441ms |
| High Concurrency | 100 | 10 | 15,000+ | 50ms |
| High Concurrency | 1000 | 10 | 21,700 | 253ms |
| High Concurrency | 2000 | 10 | 12,300 | 243ms |

!!! note
    Actual performance varies based on hardware, network, and application complexity.
    Always benchmark your specific workload.

## Testing HTTP/2

### Using curl

```bash
# Check HTTP/2 support
curl -v --http2 https://localhost:443/

# Over TLS, HTTP/2 is negotiated with ALPN; --http2-prior-knowledge is
# for cleartext connections (see the h2c section).
```

### Using Python

```python
import httpx

with httpx.Client(http2=True, verify=False) as client:
    response = client.get("https://localhost:8443/")
    print(f"HTTP Version: {response.http_version}")
```

## Complete Example

A complete HTTP/2 example demonstrating priority and trailers is available in the
`examples/http2_features/` directory. This includes:

- **http2_app.py**: ASGI application showing priority access and trailer sending
- **test_http2.py**: Test script verifying HTTP/2 features
- **Dockerfile** and **docker-compose.yml**: Docker setup for testing

To run the example:

```bash
cd examples/http2_features
docker compose up --build

# In another terminal:
docker compose exec http2-features python /app/http2_features/test_http2.py
```

The example demonstrates:

1. **Priority access**: Reading `http.response.priority` extension in ASGI scope
2. **Response trailers**: Sending `http.response.trailers` messages
3. **Combined features**: Using both priority and trailers in one response

## RFC Compliance

Gunicorn's HTTP/2 implementation is built on the [h2 library](https://github.com/python-hyper/h2)
and complies with the following specifications:

| Feature | RFC | Status | Notes |
|---------|-----|--------|-------|
| HTTP/2 Protocol | [RFC 7540](https://tools.ietf.org/html/rfc7540) | Compliant | Core protocol support |
| HTTP/2 Semantics | [RFC 9113](https://tools.ietf.org/html/rfc9113) | Compliant | Updated HTTP/2 spec |
| HPACK Compression | [RFC 7541](https://tools.ietf.org/html/rfc7541) | Compliant | Via h2 library |
| Stream State Machine | RFC 7540 Section 5.1 | Compliant | Full state transitions |
| Flow Control | RFC 7540 Section 6.9 | Compliant | Stream and connection level |
| Stream Priority | RFC 7540 Section 5.3 | Compliant | Weight and dependency tracking |
| Frame Size Limits | RFC 7540 Section 6.2 | Compliant | Validated 16384-16777215 bytes |
| Pseudo-Headers | RFC 9113 Section 8.3 | Compliant | All required headers supported |
| `:authority` Handling | RFC 9113 Section 8.3.1 | Compliant | Takes precedence over Host |
| Response Trailers | RFC 9110 Section 6.5 | Compliant | Pseudo-headers forbidden |
| GOAWAY Handling | RFC 7540 Section 6.8 | Compliant | Graceful shutdown |
| RST_STREAM Handling | RFC 7540 Section 6.4 | Compliant | Stream reset |
| Early Hints | [RFC 8297](https://tools.ietf.org/html/rfc8297) | Compliant | 103 informational responses |
| Server Push | RFC 7540 Section 6.6 | Not Implemented | Optional feature, rarely used |

!!! note
    Server Push (PUSH_PROMISE) is not implemented. This is an optional HTTP/2 feature that is
    being deprecated in HTTP/3 and is rarely used in practice.

## Security Considerations

HTTP/2 introduces new attack vectors compared to HTTP/1.1. Gunicorn includes several
protections against known vulnerabilities.

### TLS ciphers

RFC 9113 lists cipher suites that must not be used with HTTP/2 (Appendix A).
The default TLS 1.2 list still contains CBC suites that browsers never pick
for HTTP/2; gunicorn does not send `INADEQUATE_SECURITY` itself. Set
`ciphers` to a modern list if your clients are not browsers.

### Built-in Protections

| Attack | Protection | Setting |
|--------|------------|---------|
| Stream Multiplexing Abuse | Limit concurrent streams | `http2_max_concurrent_streams` (default: 100) |
| HPACK Bomb | Header size limits | `http2_max_header_list_size` (default: 65536) |
| Unbounded Upload | Body streams to the app; window credit returns as it reads | `http2_initial_window_size` (default: 65535) |
| Large Frame Attack | Frame size limits | `http2_max_frame_size` (validated: 16384-16777215) |
| Resource Exhaustion | Flow control windows | `http2_initial_window_size` (default: 65535) |
| Slow Read (Slowloris) | Connection timeouts | `timeout` and `keepalive` settings |

### Recommended Security Settings

```python
# gunicorn.conf.py - Security-hardened HTTP/2 configuration

# Limit concurrent streams to prevent resource exhaustion
http2_max_concurrent_streams = 100

# Limit header size to prevent HPACK bomb attacks
http2_max_header_list_size = 65536  # 64KB

# Standard frame size (RFC minimum)
http2_max_frame_size = 16384

# Reasonable flow control window
http2_initial_window_size = 65535  # 64KB

# Connection timeouts to prevent slow attacks
timeout = 30
keepalive = 120
graceful_timeout = 30

# Limit request sizes
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
```

### Additional Recommendations

1. **Use a reverse proxy**: Deploy behind nginx, HAProxy, or a cloud load balancer
   for additional DDoS protection and rate limiting.

2. **Enable rate limiting**: Use your reverse proxy to limit requests per client.

3. **Monitor connections**: Watch for clients opening many streams or holding
   connections open without sending data.

4. **Keep dependencies updated**: Regularly update the `h2` library for security fixes.

For more information on HTTP/2 security vulnerabilities, see:

- [Imperva HTTP/2 Vulnerability Report](https://www.imperva.com/docs/Imperva_HII_HTTP2.pdf)
- [NGINX HTTP/2 Security Advisory](https://www.nginx.com/blog/the-imperva-http2-vulnerability-report-and-nginx/)

## Compliance Testing

### h2spec

[h2spec](https://github.com/summerwind/h2spec) is the standard conformance testing tool
for HTTP/2 implementations. It tests compliance with RFC 7540 and RFC 7541.

```bash
# Install h2spec
# macOS
brew install h2spec

# Linux (download from releases)
curl -L https://github.com/summerwind/h2spec/releases/download/v2.6.0/h2spec_linux_amd64.tar.gz | tar xz

# Run against your server
h2spec -h localhost -p 8443 -t -k

# Options:
#   -t    Use TLS
#   -k    Skip certificate verification
#   -S    Strict mode (test SHOULD requirements)
#   -v    Verbose output
#   -j    Generate JUnit report
```

Example output:
```
Generic tests for HTTP/2 server
  1. Starting HTTP/2
    ✓ Sends a client connection preface
    ...

Hypertext Transfer Protocol Version 2 (HTTP/2)
  3. Starting HTTP/2
    3.5. HTTP/2 Connection Preface
      ✓ Sends invalid connection preface
      ...

94 tests, 94 passed, 0 skipped, 0 failed
```

### nghttp2 Tools

[nghttp2](https://nghttp2.org/) provides useful debugging tools:

```bash
# Install nghttp2
# macOS
brew install nghttp2

# Linux
apt-get install nghttp2-client

# Test HTTP/2 connection
nghttp -v https://localhost:8443/

# Benchmark with h2load
h2load -n 1000 -c 10 https://localhost:8443/
```

### Online Testing

For public servers, you can use online tools:

- [KeyCDN HTTP/2 Test](https://tools.keycdn.com/http2-test)
- [HTTP/2 Check](https://http.dev/2/test)

## See Also

- [Settings Reference](../reference/settings.md#http2_max_concurrent_streams) - All HTTP/2 settings
- [ASGI Worker](../asgi.md) - ASGI worker with HTTP/2 support
- [Deploy](../deploy.md) - General deployment guidance

---

### Content Reference Settings (`docs/content/reference/settings.md`)

> **Generated file** — update `gunicorn/config.py` instead.

# Settings

This reference is built directly from `gunicorn.config.KNOWN_SETTINGS` and is
regenerated during every documentation build.

!!! note
    Settings can be provided through the `GUNICORN_CMD_ARGS` environment
    variable. For example:

    ```console
    $ GUNICORN_CMD_ARGS="--bind=127.0.0.1 --workers=3" gunicorn app:app
    ```

    _Added in 19.7._


<span id="blocking_os_fchmod"></span>

## Config File

### `config`

**Command line:** `-c CONFIG`, `--config CONFIG`

**Default:** `'./gunicorn.conf.py'`

[The Gunicorn config file](../configure.md#configuration-file).

A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``.

Only has an effect when specified on the command line or as part of an
application specific configuration.

By default, a file named ``gunicorn.conf.py`` will be read from the same
directory where gunicorn is being run.

!!! info "Changed in 19.4"
    Loading the config from a Python module requires the ``python:``
    prefix.

### `wsgi_app`

**Default:** `None`

A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``.

!!! info "Added in 20.1.0"

## Control

### `control_socket`

**Command line:** `--control-socket PATH`

**Default:**

$XDG_RUNTIME_DIR/gunicorn.ctl or $HOME/.gunicorn/gunicorn.ctl

Unix socket path for control interface.

The control socket allows runtime management of Gunicorn via the
``gunicornc`` command-line tool. Commands include viewing worker
status, adjusting worker count, and graceful reload/shutdown.

Default: ``$XDG_RUNTIME_DIR/gunicorn.ctl`` if XDG_RUNTIME_DIR is set,
otherwise ``$HOME/.gunicorn/gunicorn.ctl``. The parent directory is
created automatically if needed.

Use ``--no-control-socket`` to disable.

!!! info "Added in 25.1.0"

### `control_socket_mode`

**Command line:** `--control-socket-mode INT`

**Default:** `384`

Permission mode for control socket.

Restricts who can connect to the control socket. Default ``0600``
allows only the socket owner. Set to ``0660`` to allow group access.

!!! info "Added in 25.1.0"

### `control_socket_disable`

**Command line:** `--no-control-socket`

**Default:** `False`

Disable control socket.

When set, no control socket is created and ``gunicornc`` cannot
connect to this Gunicorn instance.

!!! info "Added in 25.1.0"

## Debugging

### `reload`

**Command line:** `--reload`

**Default:** `False`

Restart workers when code changes.

This setting is intended for development. It will cause workers to be
restarted whenever application code changes.

The reloader is incompatible with application preloading. When using a
paste configuration be sure that the server block does not import any
application code or the reload will not work as designed.

The default behavior is to attempt inotify with a fallback to file
system polling. Generally, inotify should be preferred if available
because it consumes less system resources.

!!! note
    In order to use the inotify reloader, you must have the ``inotify``
    package installed.

!!! warning
    Enabling this will change what happens on failure to load the
    the application: While the reloader is active, any and all clients
    that can make requests can see the full exception and traceback!

### `reload_engine`

**Command line:** `--reload-engine STRING`

**Default:** `'auto'`

The implementation that should be used to power [reload](#reload).

Valid engines are:

* ``'auto'``
* ``'poll'``
* ``'inotify'`` (requires inotify)

!!! info "Added in 19.7"

### `reload_extra_files`

**Command line:** `--reload-extra-file FILES`

**Default:** `[]`

Extends [reload](#reload) option to also watch and reload on additional files
(e.g., templates, configurations, specifications, etc.).

Entries containing ``*``, ``?`` or ``[`` are treated as glob patterns and
are re-expanded on every reload check, so a file created after startup
starts being watched without restarting gunicorn. Note that it is the
first edit to that file which triggers a reload, not its creation. Use
``**`` to recurse, and keep patterns narrow: a recursive pattern over a
large tree is walked once per check.

Two things behave the way glob does, not the way a path does. ``*`` does
not match dotfiles, so list ``.env`` literally rather than expecting
``*`` to find it. And a literal path that happens to contain ``*``, ``?``
or ``[`` is read as a pattern, so it warns and matches nothing instead of
failing outright the way a missing plain path does.

Patterns are relative to the directory gunicorn is started from, not to
chdir, which is applied later.

!!! info "Added in 19.8"

!!! info "Changed in 26.1.0"
    Glob patterns are supported.

### `spew`

**Command line:** `--spew`

**Default:** `False`

Install a trace function that spews every line executed by the server.

This is the nuclear option.

### `check_config`

**Command line:** `--check-config`

**Default:** `False`

Check the configuration and exit. The exit status is 0 if the
configuration is correct, and 1 if the configuration is incorrect.

### `print_config`

**Command line:** `--print-config`

**Default:** `False`

Print the configuration settings as fully resolved. Implies [check-config](#check_config).

## Dirty Arbiter Hooks

### `on_dirty_starting`

**Default:**

```python
def on_dirty_starting(arbiter):
    pass
```

Called just before the dirty arbiter process is initialized.

The callable needs to accept a single instance variable for the
DirtyArbiter.

!!! info "Added in 25.0.0"

### `dirty_post_fork`

**Default:**

```python
def dirty_post_fork(arbiter, worker):
    pass
```

Called just after a dirty worker has been forked.

The callable needs to accept two instance variables for the
DirtyArbiter and new DirtyWorker.

!!! info "Added in 25.0.0"

### `dirty_worker_init`

**Default:**

```python
def dirty_worker_init(worker):
    pass
```

Called just after a dirty worker has initialized all applications.

The callable needs to accept one instance variable for the
DirtyWorker.

!!! info "Added in 25.0.0"

### `dirty_worker_exit`

**Default:**

```python
def dirty_worker_exit(arbiter, worker):
    pass
```

Called when a dirty worker has exited.

The callable needs to accept two instance variables for the
DirtyArbiter and the exiting DirtyWorker.

!!! info "Added in 25.0.0"

## Dirty Arbiters

### `dirty_apps`

**Command line:** `--dirty-app STRING`

**Default:** `[]`

Dirty applications to load in the dirty worker pool.

A list of application paths in one of these formats:

- ``$(MODULE_NAME):$(CLASS_NAME)`` - all workers load this app
- ``$(MODULE_NAME):$(CLASS_NAME):$(N)`` - only N workers load this app

Each dirty app must be a class that inherits from ``DirtyApp`` base class
and implements the ``init()``, ``__call__()``, and ``close()`` methods.

Example::

    dirty_apps = [
        "myapp.ml:MLApp",           # All workers load this
        "myapp.images:ImageApp",    # All workers load this
        "myapp.heavy:HugeModel:2",  # Only 2 workers load this
    ]

The per-app worker limit is useful for memory-intensive applications
like large ML models. Instead of all 8 workers loading a 10GB model
(80GB total), you can limit it to 2 workers (20GB total).

Alternatively, you can set the ``workers`` class attribute on your
DirtyApp subclass::

    class HugeModelApp(DirtyApp):
        workers = 2  # Only 2 workers load this app

        def init(self):
            self.model = load_10gb_model()

Note: The config format (``module:Class:N``) takes precedence over
the class attribute if both are specified.

Dirty apps are loaded once when the dirty worker starts and persist
in memory for the lifetime of the worker. This is ideal for loading
ML models, database connection pools, or other stateful resources
that are expensive to initialize.

!!! info "Added in 25.0.0"

!!! info "Changed in 25.1.0"
    Added per-app worker allocation via ``:N`` format suffix.

### `dirty_workers`

**Command line:** `--dirty-workers INT`

**Default:** `0`

The number of dirty worker processes.

A positive integer. Set to 0 (default) to disable the dirty arbiter.
When set to a positive value, a dirty arbiter process will be spawned
to manage the dirty worker pool.

Dirty workers are separate from HTTP workers and are designed for
long-running, blocking operations like ML model inference or heavy
computation.

!!! info "Added in 25.0.0"

### `dirty_timeout`

**Command line:** `--dirty-timeout INT`

**Default:** `300`

Timeout for dirty task execution in seconds.

Workers silent for more than this many seconds are considered stuck
and will be killed. Set to a high value for operations like model
loading that may take a long time.

Value is a positive number. Setting it to 0 disables timeout checking.

!!! info "Added in 25.0.0"

### `dirty_threads`

**Command line:** `--dirty-threads INT`

**Default:** `1`

The number of threads per dirty worker.

Each dirty worker can use threads to handle concurrent operations
within the same process, useful for async-safe applications.

!!! info "Added in 25.0.0"

### `dirty_graceful_timeout`

**Command line:** `--dirty-graceful-timeout INT`

**Default:** `30`

Timeout for graceful dirty worker shutdown in seconds.

After receiving a shutdown signal, dirty workers have this much time
to finish their current tasks. Workers still alive after the timeout
are force killed.

!!! info "Added in 25.0.0"

## HTTP/2

### `http_protocols`

**Command line:** `--http-protocols STRING`

**Default:** `'h1'`

HTTP protocol versions to support (comma-separated, order = preference).

Valid protocols:

* ``h1`` - HTTP/1.1 (default)
* ``h2`` - HTTP/2 (TLS with ALPN, or cleartext via ``http2_cleartext``)

Examples::

    # HTTP/1.1 only (default, backward compatible)
    --http-protocols=h1

    # Prefer HTTP/2, fallback to HTTP/1.1
    --http-protocols=h2,h1

    # Offer HTTP/2 only. A client that does not select it over ALPN
    # is still served HTTP/1.1; there is no rejection.
    --http-protocols=h2

HTTP/2 requires:

* TLS (--certfile and --keyfile)
* The h2 library: ``pip install gunicorn[http2]``
* ALPN-capable TLS client

!!! note
    HTTP/2 cleartext (h2c) is disabled by default. Deployments behind
    a TLS-terminating proxy can enable prior-knowledge h2c with
    http2-prior-knowledge.

!!! info "Added in 25.0.0"

### `http2_cleartext`

**Command line:** `--http2-cleartext STRING`

**Default:** `'off'`

Accept HTTP/2 over cleartext TCP (h2c), and by which mechanism.

Valid values are:

* ``off`` (default) - no cleartext HTTP/2
* ``prior-knowledge`` - serve connections that open with the HTTP/2
  connection preface (``PRI * HTTP/2.0``), RFC 9113 section 3.4
* ``upgrade`` - honour an HTTP/1.1 ``Upgrade: h2c`` request
* ``both`` - accept either

The mechanisms are listed separately on purpose: enabling one does not
enable the other.

With ``prior-knowledge`` alone, a trusted peer that does not send the
preface is refused with 400, since it is expected to speak HTTP/2.
With ``upgrade`` or ``both`` an HTTP/1 request is how an upgrade
begins, so it is served normally.

Only peers in [forwarded-allow-ips](#forwarded_allow_ips) are considered. Everyone else
is served HTTP/1.x exactly as if this were ``off``. Anything else from
a trusted peer on a prior-knowledge port (an HTTP/1.x request, a
malformed or stalled preface) is rejected with a 400: such a peer is
expected to speak HTTP/2, and a silent downgrade would only hide a
misconfiguration.

This is meant for deployments where TLS is terminated by a trusted
proxy that speaks HTTP/2 upstream (Cloud Run, fly.io, a service-mesh
sidecar). Do not expose a cleartext HTTP/2 port to the internet.

Requires ``h2`` in http-protocols and the h2 library
(``pip install gunicorn[http2]``). Ignored when TLS is configured;
there HTTP/2 is negotiated by ALPN.

!!! info "Added in 26.2.0"

### `http2_max_concurrent_streams`

**Command line:** `--http2-max-concurrent-streams INT`

**Default:** `100`

Maximum number of concurrent HTTP/2 streams per connection.

This limits how many requests can be processed simultaneously on a
single HTTP/2 connection. Higher values allow more parallelism but
use more memory.

Default is 100, which matches common server configurations.
The HTTP/2 specification allows up to 2^31-1.

!!! info "Added in 25.0.0"

### `http2_initial_window_size`

**Command line:** `--http2-initial-window-size INT`

**Default:** `65535`

Initial HTTP/2 flow control window size in bytes.

This controls how much data can be in-flight before the receiver
sends WINDOW_UPDATE frames. Larger values can improve throughput
for large transfers but use more memory.

Default is 65535 (64KB - 1), the HTTP/2 specification default.
Maximum is 2^31-1 (2147483647).

!!! info "Added in 25.0.0"

### `http2_max_frame_size`

**Command line:** `--http2-max-frame-size INT`

**Default:** `16384`

Maximum HTTP/2 frame payload size in bytes.

This is the largest frame payload the server will accept.
Larger frames reduce framing overhead but may increase latency
for small messages.

Default is 16384 (16KB), the HTTP/2 specification minimum.
Range is 16384 to 16777215 (16MB - 1).

!!! info "Added in 25.0.0"

### `http2_max_header_list_size`

**Command line:** `--http2-max-header-list-size INT`

**Default:** `65536`

Maximum size of HTTP/2 header list in bytes (HPACK protection).

This limits the total size of headers after HPACK decompression.
Protects against compression bombs and excessive memory use.

Default is 65536 (64KB). Set to 0 for unlimited (not recommended).

!!! info "Added in 25.0.0"

## Logging

### `accesslog`

**Command line:** `--access-logfile FILE`

**Default:** `None`

The Access log file to write to.

``'-'`` means log to stdout.

### `disable_redirect_access_to_syslog`

**Command line:** `--disable-redirect-access-to-syslog`

**Default:** `False`

Disable redirect access logs to syslog.

!!! info "Added in 19.8"

### `access_log_format`

**Command line:** `--access-logformat STRING`

**Default:** `'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'`

The access log format.

| Identifier  | Description                                                                 |
|-------------|-----------------------------------------------------------------------------|
| h           | remote client IP address (not overridden by proxy headers like X-Real-IP)   |
| l           | `'-'` for compatibility with Common Log Format (CLF)                        |
| u           | user name (if HTTP Basic auth used)                                         |
| t           | date of the request at logging time in CLF format                           |
| r           | status line (e.g. `GET / HTTP/1.1`)                                         |
| m           | request method                                                              |
| U           | URL path without query string                                               |
| q           | query string without a leading '?'                                          |
| H           | protocol                                                                    |
| s           | status                                                                      |
| B           | response length                                                             |
| b           | response length or `'-'` (CLF format)                                       |
| f           | referrer (note: header is `referer`)                                        |
| a           | user agent                                                                  |
| T           | request duration in seconds                                                 |
| M           | request duration in milliseconds                                            |
| D           | request duration in microseconds                                            |
| L           | request duration in decimal seconds                                         |
| p           | process ID                                                                  |
| {header}i   | request header                                                              |
| {header}o   | response header                                                             |
| {variable}e | environment variable                                                        |

Use lowercase for header and environment variable names, and put
``{...}x`` names inside ``%(...)s``. For example::

    %({x-forwarded-for}i)s

### `errorlog`

**Command line:** `--error-logfile FILE`, `--log-file FILE`

**Default:** `'-'`

The Error log file to write to.

Using ``'-'`` for FILE makes gunicorn log to stderr.

!!! info "Changed in 19.2"
    Log to stderr by default.

### `loglevel`

**Command line:** `--log-level LEVEL`

**Default:** `'info'`

The granularity of Error log outputs.

Valid level names are:

* ``'debug'``
* ``'info'``
* ``'warning'``
* ``'error'``
* ``'critical'``

### `capture_output`

**Command line:** `--capture-output`

**Default:** `False`

Redirect stdout/stderr to specified file in [errorlog](#errorlog).

!!! info "Added in 19.6"

### `logger_class`

**Command line:** `--logger-class STRING`

**Default:** `'gunicorn.glogging.Logger'`

The logger you want to use to log events in Gunicorn.

The default class (``gunicorn.glogging.Logger``) handles most
normal usages in logging. It provides error and access logging.

You can provide your own logger by giving Gunicorn a Python path to a
class that quacks like ``gunicorn.glogging.Logger``.

### `logconfig`

**Command line:** `--log-config FILE`

**Default:** `None`

The log config file to use.
Gunicorn uses the standard Python logging module's Configuration
file format.

### `logconfig_dict`

**Default:** `{}`

The log config dictionary to use, using the standard Python
logging module's dictionary configuration format. This option
takes precedence over the [logconfig](#logconfig) and [logconfig-json](#logconfig_json) options,
which uses the older file configuration format and JSON
respectively.

Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig

For more context you can look at the default configuration dictionary for logging,
which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``.

!!! info "Added in 19.8"

### `logconfig_json`

**Command line:** `--log-config-json FILE`

**Default:** `None`

The log config to read config from a JSON file

Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig

!!! info "Added in 20.0"

### `syslog_addr`

**Command line:** `--log-syslog-to SYSLOG_ADDR`

**Default:**

Platform-specific:

* macOS: ``'unix:///var/run/syslog'``
* FreeBSD/DragonFly: ``'unix:///var/run/log'``
* OpenBSD: ``'unix:///dev/log'``
* Linux/other: ``'udp://localhost:514'``

Address to send syslog messages.

Address is a string of the form:

* ``unix://PATH#TYPE`` : for unix domain socket. ``TYPE`` can be ``stream``
  for the stream driver or ``dgram`` for the dgram driver.
  ``stream`` is the default.
* ``udp://HOST:PORT`` : for UDP sockets
* ``tcp://HOST:PORT`` : for TCP sockets

### `syslog`

**Command line:** `--log-syslog`

**Default:** `False`

Send *Gunicorn* logs to syslog.

!!! info "Changed in 19.8"
    You can now disable sending access logs by using the
    disable-redirect-access-to-syslog setting.

### `syslog_prefix`

**Command line:** `--log-syslog-prefix SYSLOG_PREFIX`

**Default:** `None`

Makes Gunicorn use the parameter as program-name in the syslog entries.

All entries will be prefixed by ``gunicorn.<prefix>``. By default the
program name is the name of the process.

### `syslog_facility`

**Command line:** `--log-syslog-facility SYSLOG_FACILITY`

**Default:** `'user'`

Syslog facility name

### `enable_stdio_inheritance`

**Command line:** `-R`, `--enable-stdio-inheritance`

**Default:** `False`

Enable stdio inheritance.

Enable inheritance for stdio file descriptors in daemon mode.

Note: To disable the Python stdout buffering, you can to set the user
environment variable ``PYTHONUNBUFFERED`` .

### `statsd_host`

**Command line:** `--statsd-host STATSD_ADDR`

**Default:** `None`

The address of the StatsD server to log to.

Address is a string of the form:

* ``unix://PATH`` : for a unix domain socket.
* ``HOST:PORT`` : for a network address

!!! info "Added in 19.1"

### `dogstatsd_tags`

**Command line:** `--dogstatsd-tags DOGSTATSD_TAGS`

**Default:** `''`

A comma-delimited list of datadog statsd (dogstatsd) tags to append to
statsd metrics. e.g. ``'tag1:value1,tag2:value2'``

!!! info "Added in 20"

### `statsd_prefix`

**Command line:** `--statsd-prefix STATSD_PREFIX`

**Default:** `''`

Prefix to use when emitting statsd metrics (a trailing ``.`` is added,
if not provided).

!!! info "Added in 19.2"

### `enable_backlog_metric`

**Command line:** `--enable-backlog-metric`

**Default:** `False`

Enable socket backlog metric (only supported on Linux).

When enabled, gunicorn will emit a ``gunicorn.backlog`` histogram metric
showing the number of connections waiting in the socket backlog.

## Process Naming

### `proc_name`

**Command line:** `-n STRING`, `--name STRING`

**Default:** `None`

A base to use with setproctitle for process naming.

This affects things like ``ps`` and ``top``. If you're going to be
running more than one instance of Gunicorn you'll probably want to set a
name to tell them apart. This requires that you install the setproctitle
module.

If not set, the *default_proc_name* setting will be used.

### `default_proc_name`

**Default:** `'gunicorn'`

Internal setting that is adjusted for each type of application.

## SSL

### `keyfile`

**Command line:** `--keyfile FILE`

**Default:** `None`

SSL key file

### `certfile`

**Command line:** `--certfile FILE`

**Default:** `None`

SSL certificate file

### `ssl_version`

**Command line:** `--ssl-version`

**Default:** `<_SSLMethod.PROTOCOL_TLS: 2>`

SSL version to use (see stdlib ssl module's).

!!! danger "Deprecated in 21.0"
    The option is deprecated and it is currently ignored. Use [ssl-context](#ssl_context) instead.

============= ============
--ssl-version Description
============= ============
SSLv3         SSLv3 is not-secure and is strongly discouraged.
SSLv23        Alias for TLS. Deprecated in Python 3.6, use TLS.
TLS           Negotiate highest possible version between client/server.
              Can yield SSL. (Python 3.6+)
TLSv1         TLS 1.0
TLSv1_1       TLS 1.1 (Python 3.4+)
TLSv1_2       TLS 1.2 (Python 3.4+)
TLS_SERVER    Auto-negotiate the highest protocol version like TLS,
              but only support server-side SSLSocket connections.
              (Python 3.6+)
============= ============

!!! info "Changed in 19.7"
    The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to
    ``ssl.PROTOCOL_SSLv23``.

!!! info "Changed in 20.0"
    This setting now accepts string names based on ``ssl.PROTOCOL_``
    constants.

!!! info "Changed in 20.0.1"
    The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to
    ``ssl.PROTOCOL_TLS`` when Python >= 3.6 .

### `cert_reqs`

**Command line:** `--cert-reqs`

**Default:** `<VerifyMode.CERT_NONE: 0>`

Whether client certificate is required (see stdlib ssl module's)

===========  ===========================
--cert-reqs      Description
===========  ===========================
`0`          no client verification
`1`          ssl.CERT_OPTIONAL
`2`          ssl.CERT_REQUIRED
===========  ===========================

### `ca_certs`

**Command line:** `--ca-certs FILE`

**Default:** `None`

CA certificates file

### `suppress_ragged_eofs`

**Command line:** `--suppress-ragged-eofs`

**Default:** `True`

Suppress ragged EOFs (see stdlib ssl module's)

### `do_handshake_on_connect`

**Command line:** `--do-handshake-on-connect`

**Default:** `False`

Whether to perform SSL handshake on socket connect (see stdlib ssl module's)

### `ciphers`

**Command line:** `--ciphers`

**Default:** `None`

SSL Cipher suite to use, in the format of an OpenSSL cipher list.

By default we use the default cipher list from Python's ``ssl`` module,
which contains ciphers considered strong at the time of each Python
release.

As a recommended alternative, the Open Web App Security Project (OWASP)
offers `a vetted set of strong cipher strings rated A+ to C-
<https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet>`_.
OWASP provides details on user-agent compatibility at each security level.

See the `OpenSSL Cipher List Format Documentation
<https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-LIST-FORMAT>`_
for details on the format of an OpenSSL cipher list.

## Security

### `limit_request_line`

**Command line:** `--limit-request-line INT`

**Default:** `4094`

The maximum size of HTTP request line in bytes.

This parameter is used to limit the allowed size of a client's
HTTP request-line. Since the request-line consists of the HTTP
method, URI, and protocol version, this directive places a
restriction on the length of a request-URI allowed for a request
on the server. A server needs this value to be large enough to
hold any of its resource names, including any information that
might be passed in the query part of a GET request. Value is a number
from 0 (unlimited) to 8190.

This parameter can be used to prevent any DDOS attack.

### `limit_request_fields`

**Command line:** `--limit-request-fields INT`

**Default:** `100`

Limit the number of HTTP headers fields in a request.

This parameter is used to limit the number of headers in a request to
prevent DDOS attack. Used with the *limit_request_field_size* it allows
more safety. By default this value is 100 and can't be larger than
32768.

### `limit_request_field_size`

**Command line:** `--limit-request-field_size INT`

**Default:** `8190`

Limit the allowed size of an HTTP request header field.

Value is a positive number or 0. Setting it to 0 will allow unlimited
header field sizes.

!!! warning
    Setting this parameter to a very high or unlimited value can open
    up for DDOS attacks.

## Server Hooks

### `on_starting`

**Default:**

```python
def on_starting(server):
    pass
```

Called just before the master process is initialized.

The callable needs to accept a single instance variable for the Arbiter.

### `on_reload`

**Default:**

```python
def on_reload(server):
    pass
```

Called to recycle workers during a reload via SIGHUP.

The callable needs to accept a single instance variable for the Arbiter.

### `when_ready`

**Default:**

```python
def when_ready(server):
    pass
```

Called just after the server is started.

The callable needs to accept a single instance variable for the Arbiter.

### `pre_fork`

**Default:**

```python
def pre_fork(server, worker):
    pass
```

Called just before a worker is forked.

The callable needs to accept two instance variables for the Arbiter and
new Worker.

### `post_fork`

**Default:**

```python
def post_fork(server, worker):
    pass
```

Called just after a worker has been forked.

The callable needs to accept two instance variables for the Arbiter and
new Worker.

### `post_worker_init`

**Default:**

```python
def post_worker_init(worker):
    pass
```

Called just after a worker has initialized the application.

The callable needs to accept one instance variable for the initialized
Worker.

### `worker_int`

**Default:**

```python
def worker_int(worker):
    pass
```

Called just after a worker exited on SIGINT or SIGQUIT.

The callable needs to accept one instance variable for the initialized
Worker.

### `worker_abort`

**Default:**

```python
def worker_abort(worker):
    pass
```

Called when a worker received the SIGABRT signal.

This call generally happens on timeout.

The callable needs to accept one instance variable for the initialized
Worker.

### `pre_exec`

**Default:**

```python
def pre_exec(server):
    pass
```

Called just before a new master process is forked.

The callable needs to accept a single instance variable for the Arbiter.

### `pre_request`

**Default:**

```python
def pre_request(worker, req):
    worker.log.debug("%s %s", req.method, req.path)
```

Called just before a worker processes the request.

The callable needs to accept two instance variables for the Worker and
the Request.

### `post_request`

**Default:**

```python
def post_request(worker, req, environ, resp):
    pass
```

Called after a worker processes the request.

The callable needs to accept two instance variables for the Worker and
the Request. If a third parameter is defined it will be passed the
environment. If a fourth parameter is defined it will be passed the Response.

### `child_exit`

**Default:**

```python
def child_exit(server, worker):
    pass
```

Called just after a worker has been exited, in the master process.

The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker.

!!! info "Added in 19.7"

### `worker_exit`

**Default:**

```python
def worker_exit(server, worker):
    pass
```

Called just after a worker has been exited, in the worker process.

The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker.

### `nworkers_changed`

**Default:**

```python
def nworkers_changed(server, new_value, old_value):
    pass
```

Called just after *num_workers* has been changed.

The callable needs to accept an instance variable of the Arbiter and
two integers of number of workers after and before change.

If the number of workers is set for the first time, *old_value* would
be ``None``.

### `on_exit`

**Default:**

```python
def on_exit(server):
    pass
```

Called just before exiting Gunicorn.

The callable needs to accept a single instance variable for the Arbiter.

### `ssl_context`

**Default:**

```python
def ssl_context(config, default_ssl_context_factory):
    return default_ssl_context_factory()
```

Called when SSLContext is needed.

Allows customizing SSL context.

The callable needs to accept an instance variable for the Config and
a factory function that returns default SSLContext which is initialized
with certificates, private key, cert_reqs, and ciphers according to
config and can be further customized by the callable.
The callable needs to return SSLContext object.

Following example shows a configuration file that sets the minimum TLS version to 1.3:

```python
def ssl_context(conf, default_ssl_context_factory):
    import ssl
    context = default_ssl_context_factory()
    context.minimum_version = ssl.TLSVersion.TLSv1_3
    return context
```

!!! info "Added in 21.0"

## Server Mechanics

### `preload_app`

**Command line:** `--preload`

**Default:** `False`

Load application code before the worker processes are forked.

By preloading an application you can save some RAM resources as well as
speed up server boot times. Although, if you defer application loading
to each worker process, you can reload your application code easily by
restarting workers.

### `sendfile`

**Command line:** `--no-sendfile`

**Default:** `None`

Disables the use of ``sendfile()``.

If not set, the value of the ``SENDFILE`` environment variable is used
to enable or disable its usage.

!!! info "Added in 19.2"

!!! info "Changed in 19.4"
    Swapped ``--sendfile`` with ``--no-sendfile`` to actually allow
    disabling.

!!! info "Changed in 19.6"
    added support for the ``SENDFILE`` environment variable

### `reuse_port`

**Command line:** `--reuse-port`

**Default:** `False`

Set the ``SO_REUSEPORT`` flag on the listening socket.

!!! info "Added in 19.8"

### `chdir`

**Command line:** `--chdir`

**Default:**

``'.'``

Change directory to specified directory before loading apps.

### `daemon`

**Command line:** `-D`, `--daemon`

**Default:** `False`

Daemonize the Gunicorn process.

Detaches the server from the controlling terminal and enters the
background.

### `raw_env`

**Command line:** `-e ENV`, `--env ENV`

**Default:** `[]`

Set environment variables in the execution environment.

Should be a list of strings in the ``key=value`` format.

For example on the command line:

```console
$ gunicorn -b 127.0.0.1:8000 --env FOO=1 test:app
```

Or in the configuration file:

```python
raw_env = ["FOO=1"]
```

### `pidfile`

**Command line:** `-p FILE`, `--pid FILE`

**Default:** `None`

A filename to use for the PID file.

If not set, no PID file will be written.

### `worker_tmp_dir`

**Command line:** `--worker-tmp-dir DIR`

**Default:** `None`

A directory to use for the worker heartbeat temporary file.

If not set, the default temporary directory will be used.

!!! note
    The current heartbeat system involves calling ``os.fchmod`` on
    temporary file handlers and may block a worker for arbitrary time
    if the directory is on a disk-backed filesystem.

    See [blocking-os-fchmod](#blocking_os_fchmod) for more detailed information
    and a solution for avoiding this problem.

### `user`

**Command line:** `-u USER`, `--user USER`

**Default:**

``os.geteuid()``

Switch worker processes to run as this user.

A valid user id (as an integer) or the name of a user that can be
retrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not
change the worker process user.

### `group`

**Command line:** `-g GROUP`, `--group GROUP`

**Default:**

``os.getegid()``

Switch worker process to run as this group.

A valid group id (as an integer) or the name of a user that can be
retrieved with a call to ``grp.getgrnam(value)`` or ``None`` to not
change the worker processes group.

### `umask`

**Command line:** `-m INT`, `--umask INT`

**Default:** `0`

A bit mask for the file mode on files written by Gunicorn.

Note that this affects unix socket permissions.

A valid value for the ``os.umask(mode)`` call or a string compatible
with ``int(value, 0)`` (``0`` means Python guesses the base, so values
like ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal
representations)

### `initgroups`

**Command line:** `--initgroups`

**Default:** `False`

If true, set the worker process's group access list with all of the
groups of which the specified username is a member, plus the specified
group id.

!!! info "Added in 19.7"

### `tmp_upload_dir`

**Default:** `None`

Directory to store temporary request data as they are read.

This may disappear in the near future.

This path should be writable by the process permissions set for Gunicorn
workers. If not specified, Gunicorn will choose a system generated
temporary directory.

### `secure_scheme_headers`

**Default:** `{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}`

A dictionary containing headers and values that the front-end proxy
uses to indicate HTTPS requests. If the source IP is permitted by
[forwarded-allow-ips](#forwarded_allow_ips) (below), *and* at least one request header matches
a key-value pair listed in this dictionary, then Gunicorn will set
``wsgi.url_scheme`` to ``https``, so your application can tell that the
request is secure.

If the other headers listed in this dictionary are not present in the request, they will be ignored,
but if the other headers are present and do not match the provided values, then
the request will fail to parse. See the note below for more detailed examples of this behaviour.

The dictionary should map upper-case header names to exact string
values. The value comparisons are case-sensitive, unlike the header
names, so make sure they're exactly what your front-end proxy sends
when handling HTTPS requests.

It is important that your front-end proxy configuration ensures that
the headers defined here can not be passed directly from the client.

### `forwarded_allow_ips`

**Command line:** `--forwarded-allow-ips STRING`

**Default:** `'127.0.0.1,::1'`

Front-end's IP addresses or networks from which allowed to handle
set secure headers. (comma separated).

Supports both individual IP addresses (e.g., ``192.168.1.1``) and
CIDR networks (e.g., ``192.168.0.0/16``).

Set to ``*`` to disable checking of front-end IPs. This is useful for setups
where you don't know in advance the IP address of front-end, but
instead have ensured via other means that only your
authorized front-ends can access Gunicorn.

By default, the value of the ``FORWARDED_ALLOW_IPS`` environment
variable. If it is not defined, the default is ``"127.0.0.1,::1"``.

!!! note
    This option does not affect UNIX socket connections. Connections not associated with
    an IP address are treated as allowed, unconditionally.

!!! note
    The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of
    ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate.
    In each case, we have a request from the remote address 134.213.44.18, and the default value of
    ``secure_scheme_headers``:

    ```python
    secure_scheme_headers = {
        'X-FORWARDED-PROTOCOL': 'ssl',
        'X-FORWARDED-PROTO': 'https',
        'X-FORWARDED-SSL': 'on'
    }
    ```

    +---------------------+----------------------------+-----------------------------+-------------------------+
    | forwarded-allow-ips | Secure Request Headers     | Result                      | Explanation             |
    +=====================+============================+=============================+=========================+
    | `"127.0.0.1"`       | `X-Forwarded-Proto: https` | `wsgi.url_scheme = "http"`  | IP address was not      |
    |                     |                            |                             | allowed                 |
    +---------------------+----------------------------+-----------------------------+-------------------------+
    |                     |                            |                             | IP address allowed, but |
    | `"*"`               | `<none>`                   | `wsgi.url_scheme = "http"`  | no secure headers       |
    |                     |                            |                             | provided                |
    +---------------------+----------------------------+-----------------------------+-------------------------+
    | `"*"`               | `X-Forwarded-Proto: https` | `wsgi.url_scheme = "https"` | IP address allowed, one |
    |                     |                            |                             | request header matched  |
    +---------------------+----------------------------+-----------------------------+-------------------------+
    |                     |                            |                             | IP address allowed, but |
    | `"134.213.44.18"`   | `X-Forwarded-Ssl: on`      | `InvalidSchemeHeaders()`    | the two secure headers  |
    |                     | `X-Forwarded-Proto: http`  | raised                      | disagreed on if HTTPS   |
    |                     |                            |                             | was used                |
    +---------------------+----------------------------+-----------------------------+-------------------------+

### `pythonpath`

**Command line:** `--pythonpath STRING`

**Default:** `None`

A comma-separated list of directories to add to the Python path.

e.g.
``'/home/djangoprojects/myproject,/home/python/mylibrary'``.

### `paste`

**Command line:** `--paste STRING`, `--paster STRING`

**Default:** `None`

Load a PasteDeploy config file. The argument may contain a ``#``
symbol followed by the name of an app section from the config file,
e.g. ``production.ini#admin``.

At this time, using alternate server blocks is not supported. Use the
command line arguments to control server configuration instead.

### `proxy_protocol`

**Command line:** `--proxy-protocol MODE`

**Default:** `'off'`

Enable PROXY protocol support.

Allow using HTTP and PROXY protocol together. It may be useful for work
with stunnel as HTTPS frontend and Gunicorn as HTTP server, or with
HAProxy.

Accepted values:

* ``off`` - Disabled (default)
* ``v1`` - PROXY protocol v1 only (text format)
* ``v2`` - PROXY protocol v2 only (binary format)
* ``auto`` - Auto-detect v1 or v2

Using ``--proxy-protocol`` without a value is equivalent to ``auto``.

PROXY protocol v1: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
PROXY protocol v2: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

Example for stunnel config::

    [https]
    protocol = proxy
    accept  = 443
    connect = 80
    cert = /etc/ssl/certs/stunnel.pem
    key = /etc/ssl/certs/stunnel.key

!!! info "Changed in 24.1.0"
    Extended to support version selection (v1, v2, auto).

### `proxy_allow_ips`

**Command line:** `--proxy-allow-from`

**Default:** `'127.0.0.1,::1'`

Front-end's IP addresses or networks from which allowed accept
proxy requests (comma separated).

Supports both individual IP addresses (e.g., ``192.168.1.1``) and
CIDR networks (e.g., ``192.168.0.0/16``).

Set to ``*`` to disable checking of front-end IPs. This is useful for setups
where you don't know in advance the IP address of front-end, but
instead have ensured via other means that only your
authorized front-ends can access Gunicorn.

!!! note
    This option does not affect UNIX socket connections. Connections not associated with
    an IP address are treated as allowed, unconditionally.

### `protocol`

**Command line:** `--protocol STRING`

**Default:** `'http'`

The protocol for incoming connections.

* ``http`` - Standard HTTP/1.x (default)
* ``uwsgi`` - uWSGI binary protocol (for nginx uwsgi_pass)

When using the uWSGI protocol, Gunicorn can receive requests from
nginx using the uwsgi_pass directive::

    upstream gunicorn {
        server 127.0.0.1:8000;
    }
    location / {
        uwsgi_pass gunicorn;
        include uwsgi_params;
    }

### `uwsgi_allow_ips`

**Command line:** `--uwsgi-allow-from`

**Default:** `'127.0.0.1,::1'`

IPs allowed to send uWSGI protocol requests (comma separated).

Set to ``*`` to allow all IPs. This is useful for setups where you
don't know in advance the IP address of front-end, but instead have
ensured via other means that only your authorized front-ends can
access Gunicorn.

!!! note
    This option does not affect UNIX socket connections. Connections not associated with
    an IP address are treated as allowed, unconditionally.

### `raw_paste_global_conf`

**Command line:** `--paste-global CONF`

**Default:** `[]`

Set a PasteDeploy global config variable in ``key=value`` form.

The option can be specified multiple times.

The variables are passed to the PasteDeploy entrypoint. Example::

    $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2

!!! info "Added in 19.7"

### `permit_obsolete_folding`

**Command line:** `--permit-obsolete-folding`

**Default:** `False`

Permit requests employing obsolete HTTP line folding mechanism

The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be
 employed in HTTP request headers from standards-compliant HTTP clients.

This option is provided to diagnose backwards-incompatible changes.
Use with care and only if necessary. Temporary; the precise effect of this option may
change in a future version, or it may be removed altogether.

!!! info "Added in 23.0.0"

### `strip_header_spaces`

**Command line:** `--strip-header-spaces`

**Default:** `False`

Strip spaces present between the header name and the the ``:``.

This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard.
See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn.

Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0

!!! info "Added in 20.0.1"

### `permit_unconventional_http_method`

**Command line:** `--permit-unconventional-http-method`

**Default:** `False`

Permit HTTP methods not matching conventions, such as IANA registration guidelines

This permits request methods of length less than 3 or more than 20,
methods with lowercase characters or methods containing the # character.
HTTP methods are case sensitive by definition, and merely uppercase by convention.

If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status
in cases where otherwise 501 status is expected. While this option does modify that
behaviour, it should not be depended upon to guarantee standards-compliant behaviour.
Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible
changes around the incomplete application of those restrictions.

Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0

!!! info "Added in 22.0.0"

### `permit_unconventional_http_version`

**Command line:** `--permit-unconventional-http-version`

**Default:** `False`

Permit HTTP version not matching conventions of 2023

This disables the refusal of likely malformed request lines.
It is unusual to specify HTTP 1 versions other than 1.0 and 1.1.

This option is provided to diagnose backwards-incompatible changes.
Use with care and only if necessary. Temporary; the precise effect of this option may
change in a future version, or it may be removed altogether.

!!! info "Added in 22.0.0"

### `casefold_http_method`

**Command line:** `--casefold-http-method`

**Default:** `False`

Transform received HTTP methods to uppercase

HTTP methods are case sensitive by definition, and merely uppercase by convention.

This option is provided because previous versions of gunicorn defaulted to this behaviour.

Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0

!!! info "Added in 22.0.0"

### `forwarder_headers`

**Command line:** `--forwarder-headers`

**Default:** `'SCRIPT_NAME,PATH_INFO'`

A list containing upper-case header field names that the front-end proxy
(see [forwarded-allow-ips](#forwarded_allow_ips)) sets, to be used in WSGI environment.

This option has no effect for headers not present in the request.

This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO``
and ``REMOTE_USER``.

It is important that your front-end proxy configuration ensures that
the headers defined here can not be passed directly from the client.

### `header_map`

**Command line:** `--header-map`

**Default:** `'drop'`

Configure how header field names are mapped into environ

Headers containing underscores are permitted by RFC9110,
but gunicorn joining headers of different names into
the same environment variable will dangerously confuse applications as to which is which.

The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.
The value ``refuse`` will return an error if a request contains *any* such header.
The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different
header field names into the same environ name.

If the source is permitted as explained in [forwarded-allow-ips](#forwarded_allow_ips), *and* the header name is
present in [forwarder-headers](#forwarder_headers), the header is mapped into environment regardless of
the state of this setting.

Use with care and only if necessary and after considering if your problem could
instead be solved by specifically renaming or rewriting only the intended headers
on a proxy in front of Gunicorn.

!!! info "Added in 22.0.0"

### `root_path`

**Command line:** `--root-path STRING`

**Default:** `''`

The root path for ASGI applications.

This is used to set the ``root_path`` in the ASGI scope, which
allows applications to know their mount point when behind a
reverse proxy.

For example, if your application is mounted at ``/api``, set
this to ``/api``.

!!! info "Added in 24.0.0"

## Server Socket

### `bind`

**Command line:** `-b ADDRESS`, `--bind ADDRESS`

**Default:** `['127.0.0.1:8000']`

The socket to bind.

A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``,
``fd://FD``. An IP is a valid ``HOST``.

!!! info "Changed in 20.0"
    Support for ``fd://FD`` got added.

Multiple addresses can be bound. ex.::

    $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app

will bind the `test:app` application on localhost both on ipv6
and ipv4 interfaces.

If the ``PORT`` environment variable is defined, the default
is ``['0.0.0.0:$PORT']``. If it is not defined, the default
is ``['127.0.0.1:8000']``.

### `backlog`

**Command line:** `--backlog INT`

**Default:** `2048`

The maximum number of pending connections.

This refers to the number of clients that can be waiting to be served.
Exceeding this number results in the client getting an error when
attempting to connect. It should only affect servers under significant
load.

Must be a positive integer. Generally set in the 64-2048 range.

## Worker Processes

### `workers`

**Command line:** `-w INT`, `--workers INT`

**Default:** `1`

The number of worker processes for handling requests.

A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.
You'll want to vary this a bit to find the best for your particular
application's work load.

By default, the value of the ``WEB_CONCURRENCY`` environment variable,
which is set by some Platform-as-a-Service providers such as Heroku. If
it is not defined, the default is ``1``.

### `worker_class`

**Command line:** `-k STRING`, `--worker-class STRING`

**Default:** `'sync'`

The type of workers to use.

The default class (``sync``) should handle most "normal" types of
workloads. You'll want to read :doc:`design` for information on when
you might want to choose one of the other worker classes. Required
libraries may be installed using setuptools' ``extras_require`` feature.

A string referring to one of the following bundled classes:

* ``sync``
* ``gevent``   - Requires gevent >= 24.10.1 (or install it via
  ``pip install gunicorn[gevent]``)
* ``tornado``  - Requires tornado >= 6.5.0 (or install it via
  ``pip install gunicorn[tornado]``)
* ``gthread``  - Python 2 requires the futures package to be installed
  (or install it via ``pip install gunicorn[gthread]``)

Optionally, you can provide your own worker by giving Gunicorn a
Python path to a subclass of ``gunicorn.workers.base.Worker``.
This alternative syntax will load the gevent class:
``gunicorn.workers.ggevent.GeventWorker``.

### `threads`

**Command line:** `--threads INT`

**Default:** `1`

The number of worker threads for handling requests.

Run each worker with the specified number of threads.

A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.
You'll want to vary this a bit to find the best for your particular
application's work load.

If it is not defined, the default is ``1``.

This setting only affects the Gthread worker type.

!!! note
    If you try to use the ``sync`` worker type and set the ``threads``
    setting to more than 1, the ``gthread`` worker type will be used
    instead.

### `worker_connections`

**Command line:** `--worker-connections INT`

**Default:** `1000`

The maximum number of simultaneous clients.

This setting only affects the ``gthread`` and ``gevent`` worker types.

### `max_requests`

**Command line:** `--max-requests INT`

**Default:** `0`

The maximum number of requests a worker will process before restarting.

Any value greater than zero will limit the number of requests a worker
will process before automatically restarting. This is a simple method
to help limit the damage of memory leaks.

If this is set to zero (the default) then the automatic worker
restarts are disabled.

### `max_requests_jitter`

**Command line:** `--max-requests-jitter INT`

**Default:** `0`

The maximum jitter to add to the *max_requests* setting.

The jitter causes the restart per worker to be randomized by
``randint(0, max_requests_jitter)``. This is intended to stagger worker
restarts to avoid all workers restarting at the same time.

!!! info "Added in 19.2"

### `timeout`

**Command line:** `-t INT`, `--timeout INT`

**Default:** `30`

Workers silent for more than this many seconds are killed and restarted.

Value is a positive number or 0. Setting it to 0 has the effect of
infinite timeouts by disabling timeouts for all workers entirely.

Generally, the default of thirty seconds should suffice. Only set this
noticeably higher if you're sure of the repercussions for sync workers.
For the non sync workers it just means that the worker process is still
communicating and is not tied to the length of time required to handle a
single request.

On HTTP/2 connections it also bounds a single stream: a request body
that makes no progress for this long, or a response the peer stops
reading for this long, is cancelled with ``RST_STREAM``. 0 disables
that too.

!!! info "Changed in 26.3.0"
    Also bounds a stalled HTTP/2 stream.

### `graceful_timeout`

**Command line:** `--graceful-timeout INT`

**Default:** `30`

Timeout for graceful workers restart in seconds.

After receiving a restart signal, workers have this much time to finish
serving requests. Workers still alive after the timeout (starting from
the receipt of the restart signal) are force killed.

### `keepalive`

**Command line:** `--keep-alive INT`

**Default:** `2`

The number of seconds to wait for requests on a Keep-Alive connection.

Generally set in the 1-5 seconds range for servers with direct connection
to the client (e.g. when you don't have separate load balancer). When
Gunicorn is deployed behind a load balancer, it often makes sense to
set this to a higher value.

!!! note
    ``sync`` worker does not support persistent connections and will
    ignore this option.

On the ``gthread`` and ``gevent`` workers an HTTP/2 connection with no
stream open is closed with ``GOAWAY`` after this many seconds.

!!! info "Changed in 26.3.0"
    Also applies to idle HTTP/2 connections.

### `asgi_loop`

**Command line:** `--asgi-loop STRING`

**Default:** `'auto'`

Event loop implementation for ASGI workers.

- auto: Use uvloop if available, otherwise asyncio
- asyncio: Use Python's built-in asyncio event loop
- uvloop: Use uvloop (must be installed separately)

This setting only affects the ``asgi`` worker type.

uvloop typically provides better performance but requires
installing the uvloop package.

!!! info "Added in 24.0.0"

### `asgi_lifespan`

**Command line:** `--asgi-lifespan STRING`

**Default:** `'auto'`

Control ASGI lifespan protocol handling.

- auto: Detect if app supports lifespan, enable if so
- on: Always run lifespan protocol (fail if unsupported)
- off: Never run lifespan protocol

The lifespan protocol allows ASGI applications to run code at
startup and shutdown. This is essential for frameworks like
FastAPI that need to initialize database connections, caches,
or other resources.

This setting only affects the ``asgi`` worker type.

!!! info "Added in 24.0.0"

### `asgi_disconnect_grace_period`

**Command line:** `--asgi-disconnect-grace-period INT`

**Default:** `3`

Grace period (seconds) for ASGI apps to handle client disconnects.

When a client disconnects, the ASGI app receives an http.disconnect
message and has this many seconds to clean up resources (like database
connections) before the request task is cancelled.

Set to 0 to cancel immediately (not recommended for apps with async
database connections). Apps with long-running database operations may
need to increase this value.

This setting only affects the ``asgi`` worker type.

!!! info "Added in 25.0.0"

### `http_parser`

**Command line:** `--http-parser STRING`

**Default:** `'auto'`

HTTP parser implementation.

- auto: Use gunicorn_h1c if it is available, else use the pure Python parser (default)
- fast: Require gunicorn_h1c (fail if unavailable)
- python: Force pure Python parser

The gunicorn_h1c C extension provides significantly faster HTTP parsing
using picohttpparser with SIMD optimizations.

Install it with: pip install gunicorn[fast]

!!! info "Added in 25.0.0"

---

--- METRICS ---
- Files Extracted: 50
- Estimated Token Budget: ~95744 tokens
- Canonical Reference: https://codewiki.google/github.com/benoitc/gunicorn
