Somewhere around commit ninety, I stopped asking how to parse a log line faster and started asking how to never look at most of it.
The task was Track A of Speed Demon 2026: read 3,100,000 HTTP request log lines, and for every hour and every endpoint, print the request count, the p50, p95 and p99 latency, and the 5xx error count and rate. One file in, one file out, and the output has exactly one correct SHA-256. The organisers' Python reference does it in 32.05 seconds using 228 MB.
Ours does it in 19 milliseconds using 16 MB, in one Rust file of 524 lines with zero dependencies. That is roughly 1,690 times faster, and a 238.8 MB file read at about 12.6 GB/s, end to end, including writing the answer.
This is the story of how it got there, told from the keyboard: the engine, the tooling we built around it, and the 72 hours in between.
The event: 72 hours, one box, one SHA-256
Speed Demon 2026, run by Hackathon Raptors, opened on October 30 at 15:00 UTC and froze code on November 2 at 15:00 UTC. The format is beautifully unforgiving:
- Push to a branch, get measured. Every push to
speed-demonis picked up by the Speed Demon Board app, built offline from our Dockerfile against an allowlisted image pinned by digest, and run once on the full public dataset. Results appear on a live leaderboard at speeddemon.dev. - One scoring machine. 12 hardware threads on six P-cores, turbo off, 8 GB of memory, no swap, no network at build or run time, a read-only filesystem and an unprivileged user.
- Exact or nothing. The output must match the reference byte-for-byte, by SHA-256, on every run. Final scoring reruns every frozen commit on a private holdout seed.
- Speed is not the only axis. Points weigh wall time T, peak memory M, source size S and dependency count D, and the optimisation log
PERF.mdis worth its own share of the total.
That last line shaped the whole project. A fast program that pulls in a crate, or sprawls over thousands of lines, or holds a gigabyte of RAM, loses points it does not need to lose. So the brief I set myself on hour zero was: as fast as the hardware allows, as small as memory allows, in as few lines as clarity allows, with nothing but the standard library.
We registered as team nastik, created the repo from the official template, installed the board app, and pushed the first commit within a few hours of kickoff.
The scoreboard, up front
| Reference (Python) | Nastik, Track A (Rust) | |
|---|---|---|
| Wall time | 32.05 s | 0.019 s |
| Peak memory | 228 MB | ~16 MB |
| Dependencies | — | 0 (standard library only) |
| Points | 0 | 86.25 |
| Source | — | 1 file, 524 lines |
| Commits to Track A | — | 133 in 72 hours |
| Seeds verified byte-for-byte | — | 1, 2, 3 and 7, at full scale |
Hour 0: reading the spec like a contract
Before writing a line, I read the spec the way you read a contract you are about to sign, looking for every promise it makes, because every promise is something the code is allowed to lean on.
- Every timestamp is exactly 27 bytes. So the hour lives at a fixed byte offset in every line, and nobody needs to parse a date.
- Every timestamp falls in the same 72-hour window, for every seed. So an hour is an index from 0 to 71, not a key in a map.
- There are exactly 1,842 endpoints, Zipf-distributed. So the endpoint table is small enough to live in L1/L2 cache, and it fills in the first moments of the run.
- The status is always three digits. So "is this a 5xx" is one byte compare.
- Percentiles are nearest-rank. So once a group's latencies are sorted, a percentile is one array read.
- The file is eight time-ordered shards, interleaved as chunks arrive. That one turned out to be the most valuable sentence in the whole document, and I come back to it below.
The first Rust version went in on the evening of day one, with the dependency count already at zero. Everything after that was the journey from "correct" to "as fast as the hardware allows".
The architecture in one paragraph
The log is mmaped with a spare zero page behind it, so wide SIMD loads can safely run past the end of the file. Twelve threads claim the file 1 MiB at a time. Within a claim, each 64 KiB block is scanned for newlines with AVX2, and then every line is decoded from both ends at once with no data-dependent branches. Each line becomes a single 4-byte row in its hour's region. Hours are sealed and summarised while the file is still being parsed, radix sorted in place, dealt out by endpoint, formatted over their own memory, and written to the output with parallel pwrite, after which their pages go straight back to the kernel.

Now the parts that made the difference.
1. Find every newline first, then parse with no branches
A classic parser walks bytes and branches on what it finds. Branches on data are what a CPU predicts worst, and log lines vary in length on every single line.
So the parser works in two passes over each 64 KiB block, which fits in L2 between them:
- Newline pass. AVX2 compares 32 bytes at a time against
\n, unrolled to 128 bytes per iteration. Lines are longer than 32 bytes, so a step holds at most one newline, and one unconditional store records it. A step with no newline writes a junk entry the next store overwrites. The loop has no branch on where lines end. - Line pass. A counted loop over the recorded line ends. Each line is read from its start and from its end simultaneously.
From the front: the day and hour digits sit at bytes 8 to 15 of the timestamp. One 8-byte load gives them all, and a bit trick turns them into an hour index (October's tens digit '3' has bit 1 set where '0' does not, which handles the month rollover with no comparison). Hours change rarely along the file, so the current hour's write cursor stays in registers until those digits change.
From the back: the latency is the last field, so it is read back from the newline. Its digits are joined in three multiply-shift steps (pairs, then fours, then all eight), SWAR style, inside one 64-bit register:
let v = (v & 0x0F0F_0F0F_0F0F_0F0F).wrapping_mul(2561) >> 8; let v = (v & 0x00FF_00FF_00FF_00FF).wrapping_mul(6_553_601) >> 16; let v = (v & 0x0000_FFFF_0000_FFFF).wrapping_mul(42_949_672_960_001) >> 32; The status is a fixed three bytes before the latency, and the endpoint is whatever sits between the method and the status. The method's length comes from finding the first space in one 8-byte word with a borrow trick. No loops over characters anywhere in the hot path.
2. Endpoints become ids, through one AES instruction
Every line needs to become an endpoint id. Comparing strings is expensive, and endpoints run up to 125 bytes.
The endpoint is hashed without a length-dependent loop: its first and last 32 bytes are loaded as two AVX2 registers (bytes outside the name masked off with a sliding window of ones and zeros), and mixed with vaesenc, which runs a full AES round on both 16-byte halves in a single instruction. Two more rounds finish the mix. Names over 64 bytes fold in two overlapping middle loads.
The 64-bit hash is looked up in one shared open-addressing table of 8,192 slots. Each slot packs the hash and id + 1 into a single AtomicU64, so a lookup is one load and one compare. The table fills under a lock in the first moments of the run and is read-only after that, so all twelve threads share one hot copy in cache. With 1,842 names and 53 compared bits, the odds of any two names colliding are about 2 × 10⁻¹⁰.
I measured a hardware-CRC32 hash against it along the way. The profile showed the AES hash at under 5% of samples, so the one with the full 64 bits of mixing stayed. That became a habit for the rest of the project: the profile decides, not the instruction count.
3. A whole log line in 4 bytes
An average line is about 77 bytes. By the time it leaves the parser it is 4 bytes:
| id + 1 (11 bits) | error (1 bit) | latency (20 bits) | The hour is implicit: the row is written into its hour's region of one large anonymous reservation, which costs nothing until a page is touched. About 1% of latencies need more than 20 bits; those store an escape value and spill the full number into the next row. Each thread fills its own 256-row chunk of each hour, so writing a row needs no atomics at all; only grabbing a new chunk touches a shared counter.
That is roughly a 19× reduction in the data everything downstream has to touch.
4. The idea that changed the shape of the program: seal hours early
For the first day and a half, the program had two phases: parse everything, then summarise everything. That meant all 72 hours of rows were alive at once.
Then I went back to the most valuable sentence in the spec. The file is eight shards, each writing in time order, interleaved as chunks arrive. That means once the parsed prefix of the file contains a line from hour h + 2, the lines of hour h have effectively all arrived.
So the engine seals hour h at that moment, and summarises it while the rest of the file is still being parsed. Parsing and summarising overlap across all twelve threads, and only a few hours of rows are ever alive instead of all 72.
The spec deliberately promises no bound on how out of order a line can be, so this is designed as a verified bet with an exact fallback. Every claim records the lowest and highest hour it contains. If a line ever turns up in an hour that was already sealed, a dirty flag is set and the whole job reruns with early sealing switched off. The output is byte-exact for any line order; on the real data the fast path simply always wins (on seed 1 the margin is at least 0.78 MiB of file).
// Hours below `sealed` have all their rows. Each thread works from its own snapshot of // the claims, which only ever lags behind the truth, so it seals too little, never too much. That comment is the whole correctness argument for the concurrency: a stale view can only make the engine more cautious, never wrong.
5. Sort without comparing, percentiles without searching
A sealed hour's rows are radix sorted by latency, in place, in two 10-bit passes. Then they are dealt out by endpoint, which is stable, so every endpoint's latencies come out already sorted. Nearest-rank p50, p95 and p99 are then three array reads at index (p * n + 99) / 100.
No comparison sort, no per-group allocation, no hash map of vectors. Every count, error tally and percentile for an hour comes out of one linear sweep.
6. Write in parallel, give memory back immediately
Each hour's text is formatted over its own rows, in the same pages. Once every earlier hour's text exists, an hour's byte offset in the output file is known, and whichever thread notices goes ahead and writes it with pwrite. So writes happen from several threads at once, overlapped with parsing.
Right after its write, the hour's pages are returned with MADV_DONTNEED. The input is treated the same way: each 1 MiB claim's file pages are dropped as soon as it is parsed, while every thread is still busy, instead of being torn down by one thread at exit. That single change took about 7 ms of teardown off the critical path.
This is how peak memory went from 228 MB to around 16 MB while the program got faster, not slower.
The journey, commit by commit

133 commits touched Track A in 72 hours. The arc looks like this:
| Stage | What happened | Result |
|---|---|---|
| Kickoff | Repo from the template, team registered, board app installed, check.py and generated datasets reproducing the reference's SHA-256 | A working pipeline from laptop to leaderboard |
| Day 1, evening | First dependency-free Rust engine: mmap, threads, contiguous arena, target-cpu=native | Off the reference, onto the board |
| Day 1, night | Zero-copy shared mapping, single-pass streaming, lock-free tallying | 79.98 pts, 33.5 ms, 19.3 MB |
| Day 2 | AVX2 SIMD parser, L1-resident endpoint table, SWAR digit decoding, branchless date math | 83.16 pts, 19.3 ms |
| Day 2 | Explored C and C++20 engines side by side to try ideas fast: per-hour regions, in-place radix sort, parallel pwrite | 84.38 pts, 17.6 MB |
| Day 3 | Brought every winning idea home into one Rust file; vaesenc endpoint hash, zero-alloc hour summaries | 85.71 pts, 15.4 MB |
| Day 3 | Lab A/B workflow, perf profiling branch and PGO lane come online; every change measured before it is pushed | 1% differences become visible |
| Day 3 | Streaming hour sealer: summarise and write hours while the file is still being parsed | 86.21 pts, 19.0 ms |
| Final | 128-byte unrolled newline scan, P-core aware build, static linking, final tuning | 86.25 pts |
The C and C++ detour deserves a word, because it was one of the most productive parts of the hackathon. Writing the same engine in three languages in parallel let me test a structural idea in whichever was quickest to change, and keep the idea rather than the language. Every technique that survived ended up in the final Rust file, with unsafe kept to the raw-pointer hot loops and the standard library doing everything else.
The pit crew: everything we built around the engine
The engine is 524 lines. The project around it is what let those 524 lines get tuned 133 times in 72 hours without a single wrong answer reaching the final commit. When a run lasts 19 ms, the tooling is the methodology, so it grew in layers, each one added the moment the previous one stopped being precise enough.
Layer 1: the board, locally. check.py builds the track exactly the way the board does (offline, --network=none, from the real Dockerfile) and runs it in the board's sandbox: read-only filesystem, unprivileged user, 8 GB, 512 processes, 1 GB of /tmp. It prints the output's SHA-256 and fails when it differs from the expected hash. The reference's hash on the generated data became the contract, and every change had to reproduce it before it was allowed near a push.
Layer 2: data at every scale. The organisers' generators are deterministic, so tiny, sample and full datasets for any seed could be generated locally and compared against the Python reference. Seed 1 at full scale is the board's dataset; seeds 2, 3 and 7 became our private holdouts, because final scoring uses a seed nobody has seen.
Layer 3: reading the board like an API. The leaderboard publishes a JSON file, so small Python scripts turned it into instruments: one waits until the board has picked up a specific commit and finished evaluating it, one polls the queue state of our in-flight run, and one prints a ranked table of time, memory, source lines and dependency count for every Track A entry. Pushing stopped being "refresh and hope" and became "push, wait for this SHA, read the numbers".
Layer 4: a lab with real statistics. At 19 ms, the live board measures one run per push, and near-identical code scored anywhere from 19.2 to 22.6 ms between runs. A 1% improvement is invisible there. So a lab branch got a GitHub Actions workflow that:
- builds the base (
speed-demon) and the candidate withdocker buildof the real Dockerfile, - checks the candidate's output against the reference on seed 1 and a freshly generated seed 7, both at full scale,
- times base and candidate alternately for 25 rounds on the same machine and compares medians and trimmed means,
- reads the cgroup's
memory.peak, the same counter the board uses, so memory is measured the way it is scored.
Every experiment then got its own branch, named after the idea (ab-a-scan128 for the 128-byte newline scan), merged on top of the shared ab-base harness. An idea was a branch, a branch was a measurement, and a measurement was a decision.
Layer 5: a profiler and a PGO lane. A prof branch runs perf record on the engine on seed 1 at full scale, with a source-line view, so the question "where does the time go" always had a current answer: parsing about 50%, summarising hours about 25%, the kernel about 20%, mostly page faults. A pgo branch trains profiles inside the allowlisted toolchain image with target-cpu pinned to the scoring box's Alder Lake, so profile-guided builds could be evaluated on equal terms. Because the parser is branch-free by design, it turned out to need no profile to run at full speed, which was a nice confirmation of the design.
Layer 6: a champion that every candidate must beat. The repository always held the best-scoring version of the engine. A candidate went live only if it beat the champion, and if a board run disagreed with the lab, the champion was restored in the next commit. That is why the history contains commits titled "Restore personal best": they are the ratchet that kept the score moving in one direction.
Layer 7: the log. Every board step is recorded in track-a/PERF.md with the run ID that measured it, alongside the lab method and the profile, so every number in this post can be traced to the run that produced it.
The 128-byte newline scan is the cleanest example of the whole pipeline working: the lab measured it at 1.1% to 2.9% faster, a difference no single board run could have shown, and it shipped because the medians on two different CPUs agreed.
What 72 hours felt like
The surprising thing was how little of the speed came from making any single operation faster. The big wins all came from not doing work: not parsing the date, because its position is fixed; not comparing strings, because a hash in a register is enough; not sorting with comparisons, because latencies are integers; not keeping 72 hours alive, because the shards are time-ordered; not tearing down pages at exit, because twelve threads can release them while they work.
Each of those came from rereading the spec and asking what it promised, then building the fastest program that keeps every one of those promises and checks the one it is not allowed to assume.
The other thing I will keep is the measurement habit. When a run lasts 19 milliseconds, your intuition is the noisiest instrument in the room. An A/B harness that alternates runs and compares medians turned "I think this is faster" into "this is 2% faster, on two CPUs, on four seeds, with the same SHA-256", and that is what let the last few points land.
Zero dependencies, one file, 524 lines, 1,690× faster than the reference, and every byte of the output exact.
The numbers
| Number | What it counts |
|---|---|
| 3,100,000 | log lines summarised per run |
| 238.8 MB | input file size |
| 19 ms | wall time on the scoring machine, down from 32.05 s |
| ~1,690× | speedup over the reference |
| ~12.6 GB/s | end-to-end throughput, input to written output |
| 16 MB | peak memory, down from 228 MB |
| 4 bytes | per log line after parsing |
| 0 | data-dependent branches in the line decoder's common path |
| 0 | third-party dependencies |
| 524 | lines of Rust, in one file |
| 133 | commits to Track A in 72 hours |
| 25 | alternating timed rounds per lab A/B comparison |
| 7 | layers of tooling: local board, generators, board watchers, lab, profiler, PGO lane, champion ratchet |
| 4 | seeds verified byte-for-byte at full scale |
| 86.25 | points on the Speed Demon board |
Repository: https://github.com/ewwhardik/speedy (Track A lives in track-a/, with the full optimisation log in track-a/PERF.md)
Developed by Sai Ram Dash (Hardik).


