docs: add latency benchmark vs LiteLLM/Portkey

- bench/ reproducible harness: instant mock backend + Apache Bench (ab -k -r)
- measures gateway overhead in passthrough mode (apples-to-apples)
- BENCHMARKS.md: methodology, caveats, results (cortiq ~57k rps p99 1ms vs
  Portkey ~5.8k p99 9ms vs LiteLLM 4-workers ~1.2k p99 59ms)
- README (EN+RU): Performance section + highlight
This commit is contained in:
infosave2007
2026-06-30 18:11:26 +03:00
parent 6a25d8ac08
commit 6466006de6
8 changed files with 279 additions and 1 deletions
+80
View File
@@ -0,0 +1,80 @@
# Gateway latency benchmark
How much latency does the **gateway itself** add? For an LLM proxy the model call
dominates wall-clock time, but the gateway's own overhead is what differs between
products — and it compounds across the many calls an agent makes. This benchmark
isolates that overhead and compares Cortiq Gateway with **LiteLLM** and **Portkey**.
> Reproduce on your own hardware: `bash bench/run.sh` (see [Methodology](#methodology)).
## TL;DR
Every gateway proxies the **same instant mock backend** in passthrough mode, under
identical load (`ab -k -r -c 20 -n 5000`). Latency is in milliseconds.
| Gateway | Throughput (req/s) | p50 | p90 | p99 | mean |
|---|--:|--:|--:|--:|--:|
| **Cortiq Gateway** (Rust) | **~57,300** | 0 ms | 1 ms | **1 ms** | 0.35 ms |
| Portkey Gateway (Node) | ~5,800 | 3 ms | 5 ms | 9 ms | 3.5 ms |
| LiteLLM (Python, 4 workers) | ~1,200 | 9 ms | 37 ms | 59 ms | 16.6 ms |
| _mock-direct (no gateway)_ | _~29,000_ | _1 ms_ | _1 ms_ | _3 ms_ | _0.7 ms_ |
On this machine Cortiq adds **sub-millisecond p99** overhead and sustains **~10×**
the throughput of Portkey and **~47×** of LiteLLM. The gap is architectural: a Rust
/ Tokio gateway uses every core in one process, with no interpreter or GC in the hot
path.
> Absolute numbers depend on hardware; the **ratios** between gateways are the point.
> Run `bench/run.sh` to get numbers for your environment.
## Why this matters
Agentic workloads make **many** gateway calls per task (plan → sub-tasks → tool calls
→ synthesis). Overhead that looks small per call (tens of ms) multiplies across a run
and caps the throughput a single instance can serve. Lower, more predictable tail
latency (p99) is the developer-facing differentiator when the model time is equal.
## Methodology
- **Isolate gateway overhead.** A tiny, dependency-free Node mock ([`bench/mock.js`](bench/mock.js))
returns a fixed OpenAI-compatible completion **instantly**. So measured latency is
the gateway's own cost, not model time. Every gateway proxies this same mock.
- **Apples-to-apples.** Each gateway runs in **passthrough / pinned** mode (no routing
or model-selection logic) so we compare raw proxy cost. Cortiq's intelligent routing
(`cortiq-auto`) adds one classifier round-trip — that's a feature, measured separately,
not folded into this overhead number.
- **Load.** Apache Bench, keep-alive, tolerant of length variance:
`ab -k -r -c 20 -n 5000 -p bench/body.json -T application/json`.
- **Fair to each runtime.** Cortiq = one process (Tokio, multi-core). Portkey = one Node
process (event loop). LiteLLM = **4 uvicorn workers** (its recommended multi-process
setup; a single Python worker is GIL-bound to one core and scores ~860 req/s).
- **Same box, localhost**, warm processes, results are representative of one run.
### Caveats (read me)
- This measures **gateway overhead only**. Real requests are dominated by model latency,
which is identical regardless of gateway — but overhead and tail latency are not.
- Localhost + mock backend removes network/model variance on purpose. Your absolute
numbers will differ; reproduce with `bench/run.sh`.
- `ab` reports LiteLLM "Failed requests" as a non-zero count — these are **response
content-length variations, not HTTP errors** (all responses are `200`, verified).
- Tested versions: Cortiq Gateway (this repo, `--release`), LiteLLM proxy via
`uvicorn litellm.proxy.proxy_server:app --workers 4`, Portkey `@portkey-ai/gateway`,
Apache Bench. Hardware: Apple Silicon, macOS.
## Reproduce
```bash
# prerequisites: node, ab (Apache Bench), a release build of the gateway
cargo build --release
# optional competitors (the harness auto-detects them):
pip install 'litellm[proxy]' uvicorn
npm install -g @portkey-ai/gateway
# run (tune load via env)
CONC=20 REQ=5000 bash bench/run.sh
```
The harness ([`bench/run.sh`](bench/run.sh)) starts the mock, then benchmarks the mock
directly and each available gateway, printing a throughput / p50 / p90 / p99 table.
+7
View File
@@ -3,6 +3,13 @@
All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **Latency benchmark** vs LiteLLM and Portkey: a reproducible harness (`bench/`,
instant mock backend + Apache Bench) and `BENCHMARKS.md`. Measured gateway overhead:
Cortiq ~57k req/s / p99 1 ms vs Portkey ~5.8k / p99 9 ms vs LiteLLM ~1.2k / p99 59 ms.
## [0.2.1] - 2026-06-30
### Added
+14
View File
@@ -35,6 +35,7 @@ One OpenAI-compatible endpoint → automatic model selection from your pool
## ✨ Highlights
- **Fast.** Rust/Tokio: **sub-millisecond p99** gateway overhead — ~10× the throughput of Portkey and ~47× of LiteLLM in a like-for-like proxy benchmark ([details](BENCHMARKS.md)).
- **Drop-in OpenAI API + streaming.** Point any OpenAI client at the gateway and send `model: "cortiq-auto"`; `stream: true` is fully supported (SSE).
- **Multi-protocol.** OpenAI Chat & Embeddings, **Anthropic Messages** (in/out, streaming), and an **MCP** server (`POST /mcp`) exposing routing as tools for agent orchestrators.
- **Intelligent routing.** `complexity.tier` → ordered model pool (`low → local`, `high → cloud`), with fallback and graceful degradation when the router is down.
@@ -219,6 +220,19 @@ Details — [docs/PROTOCOLS.md](https://github.com/infosave2007/cortiq-gateway/b
---
## ⚡ Performance
Gateway overhead proxying the same instant backend, `ab -k -r -c 20 -n 5000`:
| Gateway | req/s | p50 | p99 |
|---|--:|--:|--:|
| **Cortiq Gateway** (Rust) | **~57,300** | 0 ms | **1 ms** |
| Portkey (Node) | ~5,800 | 3 ms | 9 ms |
| LiteLLM (Python, 4 workers) | ~1,200 | 9 ms | 59 ms |
Methodology, caveats and a reproducible harness: **[BENCHMARKS.md](BENCHMARKS.md)**
(`bash bench/run.sh`). Numbers vary by hardware; the ratios are the point.
## Build & test
```bash
+15 -1
View File
@@ -34,7 +34,8 @@
## ✨ Ключевое
- **Drop-in OpenAI API.** Направьте любой OpenAI-клиент на шлюз и шлите `model: "cortiq-auto"`.
- **Быстрый.** Rust/Tokio: overhead шлюза с **субмиллисекундным p99** — ~10× к throughput Portkey и ~47× к LiteLLM в сопоставимом бенчмарке ([детали](BENCHMARKS.md)).
- **Drop-in OpenAI API + стриминг.** Направьте любой OpenAI-клиент на шлюз и шлите `model: "cortiq-auto"`; `stream: true` поддержан (SSE).
- **Интеллектуальный роутинг.** `complexity.tier` → упорядоченный пул моделей (`low → локально`, `high → облако`), с fallback и мягкой деградацией, если роутер недоступен.
- **Встроенная многоязычная панель управления.** Модели, роутинг, протоколы, ключи и секреты — из веб-интерфейса, **без ручного TOML и без рестарта** (горячая перезагрузка). **7 языков** (en, ru, de, fr, es, zh, tr), тёмная/светлая тема.
- **Живая аналитика.** Статистика по запросам (токены, стоимость, латентность, доля успеха, failover), графики, разбивка по моделям/полосам/задачам и Prometheus `GET /metrics`.
@@ -168,6 +169,19 @@ print(resp.choices[0].message.content)
---
## ⚡ Производительность
Overhead шлюза при проксировании одного и того же мгновенного бэкенда, `ab -k -r -c 20 -n 5000`:
| Шлюз | req/s | p50 | p99 |
|---|--:|--:|--:|
| **Cortiq Gateway** (Rust) | **~57 300** | 0 ms | **1 ms** |
| Portkey (Node) | ~5 800 | 3 ms | 9 ms |
| LiteLLM (Python, 4 воркера) | ~1 200 | 9 ms | 59 ms |
Методология, оговорки и воспроизводимый harness — **[BENCHMARKS.md](BENCHMARKS.md)**
(`bash bench/run.sh`). Абсолютные числа зависят от железа; важны соотношения.
## Сборка и тесты
```bash
+1
View File
@@ -0,0 +1 @@
{"model":"bench","messages":[{"role":"user","content":"ping"}],"max_tokens":1}
+26
View File
@@ -0,0 +1,26 @@
# Benchmark config: one model pinned to the mock upstream. Requests use
# model="bench" (a real id) → direct passthrough, no router call — so we measure
# pure proxy overhead, comparable to LiteLLM/Portkey passthrough.
listen = "0.0.0.0:9100"
[router]
url = "http://127.0.0.1:8100"
[[models]]
id = "bench"
provider = "openai"
base_url = "http://127.0.0.1:8100/v1"
model = "bench-model"
[routing]
default = "bench"
[protocols]
openai_chat = true
[admin]
enabled = false
[stats]
enabled = true
file = ""
+40
View File
@@ -0,0 +1,40 @@
// Fast, dependency-free mock upstream for gateway benchmarks.
// Responds instantly with a fixed OpenAI-compatible chat completion so the
// measured latency reflects GATEWAY overhead, not model time.
//
// node bench/mock.js [port] # default 8100
const http = require("http");
const PORT = parseInt(process.argv[2] || "8100", 10);
const COMPLETION = JSON.stringify({
id: "chatcmpl-bench",
object: "chat.completion",
model: "bench-model",
choices: [
{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" },
],
usage: { prompt_tokens: 8, completion_tokens: 1, total_tokens: 9 },
});
const ROUTE = JSON.stringify({
request_id: "bench",
decision: { task_label: "general", complexity: { score: 0.2, tier: "low" } },
});
const server = http.createServer((req, res) => {
if (req.method === "GET") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
return;
}
// drain the request body, then reply
req.on("data", () => {});
req.on("end", () => {
const body = req.url.includes("/route") ? ROUTE : COMPLETION;
res.writeHead(200, { "content-type": "application/json" });
res.end(body);
});
});
server.keepAliveTimeout = 60000;
server.listen(PORT, "127.0.0.1", () => console.log(`mock upstream on :${PORT}`));
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Gateway latency benchmark: measures pure proxy overhead by pointing every
# gateway at the same instant mock upstream and load-testing with Apache Bench.
#
# CONC=20 REQ=10000 bash bench/run.sh
#
# Compares (when installed): mock-direct (baseline), cortiq-gateway, LiteLLM, Portkey.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
CONC="${CONC:-20}"
REQ="${REQ:-10000}"
BODY="$HERE/body.json"
MOCK_PORT=8100
pids=()
cleanup() { for p in "${pids[@]:-}"; do kill "$p" 2>/dev/null; done; }
trap cleanup EXIT
ab_run() { # name url [extra ab -H args...]
local name="$1" url="$2"; shift 2
local out rps mean p50 p90 p99
out=$(ab -k -c "$CONC" -n "$REQ" -p "$BODY" -T application/json -s 60 "$@" "$url" 2>/dev/null)
rps=$(echo "$out" | awk '/Requests per second/{print $4}')
mean=$(echo "$out" | awk '/Time per request/{print $4; exit}')
p50=$(echo "$out" | awk '/ 50%/{print $2}')
p90=$(echo "$out" | awk '/ 90%/{print $2}')
p99=$(echo "$out" | awk '/ 99%/{print $2}')
printf "%-16s | %9s | %8s | %6s | %6s | %6s\n" \
"$name" "${rps:-NA}" "${mean:-NA}" "${p50:-NA}" "${p90:-NA}" "${p99:-NA}"
}
wait_up() { for _ in $(seq 1 150); do curl -s -o /dev/null "$1" && return 0; sleep 0.2; done; return 1; }
echo "concurrency=$CONC requests=$REQ (latency in ms; ab -k)"
printf "%-16s | %9s | %8s | %6s | %6s | %6s\n" "gateway" "rps" "mean" "p50" "p90" "p99"
printf -- "-----------------+-----------+----------+--------+--------+--------\n"
# --- mock baseline -----------------------------------------------------------
node "$HERE/mock.js" $MOCK_PORT >/tmp/bench_mock.log 2>&1 & pids+=($!)
wait_up "http://127.0.0.1:$MOCK_PORT/healthz" || { echo "mock failed"; exit 1; }
ab_run "mock-direct" "http://127.0.0.1:$MOCK_PORT/v1/chat/completions"
# --- cortiq-gateway ----------------------------------------------------------
BIN="$ROOT/target/release/cortiq-gateway"
[ -x "$BIN" ] || BIN="$ROOT/target/debug/cortiq-gateway"
"$BIN" --config "$HERE/cortiq.bench.toml" >/tmp/bench_cortiq.log 2>&1 & CG=$!; pids+=($CG)
if wait_up "http://127.0.0.1:9100/healthz"; then
ab_run "cortiq-gateway" "http://127.0.0.1:9100/v1/chat/completions"
fi
kill $CG 2>/dev/null
# --- LiteLLM -----------------------------------------------------------------
if python3 -c "import litellm" 2>/dev/null; then
cat > /tmp/litellm.yaml <<YAML
model_list:
- model_name: bench
litellm_params:
model: openai/bench-model
api_base: http://127.0.0.1:$MOCK_PORT/v1
api_key: sk-bench
YAML
# the proxy CLI is brittle across versions; run the ASGI app via uvicorn.
# 4 workers = LiteLLM's recommended multi-process setup (fair vs a multi-core gateway).
CONFIG_FILE_PATH=/tmp/litellm.yaml python3 -m uvicorn litellm.proxy.proxy_server:app \
--host 127.0.0.1 --port 4000 --workers 4 >/tmp/bench_litellm.log 2>&1 & LL=$!; pids+=($LL)
if wait_up "http://127.0.0.1:4000/health/liveliness"; then
# warm up the workers before measuring (cold imports per worker)
for _ in $(seq 1 10); do curl -s -o /dev/null -X POST -H 'content-type: application/json' -d @"$BODY" "http://127.0.0.1:4000/v1/chat/completions"; done
sleep 2
ab_run "litellm" "http://127.0.0.1:4000/v1/chat/completions"
fi
kill $LL 2>/dev/null
else
echo "litellm | (not installed — pip install 'litellm[proxy]')"
fi
# --- Portkey -----------------------------------------------------------------
# resolve relative to the active node (npm root -g may point at a different npm)
PK_MAIN="$(dirname "$(command -v node 2>/dev/null)")/../lib/node_modules/@portkey-ai/gateway/build/start-server.js"
if [ -f "$PK_MAIN" ]; then
PORT=8787 node "$PK_MAIN" >/tmp/bench_portkey.log 2>&1 & PK=$!; pids+=($PK)
if wait_up "http://127.0.0.1:8787/v1/health" || wait_up "http://127.0.0.1:8787/"; then
sleep 1
ab_run "portkey" "http://127.0.0.1:8787/v1/chat/completions" \
-H "x-portkey-provider: openai" \
-H "x-portkey-custom-host: http://127.0.0.1:$MOCK_PORT/v1" \
-H "Authorization: Bearer sk-bench"
fi
kill $PK 2>/dev/null
else
echo "portkey | (not installed — npm i -g @portkey-ai/gateway)"
fi
echo
echo "Note: numbers are gateway overhead over a zero-latency mock on one machine;"
echo "subtract mock-direct to isolate proxy cost. Re-run on your own hardware."