{"id":91,"date":"2026-04-16T18:57:29","date_gmt":"2026-04-16T18:57:29","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/?p=91"},"modified":"2026-04-16T18:57:29","modified_gmt":"2026-04-16T18:57:29","slug":"rust-vs-go-2026-what-i-reach-for","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/rust-vs-go-2026-what-i-reach-for\/","title":{"rendered":"Rust vs Go in 2026: what I actually reach for, and when"},"content":{"rendered":"<p>Confession: I spent a weekend last month rewriting the same tiny service, a CSV-to-JSON transformer with a small HTTP API, mostly because I had been losing an internal argument about which language to reach for on a side project. I lost the argument anyway, but I walked away with a cleaner answer than the usual &ldquo;it depends.&rdquo;<\/p>\n<p>This isn&rsquo;t a benchmark post. There are plenty of those, and most of them test something I don&rsquo;t care about. These are the notes I took while building the same thing twice: what was annoying, what was fast, and what broke in production when I ran it for a week. If you&rsquo;re stuck picking between Rust and Go for a 2026 project, I hope this saves you a weekend.<\/p>\n<h2 id=\"the-service-i-built-twice\">The service I built (twice)<\/h2>\n<p>Nothing fancy. A CLI plus an HTTP endpoint that accepts a CSV upload, parses it with a schema, does some light validation, and streams back newline-delimited JSON. Maybe 300 lines of real code in each language. I deliberately picked a workload that touches the stuff people argue about: allocation, async IO, error handling, and JSON.<\/p>\n<p>The Go version used <code>net\/http<\/code>, <code>encoding\/csv<\/code>, and <code>encoding\/json<\/code>. Around 280 lines.<\/p>\n<p>The Rust version used axum 0.7, serde, the <code>csv<\/code> crate, and tokio. Around 340 lines.<\/p>\n<p>Both ran behind the same nginx sidecar on a small Hetzner box. I stress-tested each one with <code>oha<\/code> at 500 concurrent connections.<\/p>\n<h2 id=\"where-go-wins-on-actual-delivery-speed\">Where Go wins on actual delivery speed<\/h2>\n<p><img decoding=\"async\" alt=\"Rust vs Go in 2026: what I actually reach for, and when\" src=\"https:\/\/abrarqasim.com\/blog\/wp-content\/uploads\/2026\/04\/rust-vs-go-2026-what-i-reach-for-inline-1776344492.png\"><\/p>\n<p>I had the Go version running end-to-end in about 90 minutes. The Rust version took the better part of a day, and I&rsquo;ve written real Rust for three years.<\/p>\n<p>Most of that gap isn&rsquo;t the compiler fighting me. It&rsquo;s the ecosystem. In Go, <code>encoding\/csv<\/code> is in the standard library and it just works. In Rust, you pick between the <code>csv<\/code> crate, <code>csv-async<\/code>, or rolling your own streaming parser, and the second you want async reading, the decision tree splinters across three or four crates.<\/p>\n<p>Here&rsquo;s the Go handler that does almost everything:<\/p>\n<pre><code class=\"language-go\">func transform(w http.ResponseWriter, r *http.Request) {\n    reader := csv.NewReader(r.Body)\n    reader.FieldsPerRecord = -1\n    enc := json.NewEncoder(w)\n    header, err := reader.Read()\n    if err != nil {\n        http.Error(w, err.Error(), 400)\n        return\n    }\n    for {\n        row, err := reader.Read()\n        if err == io.EOF {\n            return\n        }\n        if err != nil {\n            http.Error(w, err.Error(), 400)\n            return\n        }\n        out := make(map[string]string, len(header))\n        for i, h := range header {\n            out[h] = row[i]\n        }\n        if err := enc.Encode(out); err != nil {\n            return\n        }\n    }\n}\n<\/code><\/pre>\n<p>That reads, transforms, and streams. Any Go dev on the team can maintain it without a walkthrough. The equivalent Rust handler with axum and an async <code>csv<\/code> wrapper was about twice the length, mostly because I had to thread generic bounds through the streaming body type.<\/p>\n<p>If &ldquo;ship it by Friday&rdquo; is the constraint, Go is still the right tool. Rust asks you to make more decisions upfront, and a lot of them don&rsquo;t pay off on a 300-line service.<\/p>\n<h2 id=\"where-rust-wins-once-the-code-is-alive\">Where Rust wins once the code is alive<\/h2>\n<p>The interesting thing happened after I had been running both for a week.<\/p>\n<p>The Go version had three panics in production over the first week. All three were the same category: I had missed a nil check on a map lookup deep in the validation code. A runtime panic in a goroutine that I didn&rsquo;t recover, which crashed the request handler cleanly, but still, three bugs I shouldn&rsquo;t have shipped.<\/p>\n<p>The Rust version had zero. Not because I&rsquo;m a better Rust programmer (I&rsquo;m not) but because the type system wouldn&rsquo;t let me deploy the bug. <code>Option&lt;T&gt;<\/code> forced me to handle the missing case at the place where it mattered, not three call frames later.<\/p>\n<p>This is the real Rust sales pitch for me. Not performance. Not memory. The fact that the set of things that can go wrong at runtime is genuinely smaller.<\/p>\n<p>Here&rsquo;s the Rust validator I ended up with:<\/p>\n<pre><code class=\"language-rust\">async fn validate_row(\n    row: &amp;StringRecord,\n    schema: &amp;Schema,\n) -&gt; Result&lt;ValidatedRow, ValidationError&gt; {\n    let name = row.get(schema.name_idx)\n        .ok_or(ValidationError::MissingField(&quot;name&quot;))?;\n    let age: u32 = row.get(schema.age_idx)\n        .ok_or(ValidationError::MissingField(&quot;age&quot;))?\n        .parse()\n        .map_err(|_| ValidationError::BadType(&quot;age&quot;))?;\n    Ok(ValidatedRow { name: name.into(), age })\n}\n<\/code><\/pre>\n<p>Every error case shows up in the function signature. You can&rsquo;t forget one.<\/p>\n<h2 id=\"memory-and-throughput-the-numbers-from-my-laptop\">Memory and throughput: the numbers from my laptop<\/h2>\n<p>I ran both under <code>oha -c 500 -n 50000<\/code> on the same Hetzner CX22:<\/p>\n<table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Go<\/th>\n<th>Rust<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>RSS at steady state<\/td>\n<td>78 MB<\/td>\n<td>24 MB<\/td>\n<\/tr>\n<tr>\n<td>Requests\/sec<\/td>\n<td>18,400<\/td>\n<td>24,700<\/td>\n<\/tr>\n<tr>\n<td>p99 latency<\/td>\n<td>42 ms<\/td>\n<td>19 ms<\/td>\n<\/tr>\n<tr>\n<td>Binary size (stripped)<\/td>\n<td>9.1 MB<\/td>\n<td>5.8 MB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Rust wins on every metric. I want to be honest about what that means, though. At the scale I&rsquo;m actually running this service, maybe 200 req\/min on a good day, nobody would ever notice the difference.<\/p>\n<p>The real money with Rust shows up when you&rsquo;re paying for memory, not CPU. If you&rsquo;re running 30 instances of a Go service and Rust cuts RSS by 3x, that&rsquo;s a real bill. On a side project, it&rsquo;s a rounding error.<\/p>\n<h2 id=\"concurrency-goroutines-vs-async-rust-honestly\">Concurrency: goroutines vs async Rust, honestly<\/h2>\n<p>I&rsquo;ve seen plenty of takes on this. Here&rsquo;s mine, after building the same thing in both.<\/p>\n<p>Goroutines are easier. They compose better. <code>go func() {}()<\/code> is the simplest concurrency primitive in any mainstream language. You think less. You ship faster. The scheduler just works.<\/p>\n<p>Async Rust is more expressive. It is also significantly more expensive to learn. The first time you hit a <code>Send<\/code> bound error because your closure captured an <code>Rc&lt;RefCell&lt;T&gt;&gt;<\/code>, you will want to throw your laptop out a window. Once you internalize the model, though, the compile-time guarantees are remarkable. Things that would be a race in Go are a type error in Rust.<\/p>\n<p>If you like the style of &ldquo;here is what this tool actually does to your code,&rdquo; I did a similar deep dive on <a href=\"https:\/\/abrarqasim.com\/blog\/tailwind-css-v4-migration-guide-what-actually-changed\" rel=\"noopener\">the Tailwind v4 migration<\/a> that went through the same kind of &ldquo;rewrite and see what breaks&rdquo; process.<\/p>\n<p>My honest take: if your concurrency is &ldquo;fan out N requests and wait,&rdquo; Go is a better tool. If your concurrency involves shared mutable state that actually matters, Rust pays for itself.<\/p>\n<h2 id=\"what-i-reach-for-now-and-when-i-switch\">What I reach for now (and when I switch)<\/h2>\n<p>After the weekend and the week of running both, I&rsquo;ve settled into a rough rule I use for my own projects. I do a lot of this kind of &ldquo;pick the right tool for the job&rdquo; work in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my consulting practice<\/a>, and the Rust-vs-Go answer mostly comes down to two questions.<\/p>\n<p>Default to Go if the service is going to be small, the team already knows Go, you&rsquo;re IO-bound, or you need to ship this week. The standard library will carry you a long way, and anyone can maintain it.<\/p>\n<p>Default to Rust if the service is going to run a long time, memory pressure is real, you&rsquo;re doing anything CPU-bound, or the cost of a runtime bug is high. The up-front investment pays back in the things that don&rsquo;t break at 3am.<\/p>\n<p>Don&rsquo;t pick either based on a benchmark chart. Pick based on who is maintaining the code a year from now, and what they&rsquo;ll be debugging at 3am.<\/p>\n<h2 id=\"one-thing-you-can-do-this-week\">One thing you can do this week<\/h2>\n<p>If you&rsquo;ve only ever used one of these languages, pick a small service you actually run, an endpoint, a cron job, anything around 200 lines, and rewrite it in the other language. Deploy it for a week alongside the original. You&rsquo;ll learn more in that week than in any benchmark post (including this one). The <a href=\"https:\/\/rust-lang.github.io\/async-book\/\" rel=\"nofollow noopener\" target=\"_blank\">Rust async book<\/a> and the <a href=\"https:\/\/go.dev\/blog\/context\" rel=\"nofollow noopener\" target=\"_blank\">Go blog on context<\/a> are both worth reading first, but the real understanding comes from the rewrite.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I rebuilt the same service in Rust and Go over a weekend. Here is what I actually reach for in 2026, with numbers, code, and a real rule I use now.<\/p>\n","protected":false},"author":2,"featured_media":89,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I rebuilt the same service in Rust and Go over a weekend. Here is what I actually reach for in 2026, with numbers, code, and a real rule I use now.","rank_math_focus_keyword":"rust vs go","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[67,47,19,65,64,66],"class_list":["post-91","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-comparison","tag-golang","tag-performance","tag-programming-languages","tag-rust","tag-web-services"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/91","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=91"}],"version-history":[{"count":1,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/91\/revisions"}],"predecessor-version":[{"id":92,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/91\/revisions\/92"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/89"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=91"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=91"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=91"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}