Skip to content
hm::dev
← all posts

When Rust is worth it in a TypeScript-first stack

updated 2026-08-25 · by Hagop Minassian

Rust doesn’t belong in most of my client work. I say that up front because the honest answer to “should I use Rust” is no, almost every time, and I’d rather state that than build up to it. But one job this year hit a wall that TypeScript couldn’t get past no matter how I rewrote it, and that’s the case worth walking through, because it’s the only one that tells you anything about where the real line sits.

The job that was too slow to ignore

A client ingests supplier CSVs, some running past 400,000 rows, and needs every row diffed against the previous import to flag price and stock changes before the file hits Postgres. The first version was plain Node: stream the CSV, parse each row, hash it, compare against the prior hash stored in a lookup table. On a 50,000-row file it ran in under four seconds. On the 400,000-row files, the ones that actually mattered, it climbed past six minutes and once took down the worker’s memory limit entirely.

I profiled it before touching anything. The parsing wasn’t the problem. The hashing loop was, and so was the object allocation happening for every single row just to build a comparison key.

Where the TypeScript version actually broke down

I tried the obvious fixes first. Swapped the CSV parser for a faster one. Replaced the naive string concatenation with a typed buffer. Moved the hash comparison to a Map keyed by row ID instead of object lookups. Got the 400,000-row file down to about ninety seconds. Better, but still too slow for the ten-minute window the client’s overnight job had before the next system picked up the output.

The remaining cost wasn’t a bad algorithm. It was V8 doing what V8 does at that volume: garbage collection pauses from millions of short-lived objects, and a hashing function that, even using Node’s built-in crypto module, spent more time crossing between JS and native code than doing actual work. That’s the specific shape of problem Rust is good for. Not “this is slow,” but “this is slow because of allocation and hashing overhead at a scale where every microsecond per row multiplies into minutes.”

Building the Rust piece without rewriting everything

I didn’t rewrite the pipeline. I wrote one function: parse and hash a batch of rows, return the diff. Compiled it as a native Node addon with napi-rs, called from the same TypeScript service that already handled the CSV streaming, the Postgres writes, and the job scheduling. The Rust side never sees a database connection or an HTTP request. It takes a buffer, returns a buffer. That’s the whole interface.

That batch function took a day to write and most of a second day to get the napi-rs build working in the existing GitHub Actions pipeline, since it needed a prebuilt binary per target platform rather than a compile step at deploy time. The 400,000-row file went from ninety seconds to just under seven.

What it cost to add Rust

A second toolchain in the repo. A second CI job to maintain, one that builds and caches native binaries for the platforms the app deploys to. A contributor who wants to touch that one file now needs cargo installed, and the two people on this project who don’t do systems work at all won’t be the ones fixing a segfault in it if one shows up. I’m the only one who’s touched that file since I wrote it, and that’s a real cost, not a footnote.

None of that was worth paying for the CSV parsing, the API routes, the auth middleware, or anything else in that service. All of that stayed in TypeScript, and should.

The line I actually use now

I reach for Rust when a specific, isolated piece of code is slow for a reason that’s structural to the runtime, not fixable by better JS, and when that piece can sit behind a narrow interface the rest of the codebase never has to know about. If I can’t draw a box around it that small, it stays in TypeScript, even if that means accepting a slower number.

Next
Cutting a GitHub Actions pipeline from 8 minutes to under 2