Introduction
FastAPI is a modern, fast Python framework for building APIs. However, by default, the FastAPI development server (uvicorn) only runs a single worker process, which means it can only handle one request at a time. For production and handling concurrent requests, we need Gunicorn as a process manager.
Gunicorn (Green Unicorn) is an HTTP server for Python that can run multiple worker processes, allowing our FastAPI application to handle many requests simultaneously.
Why Gunicorn?
Advantages of using Gunicorn:- Multiple worker processes for concurrent request handling
- Automatic load balancing between workers
- Graceful restart without downtime
- Production-ready and battle-tested
- Compatible with uvicorn workers for async support
Installation
First, install the required dependencies:
pip install fastapi uvicorn gunicorn
Or add to requirements.txt:
fastapi==0.109.0
uvicorn[standard]==0.27.0
gunicorn==21.2.0
Then install:
pip install -r requirements.txt
Creating a Simple FastAPI Application
Create a main.py file:
from fastapi import FastAPI
import time
import os
app = FastAPI()
@app.get("/")
async def root():
return {
"message": "Hello World",
"workerpid": os.getpid()
}
@app.get("/slow")
async def slowendpoint():
# Simulate time-consuming operation
time.sleep(5)
return {
"message": "This took 5 seconds",
"workerpid": os.getpid()
}
@app.get("/health")
async def healthcheck():
return {"status": "healthy"}
Setting Up Gunicorn with Uvicorn Workers
Gunicorn itself is a synchronous server. To get async benefits from FastAPI, we use Uvicorn workers.
Running from Command Line
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Parameter explanation:
main:app- module:application (main.py with app variable)--workers 4- number of worker processes--worker-class uvicorn.workers.UvicornWorker- use async uvicorn workers--bind 0.0.0.0:8000- host and port
Creating a Configuration File
Create a gunicornconf.py file for more structured configuration:
import multiprocessing
import os
Server Socket
bind = "0.0.0.0:8000"
backlog = 2048
Worker Processes
workers = int(os.getenv("WORKERS", multiprocessing.cpucount() 2 + 1))
workerclass = "uvicorn.workers.UvicornWorker"
workerconnections = 1000
maxrequests = 10000
maxrequestsjitter = 1000
timeout = 120
keepalive = 5
Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"
accesslogformat = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
Process Naming
procname = "fastapiapp"
Server Mechanics
daemon = False
pidfile = None
user = None
group = None
tmpuploaddir = None
Graceful Timeout
gracefultimeout = 30
Run with config file:
gunicorn main:app -c gunicornconf.py
Determining Optimal Number of Workers
General formula for determining number of workers:
workers = (2 CPUCORES) + 1
Examples:
- 2 CPU cores = 5 workers
- 4 CPU cores = 9 workers
- 8 CPU cores = 17 workers
- For CPU-bound tasks: use the formula above
- For I/O-bound tasks: can use more workers
- Monitor memory usage, don't run out of RAM
- Start with the standard formula, then adjust based on monitoring
Testing Concurrent Requests
Test whether concurrent invocation works correctly.
Using Python
Create a testconcurrent.py file:
import asyncio
import aiohttp
import time
async def makerequest(session, url, requestid):
start = time.time()
async with session.get(url) as response:
data = await response.json()
duration = time.time() - start
print(f"Request {requestid}: PID={data.get('workerpid')} Duration={duration:.2f}s")
return data
async def testconcurrent():
url = "http://localhost:8000/slow"
async with aiohttp.ClientSession() as session:
tasks = [makerequest(session, url, i) for i in range(10)]
starttime = time.time()
results = await asyncio.gather(*tasks)
totaltime = time.time() - starttime
print(f"\nTotal time for 10 concurrent requests: {totaltime:.2f}s")
print(f"Without concurrency would take: ~50s")
# Check different PIDs
pids = set(r.get('workerpid') for r in results)
print(f"\nRequests handled by {len(pids)} different workers")
print(f"Worker PIDs: {pids}")
if name == "main":
asyncio.run(testconcurrent())
Run the test:
pip install aiohttp
python testconcurrent.py
Expected output:
Request 0: PID=12345 Duration=5.02s
Request 1: PID=12346 Duration=5.03s
Request 2: PID=12347 Duration=5.01s
...
Total time for 10 concurrent requests: 5.10s
Without concurrency would take: ~50s
Requests handled by 4 different workers
Worker PIDs: {12345, 12346, 12347, 12348}
Using Apache Bench
ab -n 100 -c 10 http://localhost:8000/
Parameters:
-n 100- total 100 requests-c 10- 10 concurrent requests
Environment Variables
Create a .env file for configuration:
WORKERS=4
HOST=0.0.0.0
PORT=8000
LOGLEVEL=info
Update gunicornconf.py to read env vars:
import os
from dotenv import loaddotenv
loaddotenv()
bind = f"{os.getenv('HOST', '0.0.0.0')}:{os.getenv('PORT', '8000')}"
workers = int(os.getenv('WORKERS', 4))
loglevel = os.getenv('LOGLEVEL', 'info')
Production Deployment
Using Systemd
Create a /etc/systemd/system/fastapi.service file:
[Unit]
Description=FastAPI Application
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/path/to/your/app
Environment="PATH=/path/to/venv/bin"
ExecStart=/path/to/venv/bin/gunicorn main:app -c gunicornconf.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable fastapi
sudo systemctl start fastapi
sudo systemctl status fastapi
Using Docker
Create a Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "main:app", "-c", "gunicornconf.py"]
Build and run:
docker build -t fastapi-app .
docker run -p 8000:8000 -e WORKERS=4 fastapi-app
Docker Compose
Create a docker-compose.yml file:
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- WORKERS=4
- LOGLEVEL=info
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
Run:
docker-compose up -d
Best Practices
1. Worker Management
# gunicornconf.py
Restart workers after handling requests (prevent memory leaks)
max
requests = 1000
maxrequestsjitter = 50 # Randomize restart to avoid thundering herd
Timeout for long-running requests
timeout = 120
Graceful shutdown
gracefultimeout = 30
2. Health Checks
Implement health check endpoint:
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
@app.get("/health")
async def healthcheck():
return JSONResponse(
statuscode=status.HTTP200OK,
content={
"status": "healthy",
"workerpid": os.getpid()
}
)
3. Monitoring
Use middleware for tracking:
import time
from fastapi import Request
@app.middleware("http")
async def addprocesstimeheader(request: Request, callnext):
starttime = time.time()
response = await callnext(request)
processtime = time.time() - starttime
response.headers["X-Process-Time"] = str(processtime)
response.headers["X-Worker-PID"] = str(os.getpid())
return response
4. Logging
Setup proper logging:
import logging
from fastapi import FastAPI
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(name)
app = FastAPI()
@app.onevent("startup")
async def startupevent():
logger.info(f"Worker started with PID: {os.getpid()}")
@app.onevent("shutdown")
async def shutdownevent():
logger.info(f"Worker shutting down with PID: {os.getpid()}")
Common Issues and Solutions
Issue 1: Workers Timeout
Problem: Workers die because requests take too long Solution: Increase timeouttimeout = 300 # 5 minutes
Issue 2: Memory Leak
Problem: Memory usage keeps growing Solution: Auto-restart workersmaxrequests = 1000
maxrequestsjitter = 100
Issue 3: Too Many Workers
Problem: Server runs out of memory Solution: Reduce number of workers or upgrade RAMworkers = 4 # Start small, monitor, then adjust
Conclusion
Setting up Gunicorn with FastAPI for concurrent invocation includes:
With proper setup, your FastAPI application will be ready to handle thousands of concurrent requests efficiently and reliably in a production environment.
Key Takeaways:- Gunicorn + Uvicorn workers = async concurrency support
- Worker formula: (2 × CPU cores) + 1
- Always test concurrent behavior before production
- Monitor and adjust configuration based on real usage
- Implement proper logging and health checks