22.1. Application Servers
Table of Contents
Why You Need an Application Server
When you run a backend application in production, you do not usually expose your framework directly to the internet. Instead, you run it inside a dedicated application server process.
An application server is a program that:
- Listens for HTTP requests on a port.
- Runs your application code for each request.
- Manages multiple requests at the same time.
- Handles timeouts, errors, and graceful shutdown.
It sits between your code and the outside world, often behind a reverse proxy such as Nginx or Traefik.
Typical Python examples are Uvicorn and Gunicorn. In this chapter we focus on the general ideas that apply to application servers and will look at specific ones in the next chapters.
Key idea: Do not run your development server in production. Always use a proper application server that is designed for concurrency, robustness, and performance.
Application Server vs Development Server
Most backend frameworks come with a development server:
- Flask has
flask run - Django has
python manage.py runserver - FastAPI examples often show
uvicorn main:app --reloadfor local development
These are convenient but are not tuned or hardened for production.
Typical differences:
| Feature | Development Server | Application Server |
|---|---|---|
| Performance | Basic | Optimized, often supports async and workers |
| Concurrency | Often single process, maybe single thread | Multiple workers, async event loops, threads |
| Reliability | Crashes easily, not self-healing | Designed to handle load and errors gracefully |
| Hot reload | Yes, auto restart on code change | No, must be restarted via process manager |
| Security | Minimal | Configurable limits, timeouts, and isolation |
| Use case | Local development | Staging and production |
You should:
- Use the framework’s dev server or
--reloadmode locally. - Use a real application server, often behind a reverse proxy, in production.
Where Application Servers Fit in the Stack
A typical production web stack looks like this:
- Client
Browser, mobile app, or another service. - Reverse proxy (Nginx / Traefik)
- Terminates HTTPS
- Handles TLS certificates
- Routes requests to the correct backend service
- Can serve static files directly
- Application server (Uvicorn / Gunicorn / Uvicorn + Gunicorn combo)
- Speaks HTTP (or ASGI/WSGI internally)
- Manages worker processes or async event loops
- Runs your application code for each request
- Your application code
- Framework such as FastAPI, Django, Flask
- Contains your business logic
- Databases, caches, external services
- PostgreSQL, Redis, third‑party APIs, etc.
Example flow:
- User hits
https://api.example.com/users. - Nginx receives the encrypted request on port 443, decrypts it, and forwards it (for example) to
http://127.0.0.1:8000. - The application server listens on
127.0.0.1:8000, chooses a worker, and invokes your application. - Your application queries the database, builds a response, and returns it to the server.
- The application server sends the HTTP response back to Nginx, which sends it back to the client.
Concurrency Models in Application Servers
One of the important jobs of an application server is to handle many requests at the same time. There are several models.
1. Single process, single thread
This is the simplest model:
- One process.
- One thread.
- Handles one request at a time.
This is common in simple development servers. It is not suitable for real-world traffic.
2. Multi process
The server starts multiple worker processes:
- Each process is a separate copy of your application.
- Different processes may run on different CPU cores.
- The operating system distributes connections between processes.
Example configuration idea:
- 1 master process.
- 4 worker processes.
Each worker can handle multiple requests over time. If one worker crashes, the master can start a new one.
3. Multi thread
The server starts threads inside a single process:
- One process.
- Many threads.
- Each thread can handle a request.
Threads share memory, so they can be faster to start. But bugs can affect all threads, and some languages have limitations with threads.
4. Asynchronous / event loop
In async servers:
- A single process (or several processes) runs an event loop.
- Each request is represented as a task.
- When a task is waiting for I/O (for example database query, HTTP call), the event loop runs another task instead of blocking.
This is very useful for I/O‑bound web apps, which is common in backend development.
Many modern application servers support combinations, for example:
- Multiple processes, each with an async event loop.
- Threads used for certain blocking tasks.
Rule of thumb: CPUs are good at running multiple processes, and async is good at handling many I/O-bound requests. Application servers usually combine these techniques for better throughput.
Worker Processes and How to Choose Their Number
An application server usually exposes a parameter like --workers to set how many worker processes to run.
A common heuristic:
Recommended worker formula:
For CPU‑bound workloads:
$$ \text{workers} \approx 2 \times \text{CPU\_cores} $$
For I/O‑bound workloads:
$$ \text{workers} \approx 2 \times \text{CPU\_cores} + 1 $$
Adjust based on memory and real-world testing.
Example:
- Your server has 4 CPU cores.
- For an I/O-bound FastAPI service, you might start with
workers = 2 * 4 + 1 = 9workers.
You then:
- Run load tests.
- Monitor CPU usage, memory usage, and response times.
- Increase or decrease worker count.
If you choose too few workers:
- Requests queue up.
- Response time grows under load.
If you choose too many workers:
- Each worker gets less memory.
- The OS spends more time context switching.
- Performance may get worse.
Timeouts, Keep‑Alive, and Connection Handling
Application servers need careful configuration of timeouts and connections.
Typical parameters:
- Request timeout
Maximum time the server waits for the request body to arrive.
Prevents very slow clients from holding connections forever. - Response timeout / worker timeout
Maximum time a worker may spend handling a single request.
Protects you from stuck code or extremely slow database calls. - Keep‑alive timeout
How long to keep the TCP connection open after a response, to reuse for future requests from the same client.
Example situations:
- If your worker timeout is 30 seconds and a database query hangs for 2 minutes, the server will kill the worker and possibly start a new one.
- If your keep‑alive timeout is 5 seconds, the server will close idle connections after 5 seconds of inactivity.
You will often configure timeouts both:
- In the reverse proxy (like Nginx).
- In the application server.
They must be compatible. For instance, if the proxy closes connections after 10 seconds but your app server expects 60 seconds, long responses might be cut off by the proxy.
Graceful Shutdown and Restart
When you deploy new versions or stop your server, you want a graceful shutdown:
- The server stops accepting new connections.
- Existing requests are allowed to finish up to a limit.
- Workers are then stopped.
- Resources such as database connections are cleaned up.
Benefits:
- No interrupted in-flight requests when you deploy.
- Reduced errors during restarts.
Application servers usually:
- Listen for OS signals like
SIGTERMorSIGINT. - Trigger a shutdown procedure that gives workers a grace period.
Example scenario:
- The server has a graceful timeout of 30 seconds.
- You send a
SIGTERM(for example your orchestrator stops the container). - The server stops accepting new requests, and lets current requests finish.
- After 30 seconds, any remaining workers are killed.
For rolling or zero-downtime deployments, you often run two or more instances and restart them one at a time, so there is always at least one healthy instance behind the reverse proxy.
Handling Errors at the Server Level
Your application will raise exceptions, time out, and sometimes crash. A good application server:
- Catches unhandled exceptions, logs them, and returns an HTTP 500 instead of crashing the entire process.
- Uses worker isolation so that a crash in one worker does not affect others.
- Optionally limits request size to avoid memory exhaustion.
- Configures maximum concurrent connections or requests per worker.
Common error-related settings include:
- max request size
For example, refuse bodies larger than 10 MB at the application server level. - max requests per worker
Restart a worker after it has served a certain number of requests.
This can help mitigate memory leaks in application code. - logging level and format
Server logs often include access logs (per request) and error logs.
Example idea:
You configure a server so that:
- Each worker is restarted after 10,000 requests.
- Each request must finish within 30 seconds.
- Each request body must not exceed 2 MB.
If an attacker tries to send a 500 MB POST body, the server can reject the request early, without your application trying to read it all into memory.
Application Servers and Protocols (HTTP, WSGI, ASGI)
Application servers speak two sides:
- External side:
HTTP over TCP connections from clients or a reverse proxy. - Internal side:
A protocol specific to your application type, for example: - WSGI for classic synchronous Python web applications.
- ASGI for asynchronous Python applications and WebSockets.
- Other ecosystems have their own standards.
Your application is usually a callable that the server invokes according to that protocol.
Simplified idea:
- WSGI:
The server calls your app likeresponse = app(environ, start_response). - ASGI:
The server calls an async callable that communicates over send/receive callables.
You rarely need to implement these protocols yourself, but you should understand:
- Some servers support only WSGI.
- Some support only ASGI.
- Some can act as a bridge or support both via different worker classes.
This matters when you choose a server for a specific framework.
Example: Running an Application Server Locally (Conceptual)
Imagine you have a file main.py:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from production-style server"}Using an application server might look like:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4Even without going into Uvicorn specifics yet, note what happens conceptually:
- The application server imports
main, finds theappobject. - It opens a listening socket on
0.0.0.0:8000. - It starts 4 worker processes.
- Each incoming HTTP request is routed to one of the workers.
- The worker runs your FastAPI app to build a response.
- The server sends the response to the client.
If your code crashes in one worker, the server can replace that worker.
Application Servers Behind a Reverse Proxy
Although some application servers can listen directly on the public network interface, in production you usually:
- Bind the application server to a private address, such as
127.0.0.1:8000or10.x.x.x:8000. - Let Nginx or Traefik accept public traffic on ports 80 and 443.
- Let the reverse proxy forward traffic to the application server.
Benefits:
- Reverse proxy handles HTTPS and TLS certificates.
- You can run multiple backend services, each with its own application server, behind a single entry point.
- Extra security by not exposing the application server directly to the internet.
- Easier migration or blue/green deployments by just changing proxy config.
Conceptual Nginx snippet:
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Nginx receives the request, then passes it to your application server at 127.0.0.1:8000.
Monitoring and Observing Application Servers
To keep your backend healthy, you need to observe how your application server behaves.
Useful metrics and logs:
| Metric / Log | Why it matters |
|---|---|
| Requests per second | Shows traffic volume |
| Average and percentile latency | Shows how fast responses are |
| Error rate (5xx responses) | Shows instability |
| Worker count and worker restarts | Detects crashes or memory leaks |
| CPU and memory usage per process | Helps choose worker counts and instance sizes |
Application servers often have:
- Command line options for logging levels.
- Integration points for metrics exporters.
- Signals or endpoints to report health.
You will combine these with external tools for logging and monitoring in later chapters.
Summary
In this chapter you learned what application servers are and why they are critical for production backend systems:
- They sit between your code and the outside world, often behind a reverse proxy.
- They manage worker processes, threads, or async event loops for concurrency.
- They enforce timeouts, limits, and handle errors at the process level.
- They support protocols like WSGI or ASGI to call your application code.
- They enable graceful shutdown, rolling deployments, and better observability.
In the next chapters you will see how to use Uvicorn and Gunicorn specifically as Python application servers, and how to combine them with reverse proxies in real deployments.
Views: 6
KAHIBARO