Skip to content

feat: Add FastAPI CLI and uvicorn --workers benchmarks + upgrade all deps - #15

Merged
KissPeter merged 37 commits into
mainfrom
feat/server-runners-benchmark
Jul 23, 2026
Merged

feat: Add FastAPI CLI and uvicorn --workers benchmarks + upgrade all deps#15
KissPeter merged 37 commits into
mainfrom
feat/server-runners-benchmark

Conversation

@KissPeter

Copy link
Copy Markdown
Owner

Changes

New Benchmarks

  • FastAPI CLI (fastapi run --workers) - official CLI wrapper around uvicorn
  • Uvicorn --workers - direct uvicorn multi-process mode
  • Both tested with 1 and 2 workers, comparing against Gunicorn (baseline) and existing Uvicorn

Dependency Upgrades

Package Old New
Python 3.11.1 3.14
FastAPI 0.85.1 0.139.2
Uvicorn 0.19.0 0.51.0
Gunicorn 20.1.0 26.0.0
orjson 3.8.0 3.11.9
ujson 5.5.0 5.13.0
starlette 0.20.4 1.3.1

CI Fixes

  • docker-compose v1 → docker compose v2 (v1 not available on modern runners)
  • isbang/compose-actionhoverkraft-tech/compose-action@v2.5.0
  • actions/checkout@v3@v4, setup-python@v2@v5
  • Shell scripts: added shebangs, fixed FastAPI CLI --app syntax
  • Fixed port conflict: multiprocess containers moved from 8017/8018 to 8019/8020
  • Added missing gunicorn containers (app_gunicorn_w1_t0, app_gunicorn_w2_t0)
  • Added server_runners pytest marker
  • Added server_runners.md documentation page

- Add FastAPI CLI (fastapi run --workers) and uvicorn --workers benchmarks
- Upgrade to Python 3.14, FastAPI 0.139.2, Uvicorn 0.51.0, Gunicorn 26.0.0
- Upgrade orjson 3.11.9, ujson 5.13.0, starlette 1.3.1
- Add missing gunicorn containers for existing gunicorn_vs_uvicorn tests
- Fix port conflict: multiprocess w1/w2 moved from 8017/8018 to 8019/8020
- Fix CI: docker-compose v1 -> docker compose v2
- Fix CI: isbang/compose-action -> hoverkraft-tech/compose-action v2.5.0
- Fix shell scripts: add shebangs, fix FastAPI CLI --app syntax
- Upgrade CI actions to v4/v5
- Add server_runners pytest marker and test file
- Add server_runners.md documentation page
- Remove obsolete dataclasses backport from requirements
@github-actions
github-actions Bot enabled auto-merge July 20, 2026 16:58
KissPeter added 28 commits July 20, 2026 19:01
…checkout

- docker-compose_workers_and_threads.yml: add build: context for all 25 services
- CI linter: remove broken ref: github.ref_name (resolves to PR number on PR events)
- docker-compose.yml: remove deprecated version key
- Update generator script to include build directives
- Add checks:write permission for super-linter GitHub Status API
- Make linter continue-on-error (pre-existing lint issues)
- Populate sync/async × w1/w2 tables with Gunicorn vs Uvicorn data
- Mark FastAPI CLI and Uvicorn --workers as N/A (container startup failure)
- Add verdict summarizing Gunicorn's ~12-20% throughput advantage
start_fastapi_cli.sh and start_uvicorn_workers.sh were not executable,
causing Docker ENTRYPOINT to fail silently. Added chmod +x in Dockerfiles
as safety net. This is why app_fastapi_cli and app_uvicorn_workers
containers never started in CI.
…9196

All 4 containers now working. FastAPI CLI wins with 2 workers (+10-12% vs Gunicorn),
Gunicorn best for 1-worker deployments.
- 'Uvicorn' and 'Uvicorn --workers' are the same (both use --workers flag)
- Merged into single 'Uvicorn --workers' row
- Added worker count in table headers (Runner (1 worker) / Runner (2 workers))
- Added worker count in runner names (FastAPI CLI --workers)
- Added summary table in verdict section
- Updated recommendations to be specific per worker count
start_uvicorn.sh accidentally used --workers flag, making it identical to
start_uvicorn_workers.sh. Fixed to run as single-process uvicorn (no --workers).
Also removed redundant --port 8000 from start_uvicorn_workers.sh.

Updated server_runners.md: restored 4-row comparison (Gunicorn, Uvicorn single,
Uvicorn --workers, FastAPI CLI), explicit worker counts in labels, clearer
verdict section.
- Add mock API server (Dockerfile_mock_api, mock_api.py)
- Add sync_pool/async_pool endpoints to app.py with httpx
- Add connection pool test infrastructure (docker-compose services)
- Create test_connection_pool.py with 4 test scenarios
- Create sync_vs_async.md and connection_pool.md documentation
- Update index.md, pytest.ini, CI workflow
- Add app_gunicorn_w1_t1 (port 8025) and app_gunicorn_w2_t1 (port 8026)
- Restructure test_server_runners.py: all-runners sync+async, Gunicorn thread variants, Uvicorn multiprocess
- Update CI wait loop with new ports
… tests

- Added /info/slow_sync and /info/slow_async endpoints that track worker PID,
  thread ID, and concurrent request count
- Added 3 test containers with pool_size=2/4/100 and 2 workers (ports 8070-8072)
- Added test_concurrency.py with 5 tests verifying:
  - Per-worker pool isolation (pool is per-process, not per-app)
  - Pool size limits concurrent sync handlers per worker
  - anyio thread pool ceiling (40) is a ceiling, not a reservation
  - Async endpoints not limited by HTTP pool
- Updated CI workflow to include concurrency tests and new ports
CI revealed pool_size=2 allows 10 concurrent handlers per worker.
The HTTP pool limits outgoing CONNECTION concurrency, not HANDLER
concurrency. Handlers block waiting for pool slots but are still
counted as active. Handler concurrency is limited by anyio thread pool.

Rewrote tests to verify:
- Per-process isolation (2 PIDs for 2 workers)
- Handler concurrency exceeds pool_size (queued, not rejected)
- All requests complete with small pool (throttle, not hard limit)
- Worker PID consistency (long-lived processes)
CI run 29907570007 revealed:
- Pool limits CONNECTION concurrency, not HANDLER concurrency
- pool_size=2 allowed 10 concurrent handlers per worker (thread pool limited)
- Handlers queue for pool slots but remain active
- anyio thread pool (40 tokens) is the actual handler concurrency ceiling

Updated per_worker_connection_pool.md and thread_pool_sizing.md
New containers:
- concurrency_pool_40_w2 (port 8073): pool=40, 2 workers
- concurrency_pool_80_w2 (port 8074): pool=80, 2 workers

Throughput tests (ab-based):
- sync_pool_40_vs_2, sync_pool_80_vs_40, sync_pool_80_vs_100
- async_pool_40_vs_2, async_pool_80_vs_40

Concurrency introspection tests:
- test_pool_40_handler_concurrency
- test_pool_80_handler_concurrency
- test_pool_sizing_comparison (all 4 pool sizes)
CI run 29916760540 results:
- Sync: pool=40 achieves +35% vs pool=2, pool=80/100 show no gain
- Async: pool size has minimal impact (+2.9% from 2→40, then flat)
- pool=40 matches anyio thread pool tokens — the actual concurrency ceiling

Updated connection_pool.md and pool_sizing_calculator.md with verified data
Honest presentation of data:
- Sync: pool=100 is best (616 RPS), pool=40 is close (613 RPS)
- Async: pool=40 is best (425 RPS), pool=100 is close (419 RPS)
- Added memory cost table: pool=100 uses ~11 MB vs ~4.5 MB for pool=40
- Recommendations based on deployment constraints, not one-size-fits-all
- At scale, +0.6% matters: 10K req/s * 0.6% = 60 more req/s
@KissPeter
KissPeter merged commit 70db48b into main Jul 23, 2026
7 of 10 checks passed
@KissPeter
KissPeter deleted the feat/server-runners-benchmark branch July 23, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant