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-demon is 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.md is 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.

Track A result: the Python reference takes 32.05 seconds and 228 MB, the Rust engine takes 0.019 seconds and 16 MB. That is 1,690 times faster at 12.6 GB/s, scoring 86.25 points with zero dependencies in 524 lines.

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.

Architecture diagram of six stages in three groups. Parse: mmap and 1 MiB claims, AVX2 newline pass, branch-free line pass. Summarise: seal hour, radix sort and deal by endpoint. Write: parallel pwrite and page release. Below, 12 thread lanes over 19 ms show parsing, summarising and writing happening at the same time.
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:

  1. 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.
  2. 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.

A real log line shown byte by byte, with the timestamp, method, endpoint, status and latency in different colours. Arrows show it being read from the front and the back at once. Cards explain the four tricks: the hour from one 8-byte load at offset 8, the method length from the first space in one word, the endpoint hashed with vaesenc and looked up as an id, and the latency decoded with three multiply-shift steps.

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.

Hour-sealing diagram. A bar for the 238.8 MB file shows how far parsing has got. Below it, a grid of all 72 hours, colour-coded as written and released, being summarised, still collecting rows, or not reached yet. The rule reads “prefix holds hour h + 2, so seal h”, with an exact rerun if a line ever arrives for a sealed hour. A comparison shows about 5 hours held in memory instead of 72, keeping peak memory near 16 MB.

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

Step chart of the best board score at each step. It goes from 0 to 79.98 with the first SIMD engine, then 83.16, 84.26, 85.71 and 86.21, ending at 86.25 points. Each step is labelled with what changed, from the AVX2 parser to the streaming hour sealer. Memory fell from 228 MB to about 16 MB along the way.
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 with docker build of 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).