Python / Uvicorn Fundamentals Interview Questions
1. What is Uvicorn and what problem does it solve for Python web development?
Uvicorn is a lightning-fast ASGI (Asynchronous Server Gateway Interface) web server for Python. It is the production-grade server that runs modern async Python frameworks like FastAPI and Starlette, bridging the gap between the network and the application code.
Before ASGI, Python web servers used the older WSGI (Web Server Gateway Interface) standard - a synchronous protocol that could not natively support async programming, WebSockets, or long-lived connections. Uvicorn implements ASGI, providing a minimal, high-performance server that unlocks the full power of Python's asyncio ecosystem.
| Property | Detail |
|---|---|
| Protocol | ASGI 3.0 |
| HTTP support | HTTP/1.1 |
| WebSocket support | Yes - websockets or wsproto |
| Event loop | asyncio (default) or uvloop |
| HTTP parser | h11 (default) or httptools |
| License | BSD |
| Minimum Python | 3.10+ (as of uvicorn 0.40.0, December 2025) |
Uvicorn is typically used in two ways: directly for development (single process) and as a worker class inside Gunicorn for production multi-process deployments.
2. What is ASGI and how does it differ from WSGI?
WSGI (Web Server Gateway Interface) is the original Python standard for web servers and applications. It is synchronous - each request is handled sequentially, blocking while waiting for I/O. This makes it unsuitable for WebSockets, server-sent events, or highly concurrent async applications.
ASGI (Asynchronous Server Gateway Interface) is the modern successor - an async-native interface that supports concurrent connections, WebSockets, HTTP/2, and long-lived connections via Python's asyncio.
| Feature | WSGI | ASGI |
|---|---|---|
| Execution model | Synchronous (blocking) | Asynchronous (non-blocking) |
| WebSocket support | No | Yes |
| Long-lived connections | No | Yes |
| Python async/await | Not natively | First-class support |
| HTTP/2 | Not natively | Yes (via compatible servers) |
| Example servers | Gunicorn, uWSGI | Uvicorn, Daphne, Hypercorn |
| Example frameworks | Django, Flask | FastAPI, Starlette, Django (3.1+) |
An ASGI application is an async callable that accepts three arguments: scope (connection metadata), receive (async function to receive messages), and send (async function to send messages). This simple interface enables the server to remain decoupled from the application framework.
3. How do you install Uvicorn and what is the difference between the minimal and standard installations?
Uvicorn is available on PyPI and can be installed with pip. There are two installation variants targeting different use cases.
| Variant | Command | What it includes |
|---|---|---|
| Minimal (pure Python) | pip install uvicorn | h11 HTTP parser, asyncio event loop only - no Cython deps |
| Standard (recommended) | pip install uvicorn[standard] | Adds uvloop, httptools, websockets, watchfiles for auto-reload |
# Minimal install - pure Python, good for restricted environments pip install uvicorn # Standard install - Cython-based performance extras pip install "uvicorn[standard]" # Pin a specific version for reproducible deployments pip install "uvicorn[standard]==0.30.1" # Verify installation uvicorn --version # Install in a virtual environment (recommended) python -m venv .venv source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows pip install "uvicorn[standard]"
What the standard extras add:
- uvloop - a Cython-based replacement for asyncio's event loop, providing a significant throughput boost
- httptools - a fast Cython-based HTTP parser (replaces the pure-Python h11)
- websockets - the default WebSocket protocol library
- watchfiles - enables the
--reloadfile-watching feature for development
4. How do you run a basic Uvicorn server from the command line?
Uvicorn is started from the command line by specifying the application in module:attribute format. The module is the Python file (without .py), and the attribute is the ASGI application object inside it.
# File: main.py async def app(scope, receive, send): assert scope['type'] == 'http' await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-type', b'text/plain')], }) await send({ 'type': 'http.response.body', 'body': b'Hello, world!', }) # Run from the terminal: # uvicorn main:app
# Basic run - listens on 127.0.0.1:8000 uvicorn main:app # Custom host and port uvicorn main:app --host 0.0.0.0 --port 8080 # Development mode with auto-reload uvicorn main:app --reload # FastAPI example # File: api.py from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"} # Run FastAPI app uvicorn api:app --reload # Nested attribute path # File: mypackage/server.py with app = FastAPI() uvicorn mypackage.server:app
The application path follows Python import syntax: dots separate packages, the colon separates the module from the attribute. If your ASGI application lives at mypackage/server.py and is named app, you write mypackage.server:app.
5. What are Uvicorn's default host, port, and log level settings?
Uvicorn's defaults are chosen to be safe for local development - binding only to localhost prevents accidental external exposure, while the info log level provides useful startup and access log output without being overwhelming.
| Setting | Default value | Notes |
|---|---|---|
| --host | 127.0.0.1 | Loopback only - not reachable from other machines |
| --port | 8000 | Standard development port |
| --log-level | info | Options: critical, error, warning, info, debug, trace |
| --workers | 1 | Single process - use WEB_CONCURRENCY env var or --workers for more |
| --loop | auto | Prefers uvloop if installed, falls back to asyncio |
| --http | auto | Prefers httptools if installed, falls back to h11 |
| --ws | auto | Prefers websockets if installed, falls back to wsproto |
| --lifespan | auto | Enables lifespan if supported by the application |
| --proxy-headers | enabled | Trusts X-Forwarded-* headers from 127.0.0.1 |
# To make the server externally accessible (development/testing): uvicorn main:app --host 0.0.0.0 # Production: always specify host explicitly uvicorn main:app --host 0.0.0.0 --port 8000 # Check what's running: curl http://127.0.0.1:8000/ # The WEB_CONCURRENCY environment variable sets default worker count export WEB_CONCURRENCY=4 uvicorn main:app # will start 4 workers
6. What is uvloop and how does it improve Uvicorn's performance?
uvloop is a high-performance, drop-in replacement for Python's built-in asyncio event loop. It is implemented in Cython on top of libuv - the same C library used by Node.js - making it significantly faster than the standard asyncio loop for network I/O operations.
| Aspect | asyncio (built-in) | uvloop |
|---|---|---|
| Implementation | Pure Python + C | Cython / libuv (C) |
| Performance | Baseline | ~2-4x faster on I/O-heavy workloads |
| Installation | Built into Python | pip install uvloop (included in uvicorn[standard]) |
| Drop-in replacement | N/A | Yes - same API as asyncio |
| PyPy compatibility | Yes | No - requires CPython |
# Uvicorn automatically uses uvloop when installed # Install via standard extras: pip install "uvicorn[standard]" # Or force uvloop explicitly: uvicorn main:app --loop uvloop # Force asyncio (e.g. for debugging or PyPy): uvicorn main:app --loop asyncio # Check which event loop is active (in your app): import asyncio print(type(asyncio.get_event_loop())) # will show uvloop.Loop when active # Programmatic config: uvicorn.run("main:app", loop="uvloop")
When NOT to use uvloop: uvloop only works with CPython (not PyPy or alternative runtimes). For PyPy deployments, use uvicorn.workers.UvicornH11Worker instead of the standard worker. Also, some libraries that depend on specific asyncio internals may not be compatible with uvloop.
7. What are httptools and h11, and when would you choose each?
Uvicorn supports two HTTP parser implementations, selectable via the --http flag. The default auto setting picks httptools if installed (via the standard extras) or falls back to h11.
| Feature | h11 | httptools |
|---|---|---|
| Implementation | Pure Python (h11 library) | Cython bindings to llhttp (C parser) |
| Performance | Moderate | ~20-30% faster HTTP parsing |
| PyPy support | Yes | No - requires CPython |
| Standards compliance | Very strict - helpful for debugging | Production-grade |
| Error messages | More descriptive | Minimal |
| Install | Always available (default) | pip install httptools (included in uvicorn[standard]) |
# Auto - picks httptools if available (recommended for production) uvicorn main:app --http auto # Force httptools (fast, production) uvicorn main:app --http httptools # Force h11 (pure Python, debugging, PyPy) uvicorn main:app --http h11 # h11 also has a configurable buffer limit: # --limit-max-requests sets max request buffer size (h11 only) uvicorn main:app --http h11 --limit-max-requests 1000
Production recommendation: use httptools for its speed advantage. Use h11 when running on PyPy, in strict compliance testing environments, or when debugging malformed HTTP requests (h11's error messages are more descriptive).
8. How does Uvicorn's --reload flag work and what does it require?
The --reload flag enables automatic server restart when Python source files change. It is designed for development workflows, saving you from manually restarting the server after every code change.
# Basic auto-reload (restarts when any .py file changes) uvicorn main:app --reload # Watch specific directories instead of the entire working directory uvicorn main:app --reload --reload-dir src/ uvicorn main:app --reload --reload-dir src/ --reload-dir tests/ # Include extra file patterns to watch uvicorn main:app --reload --reload-include "*.html" --reload-include "*.css" # Exclude patterns from watching uvicorn main:app --reload --reload-exclude "*.log" # IMPORTANT: --reload and --workers are mutually exclusive! # This will raise an error: # uvicorn main:app --reload --workers 4 ← WRONG
How it works: Uvicorn uses the watchfiles library (installed by uvicorn[standard]) to monitor the filesystem. When a watched file changes, the server process is restarted. Without watchfiles installed, --reload falls back to a slower polling method.
Key constraint: --reload and --workers are mutually exclusive. Reload mode uses a single process with a reloader wrapper - multiple workers are not compatible. For production, disable reload and use multiple workers instead.
9. How do you run Uvicorn programmatically using uvicorn.run() and uvicorn.Config/Server?
Beyond the CLI, Uvicorn can be configured and launched entirely from Python code. This is useful when you need to customise server startup, integrate it into a larger application, or run it from within an existing async event loop.
import uvicorn # Method 1: uvicorn.run() - simplest programmatic start if __name__ == "__main__": uvicorn.run( "main:app", # string import path host="0.0.0.0", port=8000, reload=True, # development only log_level="info", ) # You can also pass the app object directly (no reload support): # uvicorn.run(app, host="0.0.0.0", port=8000) # Method 2: uvicorn.Config + uvicorn.Server - more control import uvicorn if __name__ == "__main__": config = uvicorn.Config("main:app", port=5000, log_level="info") server = uvicorn.Server(config) server.run() # Method 3: Running inside an existing async loop import asyncio import uvicorn async def main(): config = uvicorn.Config("main:app", port=5000, log_level="info") server = uvicorn.Server(config) await server.serve() # use serve() not run() inside async context if __name__ == "__main__": asyncio.run(main())
Important distinction: when passing the app object directly (not a string) to uvicorn.run(), the reload option is not available because the reloader must re-import the module by name. Always use the 'module:attribute' string form when reload=True.
10. What are Uvicorn workers and how do you configure multi-process deployments?
By default Uvicorn runs as a single process with a single event loop. To utilise multiple CPU cores and handle more concurrent requests, you can run multiple worker processes using the --workers flag.
# Run 4 worker processes uvicorn main:app --workers 4 # Workers can also be set via environment variable: export WEB_CONCURRENCY=4 uvicorn main:app # Common formula for worker count: # workers = (2 x CPU cores) + 1 for I/O-bound apps # workers = CPU cores for CPU-bound apps import multiprocessing workers = (2 * multiprocessing.cpu_count()) + 1 # IMPORTANT: --workers and --reload are mutually exclusive
How multi-worker mode works: Uvicorn spawns the specified number of separate OS processes, each running its own event loop and handling requests independently. There is no shared state between workers - each process is completely independent. This is true parallelism (bypasses the GIL) unlike threading.
| Aspect | Single worker (default) | Multiple workers |
|---|---|---|
| CPU utilisation | One core | Multiple cores (true parallelism) |
| Shared state | N/A | Not shared - each worker is isolated |
| Process management | Simple | Use Gunicorn or systemd for production |
| Auto-reload | Supported | Not compatible with --reload |
| Memory | Lower | Each worker has its own memory space |
Production recommendation: for production multi-worker deployments, use Gunicorn with UvicornWorker rather than Uvicorn's built-in --workers. Gunicorn provides superior process lifecycle management, graceful restarts, and health monitoring.
11. How do you deploy Uvicorn with Gunicorn for production?
For production deployments, the recommended pattern is to use Gunicorn as the process manager with Uvicorn as the worker class. Gunicorn provides mature process management (graceful restarts, signal handling, worker health monitoring) while Uvicorn provides the ASGI event loop inside each worker.
# Install the uvicorn-worker package (the uvicorn.workers module is deprecated) pip install uvicorn-worker # Run with Gunicorn + UvicornWorker gunicorn main:app -w 4 -k uvicorn_worker.UvicornWorker # Full production command: gunicorn main:app \ --workers 4 \ --worker-class uvicorn_worker.UvicornWorker \ --bind 0.0.0.0:8000 \ --timeout 120 \ --keep-alive 5 \ --access-logfile - \ --error-logfile - # PyPy-compatible configuration: gunicorn main:app -w 4 -k uvicorn_worker.UvicornH11Worker # Using a gunicorn.conf.py file # gunicorn --config gunicorn.conf.py main:app
# gunicorn.conf.py import multiprocessing bind = "0.0.0.0:8000" workers = (2 * multiprocessing.cpu_count()) + 1 worker_class = "uvicorn_worker.UvicornWorker" worker_connections = 1000 max_requests = 10000 max_requests_jitter = 1000 timeout = 120 graceful_timeout = 30 keepalive = 5
Note on the deprecated module: the uvicorn.workers module is deprecated and will be removed in a future release. The replacement is the standalone uvicorn-worker package (pip install uvicorn-worker). Update your -k flag from uvicorn.workers.UvicornWorker to uvicorn_worker.UvicornWorker.
12. How do you configure SSL/TLS (HTTPS) in Uvicorn?
Uvicorn supports HTTPS natively by accepting a certificate and private key. This is useful for local development with HTTPS or for simple single-server deployments where a reverse proxy is not involved.
# Run with SSL (requires certificate and private key) uvicorn main:app \ --ssl-keyfile ./key.pem \ --ssl-certfile ./cert.pem \ --host 0.0.0.0 \ --port 443 # With a CA bundle (for mutual TLS / client certificates) uvicorn main:app \ --ssl-keyfile ./key.pem \ --ssl-certfile ./cert.pem \ --ssl-ca-certs ./ca-bundle.pem # Programmatic SSL config import ssl import uvicorn if __name__ == "__main__": uvicorn.run( "main:app", ssl_keyfile="./key.pem", ssl_certfile="./cert.pem", host="0.0.0.0", port=443, ) # Local development HTTPS with mkcert: # 1. Install mkcert: brew install mkcert # 2. mkcert -install # 3. mkcert localhost 127.0.0.1 # 4. uvicorn main:app --ssl-keyfile localhost-key.pem --ssl-certfile localhost.pem
Production approach: in most production architectures, SSL termination is handled by a reverse proxy (Nginx, Caddy, or a cloud load balancer) rather than by Uvicorn directly. The reverse proxy decrypts HTTPS traffic and forwards plain HTTP to Uvicorn. This pattern improves performance and simplifies certificate management.
13. What is the ASGI lifespan protocol and how does Uvicorn support it?
The ASGI lifespan protocol provides startup and shutdown events for an ASGI application, allowing it to initialise resources (database connections, caches, background tasks) before serving requests and clean them up on shutdown.
# FastAPI lifespan example (modern approach) from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app: FastAPI): # Startup: runs before the server begins accepting requests print("Starting up...") app.state.db = await connect_to_database() yield # server is running here # Shutdown: runs after the server stops accepting requests print("Shutting down...") await app.state.db.close() app = FastAPI(lifespan=lifespan) # Pure ASGI lifespan implementation async def app(scope, receive, send): if scope["type"] == "lifespan": while True: message = await receive() if message["type"] == "lifespan.startup": # do startup work await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown": # do cleanup await send({"type": "lifespan.shutdown.complete"}) return # ... HTTP handling
| --lifespan value | Behaviour |
|---|---|
| auto (default) | Enable lifespan if the application supports it; ignore if not |
| on | Always enable lifespan - fails at startup if the app does not support it |
| off | Never run lifespan events - useful for testing or apps that don't need it |
The lifespan protocol is especially important for managing async resources like database connection pools (asyncpg, motor), which must be created inside an async context and properly closed to avoid connection leaks.
14. How do you configure Uvicorn logging and access logs?
Uvicorn uses Python's standard logging module with configurable log levels and optional access logging. Log configuration can be provided via CLI flags, environment variables, or a config file.
# Log level options: critical, error, warning, info (default), debug, trace uvicorn main:app --log-level debug # Disable access log (reduces CPU by ~10% under high load) uvicorn main:app --no-access-log # Disable colour in logs (useful for log aggregators) uvicorn main:app --no-use-colors # Use a custom logging config file (JSON, YAML, or INI) uvicorn main:app --log-config logging.yaml # Programmatic log config: uvicorn.run("main:app", log_level="warning", access_log=False) # Production hardened logging: uvicorn main:app \ --log-level warning \ --no-access-log \ --no-use-colors
# logging.yaml - custom Uvicorn log config version: 1 formatters: default: format: "%(asctime)s %(levelname)s %(message)s" use_colors: false access: format: '%(asctime)s %(client_addr)s - "%(request_line)s" %(status_code)s' handlers: default: class: logging.StreamHandler formatter: default access: class: logging.StreamHandler formatter: access loggers: uvicorn: handlers: [default] level: INFO uvicorn.access: handlers: [access] level: INFO propagate: false
Performance tip: disabling access logs (--no-access-log) and setting --log-level warning can reduce CPU usage by up to 10-15% under high concurrency, since log formatting and I/O for every request is eliminated. In production, access logging is often handled at the reverse proxy layer (Nginx/Caddy) instead.
15. What are proxy headers in Uvicorn and how do you configure trusted proxies?
When Uvicorn runs behind a reverse proxy (Nginx, Caddy, a load balancer), the proxy forwards requests on behalf of the original client. Without proxy header support, Uvicorn would see the proxy's IP as the client address and the internal scheme (http) rather than the original (https). The --proxy-headers flag tells Uvicorn to trust X-Forwarded-For and X-Forwarded-Proto headers from trusted proxies.
# Enable proxy headers (enabled by default, trusts only 127.0.0.1) uvicorn main:app --proxy-headers # Trust a specific proxy IP uvicorn main:app --proxy-headers --forwarded-allow-ips 10.0.0.1 # Trust multiple proxy IPs or a subnet uvicorn main:app \ --proxy-headers \ --forwarded-allow-ips "10.0.0.0/8,172.16.0.0/12" # Trust ALL proxies (use ONLY on trusted internal networks!) uvicorn main:app --proxy-headers --forwarded-allow-ips "*" # Disable proxy headers (if no reverse proxy in front) uvicorn main:app --no-proxy-headers # Set via environment variable: export FORWARDED_ALLOW_IPS="10.0.0.1,10.0.0.2" uvicorn main:app --proxy-headers
Security warning: setting --forwarded-allow-ips "*" trusts ALL connecting IPs to set forwarded headers. This means a malicious client could spoof their IP address by sending a fake X-Forwarded-For header. Only trust IPs of known, controlled proxy servers.
| Setting | When to use |
|---|---|
| --forwarded-allow-ips 127.0.0.1 (default) | Reverse proxy on the same machine |
| --forwarded-allow-ips | Known proxy IP (e.g. internal load balancer) |
| --forwarded-allow-ips "*" | Trust everything - only on fully controlled internal networks |
| --no-proxy-headers | Uvicorn exposed directly (no reverse proxy) |
16. How does Uvicorn handle WebSocket connections?
Uvicorn has native WebSocket support, making it suitable for real-time applications like chat, live dashboards, and collaborative tools. WebSocket connections are handled via the websockets or wsproto library.
# Pure ASGI WebSocket application async def app(scope, receive, send): if scope["type"] == "websocket": # Accept the WebSocket connection await send({"type": "websocket.accept"}) while True: event = await receive() if event["type"] == "websocket.receive": data = event.get("text") or event.get("bytes") # Echo the message back await send({"type": "websocket.send", "text": data}) elif event["type"] == "websocket.disconnect": break # FastAPI WebSocket example from fastapi import FastAPI, WebSocket app = FastAPI() @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() while True: data = await websocket.receive_text() await websocket.send_text(f"Echo: {data}")
| Flag | Default | Description |
|---|---|---|
| --ws | auto | WebSocket implementation: auto, websockets, websockets-sansio, wsproto |
| --ws-max-size | 16777216 (16MB) | Maximum WebSocket message size in bytes |
| --ws-max-queue | 32 | Maximum WebSocket message queue length |
| --ws-ping-interval | 20.0s | Interval between server ping frames |
| --ws-ping-timeout | 20.0s | Timeout waiting for pong response |
| --ws-per-message-deflate | True | Enable per-message compression |
Note: the WSGI interface (--interface wsgi) always disables WebSocket support because WSGI does not define a WebSocket protocol. Use the ASGI interface for WebSocket-capable applications.
17. What is the --root-path setting in Uvicorn and when do you need it?
The --root-path setting tells Uvicorn (and consequently your ASGI application) that it is mounted at a specific URL path prefix. This is needed when your application is served at a sub-path behind a reverse proxy - for example, your app is at https://example.com/api/v1/ rather than https://example.com/.
# Without root-path: app thinks it lives at / # Nginx config: location /api/v1/ { proxy_pass http://127.0.0.1:8000; } uvicorn main:app # scope["root_path"] == "" # With root-path: app knows it is mounted at /api/v1 uvicorn main:app --root-path /api/v1 # scope["root_path"] == "/api/v1" # FastAPI uses root_path for generating correct OpenAPI docs URLs # Without it, /docs links will be wrong when behind a proxy from fastapi import FastAPI app = FastAPI(root_path="/api/v1") # set in the app OR in Uvicorn CLI # Programmatic: uvicorn.run("main:app", root_path="/api/v1")
Why it matters in FastAPI: FastAPI uses the ASGI root_path to generate the correct URLs in its OpenAPI (Swagger) documentation. Without setting it, the generated API docs will have incorrect base URLs when the app is served from a sub-path, causing all Try it out requests in Swagger UI to fail.
18. How do you use Uvicorn with a Unix domain socket instead of a TCP port?
Instead of binding to a TCP host:port, Uvicorn can listen on a Unix domain socket (UDS) - a special file on the filesystem used for inter-process communication. This is faster than TCP loopback for communication between processes on the same machine.
# Bind to a Unix domain socket uvicorn main:app --uds /tmp/uvicorn.sock # Programmatic: uvicorn.run("main:app", uds="/tmp/uvicorn.sock") # Nginx config to proxy to the Unix socket: # upstream app { # server unix:/tmp/uvicorn.sock; # } # server { # location / { # proxy_pass http://app; # } # } # Bind to a file descriptor (for socket activation, e.g. systemd): uvicorn main:app --fd 3 # systemd passes the pre-bound socket as fd 3
| Option | Flag | Use case |
|---|---|---|
| TCP host:port | --host + --port | Default; works everywhere |
| Unix domain socket | --uds /path/to/file.sock | Same-machine reverse proxy; slightly faster than TCP loopback |
| File descriptor | --fd | Systemd socket activation; container orchestrators |
Performance: Unix domain sockets bypass the TCP/IP stack entirely for same-machine communication, reducing latency. The improvement is measurable but modest - typically a few percent. The main practical benefit is avoiding port number management when running multiple services on the same host.
19. What is the --factory flag in Uvicorn and when is it useful?
The --factory flag tells Uvicorn to treat the specified application path as a factory function - a callable that takes no arguments and returns an ASGI application - rather than as the application itself.
# Without --factory: APP is the application object directly uvicorn main:app # With --factory: create_app() is called to produce the ASGI app uvicorn main:create_app --factory # Example factory function: # main.py def create_app(): from fastapi import FastAPI app = FastAPI() @app.get("/") def root(): return {"status": "ok"} return app # The factory is called with no arguments and must return the ASGI app # Programmatic equivalent: uvicorn.run("main:create_app", factory=True, port=8000) # Why use a factory? # - Deferred initialisation (resources created only when workers start) # - Different config per environment (testing vs production) # - Integration with dependency injection frameworks # - Worker-level initialisation (each Gunicorn worker calls the factory separately)
Key use case - Gunicorn + factory: when using Gunicorn with multiple workers, each worker process calls the factory independently. This is important for resources that must be created per-process (like database connection pools) - a factory guarantees each worker initialises its own pool rather than sharing one created before forking.
20. How do you configure Uvicorn with environment variables?
Most Uvicorn CLI flags can be set via UVICORN_* prefixed environment variables, making it easy to configure in containerised environments and CI/CD pipelines without changing command-line arguments.
# Environment variable equivalents of CLI flags export UVICORN_HOST=0.0.0.0 export UVICORN_PORT=8000 export UVICORN_WORKERS=4 export UVICORN_LOG_LEVEL=warning export UVICORN_LOOP=uvloop export UVICORN_HTTP=httptools export UVICORN_RELOAD=false export UVICORN_NO_ACCESS_LOG=true export UVICORN_PROXY_HEADERS=true export UVICORN_FORWARDED_ALLOW_IPS="10.0.0.1" # Then run with minimal flags: uvicorn main:app # Partial override: env vars + CLI flags (CLI takes precedence) export UVICORN_PORT=8000 uvicorn main:app --port 9000 # uses 9000 (CLI overrides env var) # WEB_CONCURRENCY is also respected for workers: export WEB_CONCURRENCY=4 uvicorn main:app # NOTE: UVICORN_* env vars cannot be set inside --env-file # --env-file is for configuring your APPLICATION, not Uvicorn itself
Important distinction - --env-file: the --env-file flag loads environment variables into the application process, but UVICORN_* prefixed settings in that file are ignored. The --env-file is intended for your application's configuration (e.g. DATABASE_URL), not for Uvicorn's own settings.
21. What is Uvicorn's --interface flag and what application types does it support?
The --interface flag tells Uvicorn which application protocol to expect. The default auto setting detects the interface automatically, but you can force a specific mode.
| Value | Interface | WebSocket support | Notes |
|---|---|---|---|
| auto (default) | Auto-detect ASGI3, ASGI2, or WSGI | If ASGI | Recommended for most cases |
| asgi3 | ASGI 3.0 (current standard) | Yes | Explicit ASGI3 |
| asgi2 | ASGI 2.0 (legacy double-callable) | Yes | Legacy - rarely needed |
| wsgi | WSGI (synchronous) | No | Always disables WebSocket support |
# Auto-detect (default - recommended) uvicorn main:app --interface auto # Explicit ASGI3 (FastAPI, Starlette, modern frameworks) uvicorn main:app --interface asgi3 # WSGI mode (for Django without channels, Flask) uvicorn main:app --interface wsgi # WARNING: WSGI mode disables WebSocket support entirely # Programmatic: uvicorn.run("main:app", interface="asgi3")
WSGI mode deprecation note: Uvicorn's native WSGI implementation is deprecated. For running WSGI applications through Uvicorn, you should use the a2wsgi adapter (pip install a2wsgi) which wraps your WSGI app in an ASGI-compatible interface. This approach keeps the full ASGI pipeline while supporting legacy WSGI apps.
22. How do you run Uvicorn inside a Docker container?
Uvicorn is widely used in containerised deployments. The key considerations are binding to the right host, setting the correct number of workers, and choosing between running Uvicorn directly or via Gunicorn.
# Dockerfile - single-container Uvicorn FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Expose the port EXPOSE 8000 # Run Uvicorn - bind 0.0.0.0 so Docker can forward traffic CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--loop", "uvloop", "--http", "httptools", "--log-level", "info", "--no-access-log"] # requirements.txt: # uvicorn[standard] # fastapi
# docker-compose.yml services: api: build: . ports: - "8000:8000" environment: - UVICORN_HOST=0.0.0.0 - UVICORN_PORT=8000 - UVICORN_WORKERS=4 - UVICORN_LOG_LEVEL=info - WEB_CONCURRENCY=4 command: uvicorn main:app # Health check - uvicorn has no built-in health endpoint # Add one to your application: # @app.get("/health") # def health(): return {"status": "ok"}
Key Docker-specific setting: always use --host 0.0.0.0 inside Docker containers. The default 127.0.0.1 binds to the container's loopback interface only, which means Docker's port forwarding cannot reach it - the service will be unreachable from outside the container.
23. What is the difference between Uvicorn, Hypercorn, and Daphne as ASGI servers?
Several ASGI server implementations exist. Uvicorn is the most widely used, but Hypercorn and Daphne are also production-ready alternatives with different strengths.
| Feature | Uvicorn | Hypercorn | Daphne |
|---|---|---|---|
| HTTP/1.1 | Yes | Yes | Yes |
| HTTP/2 | No | Yes | Yes |
| HTTP/3 | No | Yes | No |
| WebSockets | Yes | Yes | Yes |
| Python async | asyncio / uvloop | asyncio / uvloop / trio | asyncio |
| Process management | Via Gunicorn | Built-in | Via Gunicorn |
| Origin | Encode (Tom Christie) | Pallets project | Django/Channels team |
| Best for | FastAPI, Starlette, general ASGI | HTTP/2 needs, Quart apps | Django Channels |
When to choose each:
- Uvicorn - the default choice for FastAPI and Starlette; best performance for HTTP/1.1 + WebSocket workloads; most documentation and community support
- Hypercorn - choose when you need HTTP/2 or HTTP/3 support natively (without a proxy layer), or if your app uses the Quart framework (Hypercorn was originally Quart's server)
- Daphne - the original ASGI server; choose for Django Channels applications where it has the best compatibility
24. How do you handle graceful shutdown in Uvicorn?
Uvicorn handles OS signals to shut down gracefully - completing in-flight requests before stopping. Understanding this is important for zero-downtime deployments and container orchestration.
# Uvicorn responds to these OS signals: # SIGINT (Ctrl+C) - graceful shutdown # SIGTERM (kill default) - graceful shutdown (Kubernetes uses this) # SIGQUIT - graceful shutdown with core dump # SIGKILL (kill -9) - immediate termination (not catchable) # Kubernetes sends SIGTERM before SIGKILL: # Pod termination sequence: # 1. SIGTERM sent to container # 2. Uvicorn begins graceful shutdown # 3. Completes in-flight requests # 4. SIGKILL sent after terminationGracePeriodSeconds (default 30s) # Programmatic graceful shutdown: import asyncio import signal import uvicorn async def main(): config = uvicorn.Config("main:app", port=8000) server = uvicorn.Server(config) loop = asyncio.get_running_loop() loop.add_signal_handler( signal.SIGTERM, lambda: asyncio.create_task(server.shutdown()) ) await server.serve() asyncio.run(main())
Lifespan shutdown: during graceful shutdown, Uvicorn sends the lifespan.shutdown event to the application before terminating. This is where you should close database connections, flush caches, and clean up resources. Ensure your lifespan shutdown handler completes quickly - a slow shutdown can cause Kubernetes to send SIGKILL prematurely.
25. What are the key Uvicorn settings for a production-hardened deployment?
Development defaults are convenient but inappropriate for production. A hardened production configuration disables debug features, removes identifying headers, tunes performance, and restricts network exposure.
# Production-hardened Uvicorn command uvicorn main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 4 \ --loop uvloop \ --http httptools \ --log-level warning \ --no-access-log \ --no-server-header \ --no-date-header \ --proxy-headers \ --forwarded-allow-ips "10.0.0.0/8" # Or using environment variables (recommended for containers): export UVICORN_HOST=0.0.0.0 export UVICORN_PORT=8000 export UVICORN_WORKERS=4 export UVICORN_LOOP=uvloop export UVICORN_HTTP=httptools export UVICORN_LOG_LEVEL=warning export UVICORN_NO_ACCESS_LOG=true export UVICORN_NO_SERVER_HEADER=true export UVICORN_PROXY_HEADERS=true export UVICORN_FORWARDED_ALLOW_IPS="10.0.0.0/8" uvicorn main:app
| Setting | Reason |
|---|---|
| --no-server-header | Hides 'uvicorn' from Server response header - reduces attack surface |
| --no-date-header | Slightly reduces response size; date often added by reverse proxy |
| --log-level warning | Eliminates per-request log I/O - reduces CPU by ~10% under load |
| --no-access-log | Same reason; access logging often better done at proxy layer |
| --no-reload | Never use reload in production - it single-processes and slows startup |
| --loop uvloop | 2-4x event loop throughput improvement |
| --http httptools | Faster HTTP parsing than h11 |
For maximum reliability in production, pair with Gunicorn (pip install uvicorn-worker) for process supervision, and a reverse proxy (Nginx, Caddy) for SSL termination and rate limiting.
26. How do you run Uvicorn with systemd for automatic restarts and log rotation?
On Linux servers without Docker, systemd is the standard way to run Uvicorn as a managed service - providing automatic restarts on failure, boot-time startup, and journal-based logging.
# /etc/systemd/system/myapp.service [Unit] Description=My FastAPI Application After=network.target [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/var/www/myapp Environment="PATH=/var/www/myapp/.venv/bin" EnvironmentFile=/var/www/myapp/.env ExecStart=/var/www/myapp/.venv/bin/uvicorn main:app \ --host 127.0.0.1 \ --port 8000 \ --workers 4 \ --loop uvloop \ --log-level warning Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
# Enable and start the service sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp # Check status and logs sudo systemctl status myapp journalctl -u myapp -f # follow live logs journalctl -u myapp --since "1 hour ago" # Reload after config change sudo systemctl restart myapp # Graceful restart (sends SIGTERM then restarts) sudo systemctl reload-or-restart myapp # Socket activation (advanced - systemd creates the socket) # myapp.socket + myapp.service - Uvicorn started only when first connection arrives uvicorn main:app --fd 0 # fd 0 passed by systemd socket activation
Bind Uvicorn to 127.0.0.1 (loopback) in systemd deployments and use Nginx or Caddy as the front-facing reverse proxy. This keeps Uvicorn off the public network and lets the proxy handle SSL, rate limiting, and static files.
27. What is the relationship between Uvicorn and FastAPI?
FastAPI and Uvicorn are complementary tools at different layers of the stack. FastAPI is the ASGI application framework; Uvicorn is the ASGI server that runs it. FastAPI generates the HTTP routing, request validation, and response serialisation; Uvicorn handles the network I/O, event loop, and connection lifecycle.
| Responsibility | FastAPI | Uvicorn |
|---|---|---|
| HTTP routing | Yes | No |
| Request validation (Pydantic) | Yes | No |
| Response serialisation | Yes | No |
| WebSocket routing | Yes | No |
| Network I/O (sockets) | No | Yes |
| Event loop management | No | Yes |
| Process management | No | Partial (--workers) |
| SSL termination | No | Yes (or reverse proxy) |
# main.py - FastAPI application from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"} # Uvicorn runs the FastAPI app: # uvicorn main:app --reload # FastAPI is itself an ASGI-compliant application: # It implements: async def __call__(self, scope, receive, send) # Uvicorn calls this callable for every incoming request # Development: # uvicorn main:app --reload --host 127.0.0.1 --port 8000 # Production: # gunicorn main:app -w 4 -k uvicorn_worker.UvicornWorker
Installation: pip install fastapi uvicorn[standard] gives you both. FastAPI's documentation recommends Uvicorn as the server, and Uvicorn's documentation uses FastAPI in examples - they are tightly coupled in the ecosystem though architecturally independent (any ASGI framework can run on Uvicorn).
28. How do you configure Uvicorn's timeout settings?
Uvicorn provides several timeout parameters that control how long the server waits for client activity and worker health before taking action.
| Setting | Default | Purpose |
|---|---|---|
| --timeout-keep-alive | 5s | Seconds to wait for a new request on a keep-alive connection before closing |
| --timeout-notify | 30s | Seconds to notify worker processes during graceful shutdown before sending SIGKILL |
| --timeout-graceful-shutdown | None (no limit) | Maximum seconds for the server to wait for ongoing requests to complete during shutdown |
| --timeout-worker-healthcheck | 5s | Seconds to wait for a worker to respond to a health check |
# Configure keep-alive timeout (how long idle connections stay open) uvicorn main:app --timeout-keep-alive 10 # Graceful shutdown timeout (force-kill after N seconds if requests don't finish) uvicorn main:app --timeout-graceful-shutdown 30 # Worker health check timeout uvicorn main:app --timeout-worker-healthcheck 10 # Programmatic: uvicorn.run( "main:app", timeout_keep_alive=10, timeout_graceful_shutdown=30, ) # Gunicorn has its own timeout settings that overlay Uvicorn's: # gunicorn --timeout 120 (worker timeout for handling a request) # gunicorn --graceful-timeout 30 (graceful shutdown window)
Keep-alive tuning: the default 5-second keep-alive timeout is appropriate for most use cases. For high-throughput APIs with many short-lived requests, a lower value (2-3s) reduces idle connections. For APIs with slower clients or large payloads, a higher value reduces connection setup overhead.
29. How do you write a minimal ASGI application that works with Uvicorn without any framework?
Since Uvicorn implements ASGI, any Python callable that follows the ASGI specification can be run with it - no framework required. Understanding the raw ASGI interface deepens your understanding of how frameworks like FastAPI and Starlette work under the hood.
# main.py - minimal HTTP ASGI app async def app(scope, receive, send): assert scope["type"] == "http" # Read the request body body = b"" more_body = True while more_body: message = await receive() body += message.get("body", b"") more_body = message.get("more_body", False) # Send the HTTP response response_body = b"Hello, world!" await send({ "type": "http.response.start", "status": 200, "headers": [ (b"content-type", b"text/plain"), (b"content-length", str(len(response_body)).encode()), ], }) await send({ "type": "http.response.body", "body": response_body, }) # Run: uvicorn main:app
# Streaming response - send body in multiple chunks async def streaming_app(scope, receive, send): assert scope["type"] == "http" await send({ "type": "http.response.start", "status": 200, "headers": [(b"content-type", b"text/plain")], }) for chunk in [b"Hello", b", ", b"world!"]: await send({ "type": "http.response.body", "body": chunk, "more_body": True, # indicates more chunks are coming }) await send({"type": "http.response.body", "body": b"", "more_body": False})
The ASGI interface separates response start (status code + headers) from response body. This two-message design enables streaming - you can start sending the response before the full body is ready, which is essential for real-time and large-file use cases.
30. How do you configure Uvicorn's log config file for custom logging formats?
Uvicorn's --log-config flag accepts a logging configuration in JSON or YAML format (using Python's dictConfig) or in INI format (fileConfig). This enables structured logging, custom formats, and routing logs to different handlers.
# logging.json - custom log config in dictConfig format { "version": 1, "disable_existing_loggers": false, "formatters": { "default": { "()": "uvicorn.logging.DefaultFormatter", "fmt": "%(levelprefix)s %(asctime)s %(message)s", "use_colors": false }, "access": { "()": "uvicorn.logging.AccessFormatter", "fmt": '%(levelprefix)s %(asctime)s %(client_addr)s - "%(request_line)s" %(status_code)s', "use_colors": false } }, "handlers": { "default": { "formatter": "default", "class": "logging.StreamHandler", "stream": "ext://sys.stderr" }, "access": { "formatter": "access", "class": "logging.StreamHandler", "stream": "ext://sys.stdout" } }, "loggers": { "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": false}, "uvicorn.error": {"level": "INFO"}, "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": false} } }
# Use the custom log config: uvicorn main:app --log-config logging.json # Or YAML format (requires PyYAML - included in uvicorn[standard]): uvicorn main:app --log-config logging.yaml # Programmatically: import logging.config, json with open("logging.json") as f: log_config = json.load(f) uvicorn.run("main:app", log_config=log_config)
To override colour settings in the log config, set formatters.default.use_colors and formatters.access.use_colors to true or false explicitly. This takes precedence over auto-detection.
31. What is the --app-dir flag in Uvicorn and when do you need it?
The --app-dir flag adds a specific directory to sys.path before Uvicorn tries to import the application. This is useful when your application module is not in the current working directory or the standard Python path.
# Project structure: # /project/ # src/ # myapp/ # main.py ← app lives here # Without --app-dir: run from /project/src/ cd /project/src uvicorn myapp.main:app # With --app-dir: run from /project/ cd /project uvicorn myapp.main:app --app-dir src # Equivalent to: PYTHONPATH=/project/src uvicorn myapp.main:app # Docker example - app in /app/src CMD ["uvicorn", "myapp.main:app", "--app-dir", "src", "--host", "0.0.0.0", "--port", "8000"] # Programmatic: uvicorn.run("myapp.main:app", app_dir="src") # Multiple app directories: use PYTHONPATH env var instead export PYTHONPATH=/project/src:/project/lib uvicorn myapp.main:app
Common use case: projects using a src/ layout (where application code lives in src/mypackage/ rather than the project root) need either --app-dir src or a proper package installation (pip install -e .). The --app-dir approach is simpler for containerised deployments where installation may be skipped.
32. How do you test a Uvicorn-served ASGI application without starting the server?
For unit and integration testing, you don't want to actually start a server and make HTTP requests. The httpx library's ASGITransport (or Starlette's TestClient) allows you to test ASGI applications in-process - no socket, no port, no server startup required.
# Using httpx.AsyncClient with ASGITransport (pure async test) import pytest import httpx from main import app @pytest.mark.anyio async def test_homepage(): async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} # Using Starlette / FastAPI TestClient (sync interface, uses anyio) from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_root(): response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} # Testing lifespan events with TestClient def test_with_lifespan(): with TestClient(app) as client: # lifespan startup has run at this point response = client.get("/") assert response.status_code == 200 # lifespan shutdown has run after the with block
Why this works: ASGI applications are just async callables - they can be called directly in tests without a server. The transport layer (httpx or Starlette's TestClient) converts Python test calls into ASGI scope/receive/send messages, running the application synchronously in the test process.
33. What is Granian and how does it compare to Uvicorn as an ASGI server alternative?
Granian is a newer ASGI-compatible HTTP server written in Rust using Tokio and hyper. It is designed as a high-performance alternative to Uvicorn, with native HTTP/2, TLS, and WebSocket support baked in.
| Feature | Uvicorn | Granian |
|---|---|---|
| Language | Python (Cython extensions) | Rust |
| HTTP/1.1 | Yes | Yes |
| HTTP/2 | No | Yes (built-in) |
| TLS/SSL | Yes | Yes (native) |
| WebSockets | Yes | Yes |
| Process management | Via Gunicorn | Built-in (--workers flag) |
| Python version | 3.10+ | 3.8+ |
| Maturity | Very mature | Newer - actively developed |
| Ecosystem | Huge - FastAPI, Starlette, etc. | ASGI-compatible with same frameworks |
# Install Granian pip install granian # Run the same FastAPI/Starlette/ASGI app: granian --interface asgi main:app # With HTTP/2 + TLS: granian --interface asgi main:app \ --host 0.0.0.0 \ --port 443 \ --ssl-certificate cert.pem \ --ssl-keyfile key.pem \ --http2 # Workers: granian --interface asgi main:app --workers 4
When to consider Granian: if you need native HTTP/2 without a reverse proxy, or want to explore Rust-speed performance for your ASGI app. For most production use cases, Uvicorn (with Gunicorn) remains the default choice due to its maturity, documentation, and ecosystem support. Granian is noted in Uvicorn's own documentation as a Rust-based ASGI alternative.
34. What is the scope dictionary in the ASGI protocol and what does it contain?
The scope dictionary is the first argument passed to an ASGI application callable. It contains metadata about the incoming connection - its type (HTTP, WebSocket, or lifespan) and all relevant connection details.
# Inspecting the scope in an ASGI app async def app(scope, receive, send): print(scope["type"]) # "http", "websocket", or "lifespan" if scope["type"] == "http": print(scope["method"]) # "GET", "POST", etc. print(scope["path"]) # "/api/users" print(scope["query_string"]) # b"page=1&limit=10" print(scope["headers"]) # list of (name, value) byte tuples print(scope["client"]) # ("127.0.0.1", 54321) - IP and port print(scope["server"]) # ("127.0.0.1", 8000) print(scope["scheme"]) # "http" or "https" print(scope["root_path"]) # e.g. "/api/v1" if --root-path is set print(scope["http_version"]) # "1.1" elif scope["type"] == "websocket": print(scope["path"]) # WebSocket path print(scope["headers"]) # Upgrade headers print(scope["subprotocols"]) # Requested sub-protocols elif scope["type"] == "lifespan": pass # handle startup and shutdown events
| Field | Type | Example value |
|---|---|---|
| type | str | "http" |
| method | str | "GET" |
| path | str | '/api/users/42' |
| query_string | bytes | b'page=1&limit=10' |
| headers | list of tuples | [(b'content-type', b'application/json')] |
| client | tuple or None | ('127.0.0.1', 54321) |
| scheme | str | "https" |
| root_path | str | '/api/v1' |
35. What are best practices for deploying Uvicorn in a Kubernetes environment?
Kubernetes brings its own deployment patterns that intersect with Uvicorn's configuration. Getting the combination right ensures zero-downtime deployments, proper health checking, and correct signal handling.
| Concern | Recommendation |
|---|---|
| Host binding | Always --host 0.0.0.0 so Kubernetes can route traffic to pods |
| Workers | Set workers based on pod CPU limits - usually 1-2 per vCPU for I/O-bound apps |
| Health checks | Implement /health and /ready endpoints; Uvicorn has no built-in health endpoint |
| Graceful shutdown | SIGTERM triggers Uvicorn graceful shutdown; set terminationGracePeriodSeconds > timeout-graceful-shutdown |
| Preemptive SIGKILL | If Uvicorn doesn't finish within terminationGracePeriodSeconds, Kubernetes force-kills |
| Environment config | All Uvicorn settings via UVICORN_* env vars in ConfigMap/Secret |
| Liveness vs readiness | Liveness: is the server running? Readiness: is it ready to serve traffic? |
# Kubernetes Deployment snippet spec: containers: - name: api image: myapp:latest command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--timeout-graceful-shutdown", "25"] ports: - containerPort: 8000 env: - name: UVICORN_LOG_LEVEL value: "warning" livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 2 periodSeconds: 5 lifecycle: preStop: exec: command: ["sleep", "5"] # allow load balancer to de-register
Pre-stop hook: adding a preStop sleep of 5 seconds gives Kubernetes time to remove the pod from the service endpoint list before SIGTERM is sent. This prevents new requests from being routed to a pod that is already shutting down.
