Bash, C# and Go all had HTTP and TLS in their standard libraries, but unfortunately Rust doesn't. The spec lets a language without them use one named library for the transport, and that library counts towards size, so this is the first time the leaderboard has to price a dependency. The transport is ureq with rustls, the parsing is serde_json, and together they bring in thirty-six crates. The result is still the smallest thing on the board so far.
The code is at github.com/AndyCross/claude-hot-path, and the leaderboard is at /claude-hot-path/.
Choosing the transport
The usual answer for HTTP in Rust is reqwest, which is async and brings tokio and hyper with it. For a client that makes two sequential requests and streams the reply to stdout, async buys nothing and costs a runtime. ureq is the blocking alternative, it defaults to rustls so there is no OpenSSL to link, and version 3 exposes the response body as a Read, which is all the SSE loop needs.
One configuration line matters. By default ureq turns a 4xx or 5xx response into an Err, which would throw away the body that holds the API's error message. The spec wants that message on stderr, so the agent is built with status errors off and the client reads the status itself:
let agent: ureq::Agent = ureq::Agent::config_builder() .http_status_as_error(false) .build() .into(); The loop
The body reader goes into a BufReader and lines() does the rest. The let else on the prefix strip is doing the same job as the continue in Go, skipping event: lines, pings and blank separators:
let reader = BufReader::new(resp.body_mut().as_reader()); for line in reader.lines() { let line = line.unwrap_or_default(); let Some(data) = line.strip_prefix("data: ") else { continue }; let ev: Value = serde_json::from_str(data).unwrap_or_else(|e| { eprintln!("error: bad event: {e}"); exit(1) }); let index = ev["index"].as_u64().unwrap_or(0) as usize; match ev["type"].as_str() { Some("content_block_start") => { content.push(ev["content_block"].clone()); partial.push(String::new()); } Some("content_block_delta") => match ev["delta"]["type"].as_str() { Some("text_delta") => { let text = ev["delta"]["text"].as_str().unwrap_or(""); out.write_all(text.as_bytes()).unwrap(); out.flush().unwrap(); let existing = content[index]["text"].as_str().unwrap_or("").to_string(); content[index]["text"] = Value::String(existing + text); } Some("input_json_delta") => partial[index].push_str(ev["delta"]["partial_json"].as_str().unwrap_or("")), _ => {} }, Some("content_block_stop") => { if content[index]["type"] == "tool_use" && !partial[index].is_empty() { content[index]["input"] = serde_json::from_str(&partial[index]).unwrap_or(json!({})); } } Some("message_delta") => stop = ev["delta"]["stop_reason"].as_str().map(str::to_string), _ => {} } } serde_json::Value with index syntax is the Rust equivalent of C#'s JsonNode, and I used it for the same reason: the shapes are dynamic and a typed struct per event would be longer than the client. Indexing a Value that lacks the key returns Null rather than panicking, so the as_str().unwrap_or chains read a missing field as empty and move on.
The out is a locked stdout, flushed after every delta. Rust's Stdout is line-buffered, and a text delta rarely ends in a newline, so without the flush the words would sit in the buffer until the stream ended. The harness catches that: the runner records when the first visible byte reached stdout and the mock records when it wrote each chunk, and a client whose first text lands after the stream finished fails conformance. I tried it with the flush removed, and it failed by a millisecond. With the flush it has eleven in hand.
The one thing the library does not have
Timezones are the gap again. Rust's standard library has no zoneinfo reader and chrono-tz or jiff would be a third dependency carrying a copy of the database. The platform has one at /usr/share/zoneinfo, in the TZif format from RFC 8536, and reading it is sixty lines: skip the version 1 block, take the 64-bit transition times from the version 2 block, find the last transition before now, and apply its offset. The civil date comes out of Howard Hinnant's days-to-date algorithm, which is nine lines of integer arithmetic. It is more code than any other part of the client, and it is part of why the turnaround column reads the way it does.
What it measures as
| Rust | Go | C# Native AOT | bash and curl | |
|---|---|---|---|---|
| Artefact | 1.4 MB | 5.8 MB | 5.4 MB | 3.7 KB |
| Runtime | 2.0 MB | 0 | 2.4 MB | 23.7 MB |
| Ship | 3.3 MB | 5.8 MB | 7.8 MB | 23.7 MB |
| Cold start, median of 100 | 1.1 ms | 1.7 ms | 3.0 ms | 7.0 ms |
| Turnaround, median of 100 | 0.7 ms | 1.2 ms | 1.4 ms | 19.1 ms |
| Client total, median of 100 | 3.2 ms | 4.8 ms | 7.1 ms | 35.3 ms |
| Peak RSS | 3 MB | 6 MB | 7 MB | 11 MB |
| Lines of code | 184 | 156 | 102 | 75 |
| Third-party crates or packages | 36 | 0 | 0 | 0 |
rustc 1.99.1, linux/arm64, Apple M4 Max under Docker. Release profile with opt-level = "z", fat LTO, one codegen unit, panic = "abort" and symbols stripped.
One point four megabytes for the binary, and that is with a complete TLS stack inside it. rustls and its crypto provider are most of it. The 2.0 MB runtime is glibc, libgcc_s and the loader, the same bill .NET paid, and a musl build would fold most of that into the binary. I haven't measured whether the total would come out smaller. I have left it dynamic because the Debian image is what someone would actually deploy, and because the leaderboard already has one static entry to compare against.
Three megabytes of peak memory is half of Go's, the cold start is the fastest so far, and the turnaround is the fastest on the board by a clear margin, though it didn't start out that way: the first version shelled out to date for the timezone and measured three milliseconds between turns, and when I replaced the process with the TZif reader above it only came down to about two. The rest was ureq: it resolves the hostname on every request, before it checks the connection pool, so each turn paid a DNS round trip to Docker's resolver for a socket it then reused. Pointing the client at the mock's IP instead of its name was how I found that, and the fix is the caching resolver, plugged in through Agent::with_parts. Thirty-six crates is the cost, and it is the first non-zero number in that column. Go and .NET got their HTTP stacks from a standard library maintained by one organisation. Rust got its from a dozen maintainers who have never met, and I'm not going to pretend that is the same thing. Both are reasonable engineering positions, and the column is there so you can weigh them yourself.
How to run it
git clone https://github.com/AndyCross/claude-hot-path cd claude-hot-path ./bench/bench.sh rust Docker and Python 3, no key. The release build takes about fifteen seconds in the container. Next is Zig, which has HTTP and TLS in its standard library and a reputation for binaries measured in kilobytes, so the top of the board may not stay where it is.