A decision table for your next backend
This is the last article in the track, and it does the thing the overview promised: turns five articles of measurements into a decision you can actually make. The five before it established what each axis is worth on a real machine — a 4× throughput gap that depends on which Rust you write, a tie on parked tasks, a compiler that checks one language’s error paths and not the other’s, a 4× binary-size gap running the other way, and two hiring pools of nearly equal size with very different shapes. None of those numbers, on its own, picks a language. The question this article answers is how to combine them, and the honest answer is that the combination depends on the project — so the deliverable is not a verdict but a table, plus the weights that make the table produce different verdicts for different projects.
The scoring is deliberately crude. Each criterion gets a weight, each language a 0–5 score per criterion, and the weighted sum decides. That is not because backend decisions are arithmetic — they are not — but because writing the weights down forces the argument to happen in the right place. “Rust compiles slowly” and “Go has a deeper hiring pool” stop being slogans and become line items that a team can raise, lower, or delete before the totals ever get computed.
What the track measured
Section titled “What the track measured”One paragraph of recap, with the articles the numbers come from:
- Throughput and per-request cost — the typed Rust JSON API handled 121,256 req/s against
Go’s 29,528, and allocated 7.9 KB per request against 61.7 KB. The comfortable Rust version
using
serde_json::Valuelost to Go by 3×. The gap is real but conditional on writing the typed version, which costs more upfront (performance). - Concurrency — 10,000 parked tasks re-woken after 40 ms: tokio and Go tie on wall time, and tokio used 7.5 MB to Go’s 33.3 MB. The asymmetry is in failure modes: blocking a goroutine is nearly impossible to do by accident, while the same-looking call inside an async Rust task stalls a whole worker thread (concurrency).
- Error handling — Go’s
if err != nilmakes every error path visible and greppable but checks none of them; Rust’s?plus exhaustive match turns forgotten cases into compile errors, at the price of upfront plumbing. Happy-path cost is nil in both (error handling). - Deployment — both ship one static file that runs in an empty container. Go’s floor is about 1.5 MB against Rust’s 343 KB; Go cross-compiles to any target with two environment variables; Rust pays minutes per build where Go pays tens of seconds (deployment).
- People — 5.1 million Rust developers against 5.0 million Go (SlashData, Q1 2025), but Go recruits from a far deeper effective pool because Java and Python backends become productive in weeks. Rust’s retention is the highest of any language measured, around 72% admired (hiring).
The criteria and the default weights
Section titled “The criteria and the default weights”For a typical backend service — an API in front of a database, an internal tool, a CRUD layer — the weights below are a defensible starting point. They encode one judgement call: for the average project, iteration speed and the people axis together outweigh raw throughput, because most services spend their time waiting on upstream calls and most teams ship more often than they saturate a core.
| criterion | weight | evidence it is scored from |
|---|---|---|
| Throughput and per-request cost | 20 | typed Rust 4× Go; comfortable Rust 0.3× Go |
| Concurrency survives blocking calls | 15 | tokio and Go tie when parked; Go immune to the blocking mistake |
| Error paths checked | 15 | Rust: compile error; Go: visible but unchecked |
| Iteration speed | 20 | tens of seconds vs minutes per build |
| Deployment artifact | 5 | both static single binaries; size gap real but small in absolute terms |
| Hiring pool and onboarding | 15 | Go’s effective pool deeper; Rust ramp longer |
| Team retention after two years | 10 | 72% admired vs well behind |
Two things to notice about the weights rather than the scores. Deployment gets 5 because the deployment article’s own conclusion was that neither side’s numbers are a reason to pick a language — the difference that matters is build time, which is already counted under iteration. And retention gets 10 rather than more because it pays out in year two, while hiring and onboarding costs land in quarter one and weigh on the decision now.
Scoring three projects
Section titled “Scoring three projects”The scores below are 0–5 judgements anchored to those measurements, not measurements themselves — that is the point of writing them down where they can be argued with. Three archetypes cover most backend work: the typical IO-bound API from the weights above, a CPU-bound service (parsing, image work, compression, anything where the request spends its time in your code), and something deployed where resources are capped — an edge device, a sidecar, a function with a memory bill.
The program that computes the totals, criteria and weights included, so the arithmetic is checkable and rerunnable:
fn main() { // (criterion, weight, rust score, go score) — the tables above let typical = [ ("Throughput and per-request cost", 20, 4, 3), ("Concurrency survives blocking calls", 15, 4, 5), ("Error paths checked", 15, 5, 3), ("Iteration speed", 20, 2, 5), ("Deployment artifact", 5, 4, 4), ("Hiring pool and onboarding", 15, 3, 5), ("Team retention after two years", 10, 5, 3), ]; let cpu_bound = [ ("Throughput and per-request cost", 35, 5, 2), ("Concurrency survives blocking calls", 10, 3, 4), ("Error paths checked", 15, 5, 3), ("Iteration speed", 10, 2, 5), ("Deployment artifact", 10, 4, 4), ("Hiring pool and onboarding", 10, 3, 5), ("Team retention after two years", 10, 5, 3), ]; let edge = [ ("Throughput and per-request cost", 20, 4, 3), ("Concurrency survives blocking calls", 10, 4, 5), ("Error paths checked", 10, 5, 3), ("Iteration speed", 10, 2, 5), ("Deployment artifact", 30, 5, 3), ("Hiring pool and onboarding", 10, 3, 5), ("Team retention after two years", 10, 5, 3), ]; let projects = [ ("Typical IO-bound API", &typical), ("CPU-bound service", &cpu_bound), ("Edge or resource-capped", &edge), ]; for (name, rows) in projects { let max: u32 = rows.iter().map(|r| r.1 * 5).sum(); let (r, g) = rows.iter().fold((0, 0), |acc, row| { (acc.0 + row.1 * row.2, acc.1 + row.1 * row.3) }); println!( "{name:<24} Rust {r:>3}/{max} ({:.0}%) Go {g:>3}/{max} ({:.0}%)", r as f32 / max as f32 * 100.0, g as f32 / max as f32 * 100.0, ); }}Output:
Typical IO-bound API Rust 370/500 (74%) Go 405/500 (81%)CPU-bound service Rust 420/500 (84%) Go 325/500 (65%)Edge or resource-capped Rust 420/500 (84%) Go 360/500 (72%)Reading the three lines is the whole decision, and it is worth spelling out why each one lands where it does.
The typical API goes to Go, narrowly. Rust wins every axis the hardware can see — throughput, allocation, task memory, error checking — and still loses, because the weights say a CRUD backend’s real constraints are how often the team can ship and who it can hire. The margin is seven points, which is close enough that one changed weight flips it: a team with Rust experience already in house deletes half of the hiring penalty and the result reverses. That is not a weakness of the table; that is the table working.
The CPU-bound service goes to Rust, clearly. Raising throughput’s weight to 35 and scoring Go 2 on it reflects the one measured gap that survived every caveat in its article: 4× on throughput, 8× on allocation, in the workload where the request actually computes. Iteration speed still favours Go, but at weight 10 it no longer matters. The 19-point margin is the largest in the set, and it matches what the performance article measured directly.
The resource-capped deployment goes to Rust, and the reason is narrower than it looks. Deployment jumps to weight 30 because the 4× binary-size gap and the 7.5 MB against 33.3 MB task footprint are the two numbers in this track that change what fits on the machine at all. Go keeps its edge on cross-compilation and hiring; it loses on the thing the hardware bills for.
What the table cannot do
Section titled “What the table cannot do”Three honest limits, because a decision tool that hides its failure modes is marketing.
The scores are judgement. The measurements are real, but “Rust 4, Go 3” on throughput for a
typical API compresses a conditional result — 4× if the team writes typed serde, 0.3× if it
reaches for Value — into one number. A team that knows it will write the comfortable version
should score that row 2 against 3, not 4 against 3, and the typical-API verdict flips the other
way at 81% to 78%. Change the score to match the team, not the benchmark.
The weights are the decision. The table produces the verdict the weights imply, which means the useful argument happens before the multiplication: is iteration speed worth the same as error checking? For a prototype, no — it is worth several times as much, and the prototype should be scored as one. For a payment path that will run for a decade, error checking and retention both go up. The table is a way to have that argument in the open.
Close totals mean pick the team, not the score. When the two results land within a few points, the measurement says the languages are interchangeable for this project, and the deciding factors left are the ones no table weighs: what the team already knows, what the adjacent services are written in, and which of the two the team wants to be writing in two years — where Rust’s 72% retention is data and not sentiment.
Run it yourself
Section titled “Run it yourself”The scoring program is the only code on this page, and it needs nothing but the standard library:
cargo new decision && cd decision# paste src/main.rs from abovecargo runEvery number feeding the weights comes from the four earlier articles, each of which shows the program that produced it and the command to rerun it. If a weight above does not match your project, change it — the table’s whole value is that it changes its answer when you do.
Related
Section titled “Related”- Track overview: Rust vs Go: choosing a backend stack
- Performance in real workloads — the measurements behind the throughput weights
- Hiring and team adoption — the measurements behind the people weights