Skip to content
rustle

Why rustle is fast, one core at a time

How rustle handles a connection from accept to close, where the speed comes from, and where it does not help. With the measurements behind each claim.

· 5 min read

A forward proxy does little work per request: read a few hundred bytes, pick a destination, copy bytes both ways. So the speed of a proxy is mostly the cost of everything around that work: waking threads up, taking locks, allocating memory, copying data between the kernel and the program. rustle is built to pay as little of that as possible, on one core first.

This post walks through what happens to one connection, and what each choice buys. Every figure comes from our benchmark campaign of 28 October 2026, with one core per proxy unless stated. The conditions are on the benchmarks page; the short version is a 25 Gbit/s private network, three dedicated servers, and the median of three runs.

One connection stays on one core

rustle starts a number of workers, called shards. Each shard is an operating system thread with its own small event loop and its own listening socket on the same port. On Linux the kernel (through SO_REUSEPORT) hands each new connection to one of these sockets. From then on, that connection is accepted, read, relayed and closed by the same shard. Nothing moves it to another core, and there is no work stealing.

The effect is that the shards almost never wait for each other. Each one keeps its own pool of kept-alive connections to the servers it talks to. The few things they do share are a handful of counters and, when a client asks for a host name, a short-lived lock around the DNS cache. So we do not claim there are no locks. We claim that a connection never leaves its core, and that the data path shares very little.

One thing to know: the default is a single shard, because workers = 1 is the safe choice on a small machine. To use every core, set workers = 0, or the number you want.

Does it scale? On plain HTTP with 1 KB responses, rustle went from about 55k requests per second on one core to about 440k on eight, a factor of 7.96. The same test gave 7.74 for Squid, 6.48 for gost and 5.44 for 3proxy. That is the only multi-core test we ran: TLS, HTTP/2 and tunnels were measured on one core only, and we have not measured beyond eight.

No work that can be skipped

On a connection that stays open, the cost per request is mostly the housekeeping, so the housekeeping is kept small:

  • The request head is parsed in place, as positions inside the read buffer, without building a string for each header.
  • The buffers of a connection are reused from one request to the next.
  • Socket options such as TCP_NODELAY are set once on the listening socket and inherited, not once per request.
  • Authentication is checked on every request, but the key is decoded on the stack, and compared against all the configured keys without stopping at the first match.

None of this is clever. It adds up. For a 1 KB response over plain HTTP, rustle spent about 18 microseconds of CPU per request, against 48 for 3proxy, 66 for Squid and 210 for gost. At one core, that is roughly 55k requests per second for rustle, against 21k for 3proxy: 2.6 times as many.

New connections are where the design shows most. When every request opens a new connection, rustle served about 28k requests per second, 3.6 times as many as Squid, the next one. One caveat: proxies also differ in how they reuse connections to the server, so this is not a pure measure of accepting connections.

Handing big transfers to the kernel

Copying a large file through a proxy the obvious way means reading it into the program’s memory, then writing it back out. For big flows, rustle skips that with splice on Linux: the kernel moves the bytes from one socket to the other without passing them through the program.

It does not do this for everything. A tunnel (CONNECT, a WebSocket, a SOCKS5 connection) starts with ordinary copies through a small buffer that grows from 8 KB up to 64 KB. Only when a direction keeps filling that buffer does rustle switch to splice for the rest of the connection. Small requests, uploads, chunked bodies, HTTP/2 and anything on the TLS port without kernel TLS are copied. So the accurate claim is that data that proved to be bulk stops passing through the program, not that payload never does.

The result, on plain tunnels, was 22.0 Gbit/s of upload on one core, using about half the CPU per gigabyte of 3proxy. A word of caution: our test network measures 23.5 Gbit/s, and rustle used only 70% of its core there. The network was the limit, so rustle may go faster. The other proxies were limited by their CPU, so the real lead is at least what we show.

Memory that is only paid for when used

Holding 25,000 idle tunnels open took rustle 95 MB, against 273 MB for Tinyproxy, 912 MB for Squid, 2.5 GB for 3proxy and 3.2 GB for gost. That is about 4 KB per tunnel.

It comes from a rule: a tunnel that is not moving data holds almost nothing. The request head is released as soon as the tunnel starts, the relay buffer is only touched when bytes arrive, and no pipe is attached until a flow turns out to be bulk.

Two conditions apply. The figure is for plain tunnels, with idle connections, and it is the memory of the proxy process alone. And it needed max_connections raised to 200,000, because the default is 10,000 and anything above it receives a 503. Over TLS the same test costs about 33 KB per tunnel (810 MB for 25,000), and over HTTP/2 about 52 KB. Still less than Squid and gost over TLS, but a different order of magnitude.

Where it does not help

Kernel TLS is the surprise. It lets the kernel encrypt the connection, which brings splice back on the HTTPS port. We expected a gain. On our hardware, the measure went the other way: between 12% and 43% slower than the TLS implementation in the program, depending on the test, for a memory saving between 5% and 20%. We think it is the kernel’s software encryption, but we have not shown it. kTLS is off by default for that reason.

HTTP/2 has a similar cost. Its framing adds work, and rustle serves 1 KB requests about 32% slower over HTTP/2 than over HTTPS with HTTP/1.1. Against gost, the only other proxy in that test, the lead on tunnels is 12%, which is small, from one campaign.

Finally, all of this was measured against a static origin, from one campaign, with a noise of about 3 to 5%. The full list of limits is on the benchmarks page.

Try it

The configuration is documented in the reference, and the safe defaults in the security guide. If you reproduce a number or find a setting we should have used for another proxy, open an issue: the campaign is built to be re-run.