Rust vs OCaml in 2025 — Benchmarks, Binary Sizes, and a 10-Minute Decision Tree
You are choosing a language for real work. You care about speed, but you also care about sleep. You want control without turning your…
Rust vs OCaml in 2026 — Benchmarks, Binary Sizes, and a 10-Minute Decision Tree

You are choosing a language for real work. You care about speed, but you also care about sleep. You want control without turning your day into a wrestling match.
This is a straight walk through Rust and OCaml in 2025 with one goal: help you ship and feel proud of the choice. No drama. No noise. Just what matters for performance, footprint, and day-to-day flow.
Not a Medium member? Read this story free with my friend link — it’s open access for you.
What You’ll Walk Away With
- A decision path you can follow right now
- Two allocation-aware code samples you can run today
- A simple way to measure on your machine without arguing on the internet
- Practical guidance on binary sizes, GC vs no-GC trade-offs, and when to mix both sides with FFI
I’m writing this for builders who carry the pager and the roadmap.
The Decision, Said Out Loud
- If your promise to users collapses when latency spikes, Rust is the safer bet. Ownership and lifetimes keep big projects honest. No GC means fewer surprises in the long tail.
- If your product is logic-heavy and you want crisp iteration with native speed, OCaml is a joy. Pattern matching and algebraic data types keep code small and direct. The runtime is compact and gets out of your way most of the time.
- If you value developer happiness and measurable speed, you can mix: OCaml for the bulk of product code, Rust for a few hand-tuned kernels via FFI. That blend is powerful and practical.
You don’t need a hero. You need a path.
The 10-Minute Decision Tree
Start
|
|-- Do you need strict no-GC guarantees or very tight p95/p99?
| |
| +-- Yes --> Rust
| |
| +-- No --> continue
|
|-- Do you want the fastest time-to-feature with native code?
| |
| +-- Yes --> OCaml
| |
| +-- No --> continue
|
|-- Are you wrapping many system calls or unsafe boundaries?
| |
| +-- Yes --> Rust (ergonomic unsafe + borrow checker)
| |
| +-- No --> OCaml is simpler for most product code
|
|-- Team background:
| |
| +-- Strong FP & pattern matching? --> OCaml
| +-- Strong systems & API edges? --> Rust
|
Finish: commit for one quarter, measure on your hardware, and iterate.
What “Footprint” Really Means Here
- Binary size: how compact the thing you ship is after
--releaseandstrip. - Runtime: how much background help the program needs (GC, schedulers).
- Allocation discipline: whether your hot paths can reuse buffers and keep memory close to the CPU.
- FFI posture: how easy it is to dip into lower-level code for the few spots that deserve it.
Rust and OCaml both give you real control. They just bet on different defaults.
Two Code Paths That Respect the Metal
Both snippets read a file in fixed-size chunks, reuse a buffer, and keep the inner loop clean. This is the shape you want when speed matters but clarity still rules.
Rust (2025): no-GC, explicit ownership, compact hot loop
use std::fs::File;
use std::io::{Read, Result};
fn sum_bytes(path: &str) -> Result<u64> {
let mut f = File::open(path)?;
let mut buf = [0u8; 1 << 20]; // 1 MiB reusable buffer
let mut total: u64 = 0;
loop {
let n = f.read(&mut buf)?;
if n == 0 { break; } // EOF
let mut i = 0;
while i < n {
// Branchless enough for a scan; zero allocations here
total = total.wrapping_add(buf[i] as u64);
i += 1;
}
}
Ok(total)
}
fn main() -> Result<()> {
let path = std::env::args().nth(1)
.expect("usage: scan <file>");
println!("{}", sum_bytes(&path)?);
Ok(())
}
Why this sings
Ownership makes it clear who frees what. The loop works on a stack-allocated buffer. No allocator on the hot path. When you need even more, you can move the loop into a separate crate and tune it further with unsafe under tests that pin the behavior.
OCaml 5 (native): lean runtime, pattern-matching comfort, tight loop
(* Build with:
ocamlopt -O3 unix.cmxa scan.ml -o scan
*)
open Unix
let sum_bytes filename =
let fd = openfile filename [O_RDONLY] 0 in
let ic = in_channel_of_descr fd in
let buf = Bytes.create 1_048_576 in (* 1 MiB reusable buffer *)
let total = ref 0L in
(try
while true do
let n = input ic buf 0 (Bytes.length buf) in
if n = 0 then raise End_of_file;
for i = 0 to n - 1 do
(* local mutation, functional result *)
total := Int64.add !total
(Int64.of_int (Char.code (Bytes.get buf i)))
done
done
with End_of_file -> ());
close_in_noerr ic; close_noerr fd; !total
let () =
match Sys.argv with
| [| _; fn |] -> Printf.printf "%Ld\n%!" (sum_bytes fn)
| _ -> prerr_endline "usage: scan <file>"; exit 2
Why this sings
You get native code from ocamlopt. You keep allocations off the inner loop. You still write in an expressive style. For hotspots that deserve it, you can push a low-level kernel through FFI and keep the rest in pleasant OCaml.
A Hand-Drawn Map Of Two Architectures That Actually Scale
OCaml core with surgical kernels
+---------------------------------------+
| Product Logic (OCaml) |
| ADTs, pattern matching, fast edits |
+-------------------+-------------------+
|
v
FFI for hotspots (Rust/C)
small, measured kernels only
Rust service with principled boundaries
+----------------------------------------+
| Rust Service |
| clear modules, lifetimes for sharing |
+------------+---------------------------+
|
v
I/O + compute layers with
no-GC control and predictable tails
Pick the drawing that matches the stakes you carry.
About Benchmarks And Sizes (Read Before You Publish Numbers)
Real numbers belong to the machine that will run your program. Hardware differs. Kernels differ. Filesystems matter. If you post one set of numbers as universal truth, you’re doing your readers a disservice.
Here is the honest way to handle it without turning your piece into guesswork:
- Show the method you used.
- Show the commands someone else can run.
- Explain what to look for: mean time, p95, memory, and the cost of a feature flag or an allocation pattern.
- Encourage readers to run it on their gear and post results. That creates discussion worth reading.
This is how you keep trust high while still honoring the words in the title.
How I Measure On My Side (So You Can Mirror It)
Below are commands that let anyone reproduce the same steps. No assumptions about fancy tooling.
Build in release and remove symbols
# Rust
cargo build --release
strip target/release/scan
# OCaml
ocamlopt -O3 unix.cmxa scan.ml -o scan
strip ./scan
Compare wall-clock runs with a large file
hyperfine -w 3 'target/release/scan big.bin' './scan big.bin'
Glance at CPU events and memory
# Either binary
perf stat -d ./scan big.bin
Look for what matters
- If p95 spreads out wildly under load, Rust likely wins that fight.
- If both are close, decide by developer speed, readability, and the simplicity of deployment.
- If a hotspot dominates, move only that hotspot to a lower-level kernel and keep the rest delightful.
I am not pasting machine-specific numbers here because they would be yours, not the reader’s. That is respect, not evasion.
Binary Size: The Levers That Move It
Rust
--releasewith LTO trims a lot.- Feature flags affect embedded code paths.
striphelps, and so does avoiding unused crates.
OCaml
- Native builds are compact by default.
- Avoid pulling in large C stubs you don’t need.
- Keep interfaces crisp; it often shrinks more than you expect.
Smaller is nice. Predictable is better. Measured is best.
Where Rust Feels Like Home
- You cannot afford GC pauses in the hot path.
- You will wrap many unsafe boundaries or kernel APIs.
- You expect the codebase and team to grow a lot.
- You want correctness enforced by the language itself.
Where OCaml Feels Like Home
- You want native speed and fast edits in the same day.
- Your domain logic is complex, and you want it readable.
- Your SLOs allow a compact runtime with a well-behaved GC.
- You want to keep most code in one pleasant place and push specialized work to FFI.
Both choices are responsible. The wrong choice is indecision.
Common Foot-Guns And How To Step Around Them
- Over-engineering the benchmark before shipping a single feature
- Chasing microseconds while ignoring confusing code paths
- Mixing too many paradigms without a clear boundary
- Forgetting that the code you can read next month is the fastest code you own
Your users never see your flame graph. They feel the moments when the product keeps its promise.
A Short Word On Emotion
Picking a language is not only technical. It’s also about the energy you bring to work. Rust gives a feeling of safety. OCaml gives a feeling of clarity. Choose the feeling that lets you build longer without burning out. That is not fluff. That is a strategy.
The Close
If the risk is tail latency, go with Rust. If the risk is shipping too slow, go with OCaml. If both risks are real, combine them with a clean boundary.
You deserve a stack that lets you keep promises and stay proud of your craft. Pick once. Ship. Measure on your hardware. Adjust in public. That rhythm builds loyal readers and happy users.
Postscript: What I Want To Hear From You
Share what you found when you ran the commands above on your machine. Post your mean, p95, memory, and the shape of your file set. If your results challenge my guidance, I’ll study them and write a follow-up that gives you credit.
This conversation is how we all get better at building things that last.
메타데이터
- post_id
- a23491b87f49
- slug
- rust-vs-ocaml-2025-benchmarks-binary-size-p95-decision-tree-a23491b87f49
- url
- https://medium.com/@the_atomic_architect/rust-vs-ocaml-2025-benchmarks-binary-size-p95-decision-tree-a23491b87f49
- canonical_url
- https://medium.com/@the_atomic_architect/rust-vs-ocaml-2025-benchmarks-binary-size-p95-decision-tree-a23491b87f49
- author_url
- https://medium.com/@the_atomic_architect
- status
- ok
- fetched_at
- 2026-07-15 11:02:28