Deployment

Responder applications are standard ASGI apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor to WSGI — it supports async, WebSockets, and HTTP/2. This means you can deploy a Responder app anywhere that runs Python, using any ASGI server.

Running Locally

During development, api.run() is all you need:

if __name__ == "__main__":
    api.run()

This starts a uvicorn server on 127.0.0.1:5042. Uvicorn is a lightning-fast ASGI server built on uvloop — it handles thousands of concurrent connections efficiently and protects against slowloris attacks, making a reverse proxy like nginx optional for many deployments.

Responder can also run the current app with Granian when the optional server extra is installed:

$ uv pip install 'responder[orjson,server]'

if __name__ == "__main__":
    api.run(server="granian")

If the extra is missing, Responder raises an error with the install command. This embedded Granian path is single-process; use Granian directly for multi-worker production deployments.

Docker

Docker is the most common way to package and deploy web applications. Here’s a minimal Dockerfile:

FROM python:3.13-slim
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY . .
RUN uv pip install --system 'responder[orjson]'
ENV PORT=80
EXPOSE 80
CMD ["python", "api.py"]

Build and run:

$ docker build -t myapi .
$ docker run -p 8000:80 myapi

The python:3.13-slim image is about 150MB — small enough for fast deploys but includes everything you need. Using uv for installs is significantly faster than pip. For even smaller images, you can use python:3.13-alpine, though some packages may need extra build dependencies.

Cloud Platforms

Responder automatically honors the PORT environment variable. When PORT is set and you do not pass an explicit port=, the server binds to 0.0.0.0 on that port — this is the convention that virtually every cloud platform uses.

This means zero configuration on:

  • Fly.iofly launch and you’re done

  • Railway — push your code, Railway sets PORT

  • Render — set start command to python api.py

  • Google Cloud Run — containerize and deploy

  • Azure Container Apps — same pattern

  • AWS App Runner — and here too

The pattern is always the same: deploy your code, set the start command to python api.py, and the platform handles the rest.

Health Check Endpoint

Every production deployment needs a health check — a lightweight endpoint that monitoring tools, load balancers, and orchestrators can poll to verify your service is running:

@api.route("/health", include_in_schema=False)
def health(req, resp):
    resp.media = {"status": "healthy"}

Keep it simple. Don’t query the database or do expensive work — the health check should return instantly. Cloud platforms, Docker, and Kubernetes all look for an HTTP 200 to confirm your service is alive.

Responder generates your OpenAPI schema from route signatures, so every route is documented automatically. include_in_schema=False keeps this internal endpoint out of the public spec.

For Docker, add a HEALTHCHECK instruction:

HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost/health || exit 1

Uvicorn Directly

For production deployments where you want more control, bypass api.run() and use uvicorn directly:

$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4

The --workers flag spawns multiple processes, each handling requests independently. A good starting point is 2-4 workers per CPU core.

Note

Each worker is a separate process. With the default sessions="auto" and no signing key set, every worker mints its own random key, so signed session cookies won’t validate across workers and load-balanced users get logged out. Before running more than one worker or instance, set a stable RESPONDER_SECRET_KEY (or API(secret_key=...)). For sessions that survive across separate machines, store them server-side with a shared backend such as AsyncRedisSessionBackend. See the configuration guide.

Uvicorn supports many options — SSL certificates, access logging, graceful shutdown timeouts, and more. See the uvicorn documentation for details.

For platforms like Heroku or Railway that use a Procfile:

web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4

Granian

Granian is a Rust-based HTTP server that runs ASGI, WSGI, and RSGI apps from a single dependency. It’s a strong production peer to uvicorn — native HTTP/2, WebSockets enabled by default, and no separate worker package to install.

Install it:

$ uv pip install 'responder[orjson,server]'

Responder apps are ASGI, so run them with the asgi interface:

$ granian --interface asgi api:api

The familiar host, port, and worker flags all apply:

$ granian --interface asgi --host 0.0.0.0 --port 8000 --workers 4 api:api

To serve HTTP/2 in production, add --http 2:

$ granian --interface asgi --host 0.0.0.0 --port 8000 --workers 4 --http 2 api:api

For a Procfile:

web: granian --interface asgi --host 0.0.0.0 --port $PORT --workers 4 api:api

Like uvicorn’s --workers, every Granian worker is a separate process, so the stable-secret-key note above applies here too. api.run(server="granian") uses Granian’s embedded server for the current app object; for workers, reload, or process supervision, use the Granian CLI shown above.

Docker Compose

For local development with databases and other services, Docker Compose ties everything together:

# docker-compose.yml
services:
  api:
    build: .
    ports:
      - "5042:80"
    environment:
      - PORT=80
      - DATABASE_URL=postgresql+asyncpg://user:pass@db/myapp
      - RESPONDER_SECRET_KEY=dev-only-not-for-production-32chars
    depends_on:
      - db

  db:
    image: docker.io/postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Run with docker compose up. The API waits for db to start, then connects using the DATABASE_URL environment variable.

Reverse Proxy

For high-traffic production deployments, you may want a reverse proxy like nginx or Caddy in front of your application for:

  • SSL/TLS termination — let the proxy handle HTTPS certificates

  • Load balancing — distribute traffic across multiple app instances

  • Static asset serving — offload static files to the proxy

  • Rate limiting — at the infrastructure level

A minimal Caddy config that handles HTTPS automatically:

# Caddyfile
example.com {
    reverse_proxy localhost:5042
}

Behind a proxy, tell Responder to trust its forwarding headers:

api = responder.API(trust_proxy_headers=True)

Responder then honors RFC 7239 Forwarded (or X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-For) and rewrites the request’s scheme, host, and client address to what the original client sent — so HTTPS detection, redirects, URL building, host validation, and logged or rate-limited client IPs are all correct. Only enable it when every request reaches Responder through a proxy you control that overwrites those headers; otherwise clients can spoof them.

Behind a TLS-terminating proxy this is exactly right: in production (debug=False) Responder marks the session cookie Secure by default, so it only travels over HTTPS — no action needed. Only pass session_https_only=False if you genuinely serve plain HTTP. (Browsers reject SameSite=None without a Secure cookie, so Responder rejects that combination too.)

That said, uvicorn and Granian are both production-ready on their own. Many applications run the ASGI server directly without a reverse proxy and do just fine.

Production Checklist

Before going live:

  • Set a stable secret key — pass API(secret_key=...) or set RESPONDER_SECRET_KEY (16+ chars; generate one with python -c "import secrets; print(secrets.token_urlsafe(32))"). It must be stable across workers and restarts, or signed sessions stop validating. If your service is stateless and never touches req.session, pass API(sessions=False) instead to skip sessions entirely. See the configuration guide.

  • Disable debug mode — it’s off by default; never set debug=True in production

  • Set allowed hostsallowed_hosts=[...], restricted to your domains

  • Behind a proxy? Trust its headerstrust_proxy_headers=True so HTTPS detection, redirects, and client IPs reflect the original request (only when every request comes through your proxy — see Reverse Proxy)

  • Serving browser forms? Enable CSRF protectioncsrf=True pairs with the default-on cookie sessions; exempt webhook routes with csrf=False. Token-only APIs (no cookies) don’t need it.

  • Check the request-body cap fits — bodies over max_request_size (100 MiB by default) get a 413; raise it for large uploads (multipart streams to disk, so a bigger cap doesn’t mean more memory)

  • Use multiple workers--workers 4 or more, depending on CPU cores (set a stable secret key first — see above)

  • Add a health check/health endpoint for monitoring

  • Enable HTTPS — via your proxy, cloud platform, or your ASGI server’s --ssl-* flags; the session cookie is then Secure automatically

  • Set up logging — your ASGI server logs requests by default; pipe them to your log aggregator

  • Pin your dependencies — use a lock file or pinned requirements for reproducible deploys