{"id":407,"date":"2026-03-09T18:20:33","date_gmt":"2026-03-09T18:20:33","guid":{"rendered":"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/deno-20-in-production-2026-migration-from-nodejs-a\/"},"modified":"2026-03-18T22:00:05","modified_gmt":"2026-03-18T22:00:05","slug":"deno-20-in-production-2026-migration-from-nodejs-a","status":"publish","type":"post","link":"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/deno-20-in-production-2026-migration-from-nodejs-a\/","title":{"rendered":"Deno 2.0 in Production: Six Months of Migration From Node.js and What Actually Changed"},"content":{"rendered":"<p>I resisted Deno for years. Partly stubbornness, mostly because the original pitch \u2014 &#8220;what if Node but without npm&#8221; \u2014 felt like solving the wrong problem when you have a working product and a team that knows the Node ecosystem cold. Then Deno 2.0 dropped in October 2024 with full npm compatibility, and one of my teammates (Priya, our resident runtime nerd) kept saying &#8220;no seriously, look <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/cloudflare-workers-vs-aws-lambda-which-edge-runtim\/\" title=\"at the\">at the<\/a> tooling story.&#8221; I gave in.<\/p>\n<p>We&#8217;re a three-person backend team running a Node\/TypeScript API that handles event processing for a SaaS product. Not huge \u2014 around 800 req\/s peak, PostgreSQL backend, a handful of third-party integrations. The kind of service that&#8217;s boring by design. I started the migration in September 2025 on a non-critical service as a proving ground, then moved one of our production APIs to Deno 2.2.x in January. Here&#8217;s <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/rag-deep-dive-chunking-strategies-vector-databases\/\" title=\"What I Learned\">what I learned<\/a>.<\/p>\n<h2>What &#8220;Node.js Compatible&#8221; Means in Practice (It&#8217;s Not Magic)<\/h2>\n<p>The headline feature of Deno 2.0 was npm compatibility via the <code>npm:<\/code> specifier, and it works better than I expected \u2014 with a few important asterisks.<\/p>\n<p>Most of your npm dependencies just&#8230; work. Express, Fastify, zod, axios, date-fns, jose \u2014 all loaded fine. You drop a <code>deno.json<\/code> <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/rag-deep-dive-chunking-strategies-vector-databases\/\" title=\"in the\">in the<\/a> root and reference them:<\/p>\n<pre><code class=\"language-jsonc\">\/\/ deno.json\n{\n  &quot;imports&quot;: {\n    &quot;express&quot;: &quot;npm:express@^4.21.0&quot;,\n    &quot;@\/&quot;: &quot;.\/src\/&quot;\n  },\n  &quot;tasks&quot;: {\n    &quot;dev&quot;: &quot;deno run --allow-net --allow-read --allow-env src\/main.ts&quot;,\n    &quot;test&quot;: &quot;deno test --allow-net --allow-env&quot;\n  }\n}\n<\/code><\/pre>\n<p>No <code>package.json<\/code>. No separate <code>tsconfig.json<\/code>. TypeScript works out of the box \u2014 Deno treats <code>.ts<\/code> files as a first-class citizen, so you skip the whole <code>ts-node<\/code> or <code>tsx<\/code> setup dance. For a greenfield project this feels obvious. Migrating an existing codebase, you carry two setups for a while, which is annoying but manageable.<\/p>\n<p>Where it gets complicated: native addons. We use a library with some native C++ bindings, and that required <code>--allow-all<\/code> plus some creative workarounds. Deno 2.x can handle a <code>node_modules<\/code> directory being present (you can run <code>deno install<\/code> and it&#8217;ll populate one), but native addons are still an edge case that isn&#8217;t fully ironed out. I ended up keeping that particular integration on a small Node sidecar. Not ideal, but the blast radius was small.<\/p>\n<p>Prisma was the other thing. By early 2026 the Deno-Prisma story has improved considerably compared to 2024 \u2014 Prisma 6.x has much better first-class support \u2014 but there were edge cases around the query engine binary that took me an afternoon to debug. The GitHub issue tracker (prisma\/prisma #24218, if you want to go spelunking) has the painful details.<\/p>\n<p>If your dependency list is mostly pure-JS\/TS packages, the migration is low-friction. If you have native addons or heavy meta-frameworks, scope that work separately before you commit.<\/p>\n<h2>The Tooling Story Is the Real Selling Point<\/h2>\n<p>I&#8217;ve written probably a dozen ESLint configs in my life. Each one slightly different. Each one with at least one person on the team who disagrees with the semicolon rule. Deno ships with a formatter (<code>deno fmt<\/code>) and a linter (<code>deno lint<\/code>) that are opinionated, fast, and require zero configuration. You just run them. No <code>eslint.config.js<\/code>, no <code>.prettierrc<\/code>, no argument about whether trailing commas go after the last function parameter.<\/p>\n<p>The test runner is similarly no-ceremony:<\/p>\n<pre><code class=\"language-typescript\">\/\/ handlers\/health_test.ts\nimport { assertEquals } from &quot;jsr:@std\/assert&quot;;\nimport { createApp } from &quot;..\/src\/app.ts&quot;;\n\nDeno.test(&quot;GET \/health returns 200&quot;, async () =&gt; {\n  const app = createApp();\n  const req = new Request(&quot;http:\/\/localhost\/health&quot;);\n  const res = await app.fetch(req);\n  \/\/ No test framework config, no jest.config.ts, no separate setup file\n  assertEquals(res.status, 200);\n});\n<\/code><\/pre>\n<p><code>deno test<\/code> picks that up automatically. For a small team where everyone&#8217;s already stretched thin, removing that category of tooling toil mattered more than I expected.<\/p>\n<p><code>deno compile<\/code> is underrated for deployment. We can now ship a single self-contained binary of one of our smaller services, which simplified our Docker setup considerably. The binary is larger than I&#8217;d like \u2014 roughly 80MB for a small API \u2014 but cold starts are nonexistent and the deployment story is much cleaner.<\/p>\n<p>I know the permissions model is the first thing people push back on. I was annoyed by it initially \u2014 <code>--allow-net<\/code> and <code>--allow-read<\/code> and <code>--allow-env<\/code> feel like ceremony when you&#8217;re trying to get something running fast. But I pushed a config change on a Friday afternoon that accidentally let a dependency try to write to <code>\/tmp<\/code> in a way we hadn&#8217;t expected, and the permission system caught it before it reached prod. You get a stack trace pointing exactly to where the filesystem access was attempted. In Node you&#8217;d have gotten nothing, until something went wrong downstream. I&#8217;m a convert.<\/p>\n<h2>The Night Something Actually Broke in Production<\/h2>\n<p>Most migration writeups skip this part. Here it is.<\/p>\n<p>We had a memory leak. Not from our code \u2014 from a combination of our event loop handling and a third-party WebSocket library that had slightly different behavior under Deno&#8217;s runtime than under Node. The leak was slow enough that <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/edge-computing-in-2026-why-developers-are-adopting\/\" title=\"It Took\">it took<\/a> about six hours <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/cloudflare-workers-vs-aws-lambda-which-edge-runtim\/\" title=\"of Production\">of production<\/a> traffic to manifest, which meant we caught it around 2am on a Tuesday. Not fun.<\/p>\n<p>The root cause: Deno uses Web-standard APIs wherever possible. <code>WebSocket<\/code> in Deno behaves according to the browser spec, which is great for consistency, but some npm WebSocket libraries have code paths that assume Node&#8217;s <code>net<\/code>\/<code>stream<\/code> internals and fall back to different \u2014 in this case, leaky \u2014 behavior when those aren&#8217;t present. The library in question was <code>ws@8.x<\/code>, and the fix was switching to Deno&#8217;s native WebSocket API, which meant rewriting a chunk of our connection management layer.<\/p>\n<p>What surprised me \u2014 and I thought I understood the compatibility layer well by that point \u2014 was how hard it was to spot <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/rag-deep-dive-chunking-strategies-vector-databases\/\" title=\"in the\">in the<\/a> heap snapshots. The leak showed <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/05\/github-copilot-vs-cursor-vs-codeium-best-ai-coding\/\" title=\"Up in\">up in<\/a> what looked like native code, not in anything we&#8217;d written. I spent several hours convinced the problem was somewhere completely different before Priya ran a targeted reproduction script that isolated it.<\/p>\n<p>I&#8217;m not 100% sure this would have surfaced the same way if we&#8217;d been on <code>Deno.serve()<\/code> from the start rather than routing through Express. My hunch is yes, because the underlying WebSocket handling was in a shared module. But the failure mode was confusing enough that I&#8217;m not confident.<\/p>\n<p>What I&#8217;d do differently: before migrating, audit every package that touches network I\/O or streams, and specifically check whether there are open issues about Deno compatibility. The npm compatibility layer is solid. It is not a guarantee.<\/p>\n<h2>Performance Reality Check \u2014 My Numbers, Not the Marketing Benchmarks<\/h2>\n<p>Every runtime publishes benchmarks showing itself winning. Here&#8217;s <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/docker-compose-vs-kubernetes-when-to-use-which-in\/\" title=\"What I Actually\">what I actually<\/a> saw on our hardware \u2014 a pair of AWS c6g.xlarge instances running ARM.<\/p>\n<p>For a basic JSON API endpoint (fetch from Postgres, serialize, return):<\/p>\n<ul>\n<li>Node 22.x + Fastify: ~42,000 req\/s, ~12ms p99 latency<\/li>\n<li>Deno 2.2 + <code>Deno.serve()<\/code>: ~47,000 req\/s, ~10ms p99 latency<\/li>\n<\/ul>\n<p>That&#8217;s real \u2014 roughly 11% higher throughput in our workload. But in actual production, with <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/09\/bun-vs-nodejs-in-production-2026-real-migration-st\/\" title=\"Real Traffic\">real traffic<\/a> patterns and database contention, it&#8217;s more like 5-8% better. I&#8217;ll take it. You&#8217;re not cutting your infra bill in half.<\/p>\n<p>Startup time is where Deno shines <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/edge-computing-in-2026-why-developers-are-adopting\/\" title=\"for Our\">for our<\/a> use case. Node 22 with TypeScript (we were using <code>tsx<\/code>) added about 400ms to cold starts. Deno 2.2 TypeScript startup for the same code was around 80ms. If you&#8217;re running short-lived workers or anything Lambda-adjacent, this matters a lot. For long-running API servers, not so much.<\/p>\n<p>Memory usage was roughly comparable, maybe 10% lower under Deno in steady state. Not a deciding factor either way.<\/p>\n<h2>My Actual Recommendation<\/h2>\n<p>If you&#8217;re starting a new TypeScript backend project right now, use Deno. The zero-config TypeScript, the built-in toolchain, the standards-first approach \u2014 these compound over the life of a project in ways that are hard to see upfront but very visible 12 months in when you&#8217;re not fighting tooling debt. JSR has grown meaningfully since 2024 and the <code>jsr:@std\/*<\/code> library covers most common tasks solidly.<\/p>\n<p>If you have an existing Node project, the calculus is harder. The migration path is real but not zero-cost, especially with native addons, heavy framework dependencies (don&#8217;t try Next.js or Remix \u2014 seriously), or a large team that needs to internalize the permissions model. We spent about <a href=\"https:\/\/blog.rebalai.com\/en\/2026\/03\/08\/ai-coding-assistant-benchmarks-real-world-performa\/\" title=\"Three Weeks\">three weeks<\/a> of total engineering time across the team on the migration, including the WebSocket incident. For a three-person team, that&#8217;s non-trivial.<\/p>\n<p>The thing that would push me toward migrating an existing project: if you&#8217;re already planning TypeScript tooling work \u2014 upgrading tsconfig, moving off an old bundler, updating your test setup \u2014 bundle the Deno migration into that work. You&#8217;re paying the context-switch cost anyway, so you might as well land somewhere better on the other side.<\/p>\n<p>One place I&#8217;d still stay on Node: anything with a hard dependency on Express middleware you can&#8217;t easily replace, or any project where native addons are a core dependency, not a peripheral one. The compatibility layer handles most things. Not everything.<\/p>\n<p>We&#8217;re keeping both migrated services on Deno, and I&#8217;m planning to move a third one over in Q2.<\/p>\n<p><!-- Reviewed: 2026-03-10 | Status: ready_to_publish | Changes: removed repeated \"genuinely\" usage (5 instances), rewrote permissions model paragraph opener, replaced \"I want to be honest\" construction with direct opener, removed AI meta-commentary from recommendation section, cut \"Practical takeaway:\" label, replaced \"genuinely good\" with \"solid\", removed redundant closing sentence --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I resisted Deno for years.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[1],"tags":[],"class_list":["post-407","post","type-post","status-publish","format-standard","hentry","category-general"],"_links":{"self":[{"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/posts\/407","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/comments?post=407"}],"version-history":[{"count":5,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/posts\/407\/revisions"}],"predecessor-version":[{"id":525,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/posts\/407\/revisions\/525"}],"wp:attachment":[{"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/media?parent=407"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/categories?post=407"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rebalai.com\/en\/wp-json\/wp\/v2\/tags?post=407"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}