Workers

Configuring worker count and understanding thread vs process mode

3 min read 630 words

Worker Modes

Pounce automatically selects the worker mode based on the Python runtime:

Runtime Workers Are Detection
Python 3.14t (free-threading) Threads sys._is_gil_enabled() returns False
Standard CPython (GIL) Processes sys._is_gil_enabled() returns True

You don't need to configure this — the supervisor detects it at startup.

Configuring Worker Count

# Single worker (no supervisor, lowest overhead)
pounce serve --app myapp:app --workers 1

# Auto-detect from CPU cores
pounce serve --app myapp:app --workers 0

# Explicit count
pounce serve --app myapp:app --workers 4

Single Worker (workers=1)

The server skips the supervisor entirely and runs the worker directly in the main thread. This is the lowest-overhead mode — no supervisor thread, no health monitoring. Good for development and low-traffic services.

Auto-Detect (workers=0)

Callsos.cpu_count()and uses that as the worker count (minimum 1). On an 8-core machine, this creates 8 workers.

Multi-Worker (workers=2+)

The supervisor spawns N workers and monitors their health. Workers that crash are automatically restarted (max 5 restarts per 60-second window).

Thread vs Process Workers

flowchart LR subgraph ft ["Free-Threading (3.14t)"] direction TB P1["1 Process"] --> T1["Thread 1\nevent loop"] P1 --> T2["Thread 2\nevent loop"] P1 --> TN["Thread N\nevent loop"] T1 ~~~ Mem1["Shared Memory\nconfig, app, routes"] T2 ~~~ Mem1 TN ~~~ Mem1 end subgraph gil ["GIL Build"] direction TB Sup["Supervisor"] --> PR1["Process 1\nevent loop"] Sup --> PR2["Process 2\nevent loop"] Sup --> PRN["Process N\nevent loop"] PR1 ~~~ Mem2["Isolated Memory ×N"] end

Thread Workers (Free-Threading)

On Python 3.14t, each worker is a thread with its own asyncio event loop:

  • Shared memory — All workers see the sameServerConfig, app reference, and route tables
  • No IPC — Threads communicate through shared memory, not pipes
  • Lower memory — One copy of the application, not N copies
  • SO_REUSEPORT — Kernel distributes incoming connections across workers

Process Workers (GIL)

On GIL builds, each worker is a separate process:

  • Isolated memory — Each process has its own copy of everything
  • Fork-based — The already-imported app and completed main lifespan startup are inherited
  • Higher memory — N copies of the application

Warning

The state inherited by process workers must be fork-safe. Do not start background threads or executors, hold synchronization primitives, or create process-local clients during module import,Serverconstruction, or lifespan.startupwhen using multiple workers on a GIL build. A child can inherit a locked object without the thread that would release it, report ready, and then hang when a request touches that object.

Pounce deliberately usesforkso closures and other non-picklable ASGI callables continue to work. It does not currently provide aspawnor forkserverprocess-worker option.

Create or replace process-local resources inpounce.worker.startup, which runs separately inside every worker before it accepts requests. Set worker_startup_failure="shutdown"when that initialization must succeed before readiness. If the app cannot satisfy this pre-fork contract, use --workers 1or deploy on Python 3.14t, where automatic multi-worker mode uses threads instead of forked processes.

Tuning Guidelines

Workload Recommended Workers
CPU-bound (computation) os.cpu_count()(workers=0)
I/O-bound (database, network) os.cpu_count() * 2
Development 1(single worker)
Mixed os.cpu_count()(start here, tune)

Tip

Start with--workers 0(auto-detect) and monitor. Adjust based on actual CPU utilization and latency metrics.

Health Monitoring

The supervisor runs a health-check loop for multi-worker mode:

  • Workers that crash are restarted automatically
  • Maximum 5 restarts per 60-second window (prevents restart loops)
  • Graceful shutdown signals all workers before exit

See Also