Each app runs as one process in production mode with the same 10,000 rows. The load tool keeps 50 connections busy and sends the next request the moment one returns, so each bar is that framework's maximum. Shield and Tvine's rate limits are off here.
The same scale on every row. Raw Node and Express reach the load tool's own limit on the fast endpoints, so their real ceiling may be higher. Hover a bar for latency, CPU and memory.
Median of three 15-second runs. Latency in milliseconds; server CPU 100% is one core.
200 connections from 4 load processes on the same 8 cores. Tvine runs tvine start --workers 8 with sessions and rate limits shared on Redis.
One row by id, from 10 to 500 open connections. A flat line means the process was already full at 10.
The client sends at a fixed rate; a step fails when p99 goes over the dashed line or more than 1% of requests fail (red dots). 100,000 a second was not reachable here: the load processes ran out of CPU first.
From 20,000 rotating IPs, so the rate limit never fired: this is the cost of the checks alone.
Agents calling MCP tools at once, with a Claude-User agent name and rotating IPs.
While the flood runs, a separate visitor on another IP asks for a page five times a second. The bar is the share of the visitor's requests that were answered.
A flood from one IP is turned away by the per-IP limit, which costs far less than serving it, so the visitor keeps getting pages. A flood spread over 1,000 IPs keeps each IP under the limit, so the single process fills up and the visitor's connections are reset. Stopping that needs a whole-site emergency mode and more processes.
benchmarks/ in the Tvine repository. ./run.sh reruns everything; node report.mjs rebuilds this page.