<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Moon Robert</title>
    <description>The latest articles on DEV Community by Moon Robert (@synsun).</description>
    <link>https://dev.to/synsun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3803502%2F8d720774-f4ea-4382-ba04-b1b892bb540f.png</url>
      <title>DEV Community: Moon Robert</title>
      <link>https://dev.to/synsun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/synsun"/>
    <language>en</language>
    <item>
      <title>GitHub Copilot vs Cursor vs Windsurf: Which AI Coding Assistant Actually Makes You Faster in 2026</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:45:52 +0000</pubDate>
      <link>https://dev.to/synsun/github-copilot-vs-cursor-vs-windsurf-which-ai-coding-assistant-actually-makes-you-faster-in-2026-4db3</link>
      <guid>https://dev.to/synsun/github-copilot-vs-cursor-vs-windsurf-which-ai-coding-assistant-actually-makes-you-faster-in-2026-4db3</guid>
      <description>&lt;p&gt;Two weeks. Three tools. One Next.js codebase I actually ship to users.&lt;/p&gt;

&lt;p&gt;I'll be upfront: I went in expecting Cursor to win. I've been using it for about eight months and it became my default IDE somewhere around last summer. But my company started rolling out GitHub Copilot Enterprise licenses, and Windsurf kept appearing in threads where people claimed it was "doing things Cursor can't." So I did the thing — I actually rotated tools on real work, not toy projects.&lt;/p&gt;

&lt;p&gt;My setup: MacBook Pro M3 Max, TypeScript/React frontend with a Node.js API layer, roughly 85k lines of real production code. Three-person team. I switched tools every few days across actual tasks: building a new billing dashboard, refactoring our OAuth flow, and adding test coverage to a module that had basically none. High-stakes enough that the dumb suggestions were obvious and painful, real enough that good suggestions genuinely saved me.&lt;/p&gt;

&lt;p&gt;Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline Autocomplete Is Table Stakes, But There Are Real Differences
&lt;/h2&gt;

&lt;p&gt;All three tools are good at autocomplete now. I want to be honest about that — if you're still deciding on the basis of "which one finishes my for loops faster," that's probably not the right question anymore.&lt;/p&gt;

&lt;p&gt;That said, Copilot's completions feel the most conservative. It completes what you're typing. Cursor and Windsurf are more willing to speculate — they'll sometimes complete two or three lines ahead based on what they think you're trying to do, which is fantastic when they're right and mildly annoying when they're not.&lt;/p&gt;

&lt;p&gt;I noticed Cursor's ghost text tends to drift toward patterns it's seen in the rest of your file. Windsurf does something similar but pulls context from further away — I had it correctly infer a helper function signature from a file I hadn't opened in the current session. Surprising the first time it happens.&lt;/p&gt;

&lt;p&gt;One practical note: if you have a large TypeScript project with complex generics, Copilot gets confused more often than the other two. I don't know exactly why — probably model differences and how they handle the type context — but I hit this several times while working on our billing module, which is deep in generic utility types. Cursor and Windsurf both handled it better.&lt;/p&gt;

&lt;p&gt;The winner here is basically a tie between Cursor and Windsurf, with Copilot slightly behind in complex type-heavy situations. Your mileage may vary if your codebase is mostly Python or Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-File Editing Is Where You Either Win or Lose Hours
&lt;/h2&gt;

&lt;p&gt;This is the actual battleground in 2026. The ability to say "refactor this auth flow to use the new session model" and have the tool understand that it needs to touch six files, in the right order, without breaking the interfaces between them — that's the capability that separates the tools now.&lt;/p&gt;

&lt;p&gt;Cursor's Composer mode is mature. I've been using it for months and it has a very good intuition for dependency order. When I refactored our OAuth flow, I described what I wanted in a few sentences, and it correctly identified the files it needed to touch, showed me a plan, and executed it in a way that was maybe 85% right on the first pass. The remaining 15% was stuff I had to correct, but it surfaced the corrections clearly — it didn't silently do the wrong thing.&lt;/p&gt;

&lt;p&gt;Windsurf's Cascade is — okay, let me back up a second, because I was skeptical of this one. Codeium has been around for a while and I always thought of them as the "free tier" option, not a serious competitor. Cascade surprised me. The "flows" concept, where it tracks what it changed and why across a multi-step edit, gave me way more confidence in what it was doing. At one point I had it touch eight files to update our API client and it completed the whole thing without breaking a single type contract. I pushed this on a Friday afternoon thinking it would definitely need cleanup, and it just... didn't.&lt;/p&gt;

&lt;p&gt;(I also learned, the hard way, that if you interrupt Cascade mid-flow — close the panel, switch files before it finishes — it does not recover gracefully. Did this twice and ended up with half-applied changes that were more work to untangle than the original task. Don't do that.)&lt;/p&gt;

&lt;p&gt;GitHub Copilot's agent mode exists but it felt less confident in multi-file situations. It would often complete the primary file change correctly but then ask clarifying questions about the secondary files rather than just doing it. Which — maybe that's a design choice, and maybe it's the right one if you want more control. But in flow state, the extra confirmation prompts broke my concentration.&lt;/p&gt;

&lt;p&gt;Honest verdict for multi-file work: Windsurf slightly edges Cursor here, which I did not expect to say. Copilot agent mode lags behind both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and Chat: Who Actually Knows Your Codebase
&lt;/h2&gt;

&lt;p&gt;Here is the thing: there's a meaningful difference in how these tools understand your project, not just your open files. And it compounds over a full workday in ways that are hard to measure but easy to feel.&lt;/p&gt;

&lt;p&gt;Cursor's &lt;code&gt;@codebase&lt;/code&gt; indexing is solid and it updates incrementally as you work. When I asked it "where does our session token get validated?" it found the right middleware in about two seconds and gave me a useful summary with line references. Cursor Chat has become my default way to navigate unfamiliar parts of our repo — I use it more for exploration than for code generation at this point.&lt;/p&gt;

&lt;p&gt;Copilot Chat has improved a lot. The enterprise tier has workspace context and it does understand cross-file relationships better than it did six months ago. Where I found it weaker is in remembering the conversation thread — it loses context faster than Cursor does across a long session. I was debugging a gnarly race condition in our WebSocket handler, and about fifteen messages in, Copilot Chat started answering as if it had forgotten what I told it earlier. Cursor maintained the thread correctly.&lt;/p&gt;

&lt;p&gt;Windsurf's chat experience is good but slightly less polished in the UI. The context retrieval is excellent — arguably on par with Cursor — but the conversation flow feels a bit rougher. It's clearly an area they're still building. They shipped a significant update sometime in February, so this might already be different by the time you read this.&lt;/p&gt;

&lt;p&gt;One thing I noticed: Windsurf surfaces potential side effects more proactively. I asked it to "just quickly add a rate limiter to this endpoint" and before writing anything, it flagged that the function I was editing was called from three other places and asked if I wanted the rate limiting applied there too. Copilot and Cursor both just modified the function I pointed at. Small thing, but it saved me from a real bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing, Lock-In, and Practical Reality for Teams
&lt;/h2&gt;

&lt;p&gt;I can't write this comparison without addressing cost because it actually shapes how you use these tools.&lt;/p&gt;

&lt;p&gt;GitHub Copilot Individual is $10/month. Copilot Business is $19/user/month. Copilot Enterprise — which is what my company has — is $39/user/month. At that tier you get better codebase context, access to different models (you can switch to Claude or GPT-4o depending on the task), and some organizational policy controls that matter for compliance-heavy teams.&lt;/p&gt;

&lt;p&gt;Cursor Pro is $20/month. For that you get 500 "fast" requests per month and unlimited slow ones. In practice, I burned through fast requests faster than I expected during my two-week test, particularly when using Composer heavily. There's also a Cursor Business tier at $40/user that adds privacy mode, centralized billing, and the usual team management stuff.&lt;/p&gt;

&lt;p&gt;Windsurf Pro is $15/month as of this writing — the lowest of the three. They also have a free tier that's usable beyond just a trial. The business tier is $35/user. If your team has skeptics who want to try before committing, point them at the Windsurf free tier first.&lt;/p&gt;

&lt;p&gt;Lock-in is a real consideration. Cursor is a fork of VS Code, which means if you have VS Code extensions, keybindings, and settings — they mostly work. Windsurf is also VS Code-based. GitHub Copilot works inside VS Code, JetBrains IDEs, Neovim, and basically everything else, which matters if your team isn't all on the same editor. I have one teammate on Neovim who can't use Cursor or Windsurf in their normal flow without a full context switch. For him, Copilot is the only real option.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Actual Recommendation
&lt;/h2&gt;

&lt;p&gt;I promised not to hedge this, so here it is.&lt;/p&gt;

&lt;p&gt;If you're a solo developer or working on a small team, all on VS Code or willing to use a VS Code fork: &lt;strong&gt;use Windsurf&lt;/strong&gt;. It's cheaper than Cursor, the multi-file editing is excellent, and the proactive side-effect detection has already saved me at least one regression. The UX is slightly rougher in places, but it's closing the gap fast.&lt;/p&gt;

&lt;p&gt;If you're already deep in the Cursor ecosystem with months of muscle memory and your team workflows built around it: &lt;strong&gt;stay on Cursor&lt;/strong&gt;. The tool is excellent, the context understanding is mature, and switching for a marginal improvement in one area doesn't make sense. The grass is only slightly greener.&lt;/p&gt;

&lt;p&gt;If you're on a mid-size or larger team with mixed editors, JetBrains users, or compliance requirements: &lt;strong&gt;GitHub Copilot Enterprise is the practical answer&lt;/strong&gt;. It's not the most impressive single-tool experience, but the breadth of integration matters when you have fifteen engineers with different setups. The ability to toggle models is also useful — for certain tasks, I found switching to Claude 3.7 inside Copilot gave better results than the default model.&lt;/p&gt;

&lt;p&gt;Look, I went into this thinking Copilot's momentum and GitHub's distribution would make it the dominant tool by default. What I found instead is that Windsurf earned its way into my workflow on merit — not a marginal autocomplete difference but a noticeably better experience for the multi-file refactoring work that makes up maybe 40% of my actual day.&lt;/p&gt;

&lt;p&gt;I'm writing this in Windsurf right now. Two weeks ago I wouldn't have said that.&lt;/p&gt;

</description>
      <category>aicoding</category>
      <category>githubcopilot</category>
      <category>cursor</category>
      <category>windsurf</category>
    </item>
    <item>
      <title>TypeScript 5.x en 2026: Las Funcionalidades que Realmente Importan en Producción</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:20:12 +0000</pubDate>
      <link>https://dev.to/synsun/typescript-5x-en-2026-las-funcionalidades-que-realmente-importan-en-produccion-52fc</link>
      <guid>https://dev.to/synsun/typescript-5x-en-2026-las-funcionalidades-que-realmente-importan-en-produccion-52fc</guid>
      <description>&lt;p&gt;Llevo casi dos años usando TypeScript 5.x activamente — primero en un proyecto personal, luego en producción con un equipo de seis personas construyendo una plataforma SaaS B2B. No he probado cada feature de cada minor release, pero sí he ido adoptando las que tenían sentido para nuestro stack: Next.js en el frontend, NestJS en la API principal, un par de workers con Bun corriendo tareas de procesamiento en background.&lt;/p&gt;

&lt;p&gt;Este artículo no es una lista de todo lo que salió. Es sobre lo que realmente usé, lo que me funcionó, y honestamente, lo que me decepcionó un poco.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoradores Estables: La Historia de Nunca Acabar que Por Fin Acabó
&lt;/h2&gt;

&lt;p&gt;Si llevas tiempo en el ecosistema TypeScript, sabes que los decoradores experimentales de &lt;code&gt;experimentalDecorators: true&lt;/code&gt; estuvieron rondando durante demasiado tiempo. TypeScript 5.0, lanzado en marzo de 2023, finalmente implementó el estándar ECMAScript de decoradores. No son los mismos decoradores de antes. Son mejores, pero implican una migración real que nadie te avisa que va a doler un poco.&lt;/p&gt;

&lt;p&gt;Migramos nuestros controladores de NestJS a los nuevos decoradores durante el primer trimestre de 2024. Fue más suave de lo que esperaba — la mayoría de librerías ya habían añadido soporte — pero encontré un par de casos raros con decoradores en propiedades de clase donde el comportamiento difería del sistema legacy. Uno de esos casos me tuvo depurando durante una tarde entera porque el error en runtime no mencionaba los decoradores para nada.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Decorador de clase con el nuevo estándar ECMAScript&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClassDecoratorContext&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;InstanceType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;InstanceType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;singleton&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabasePool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El nuevo parámetro &lt;code&gt;context&lt;/code&gt; es lo que más me gustó. Tienes acceso al nombre, al tipo del decorador, y a &lt;code&gt;addInitializer&lt;/code&gt; para ejecutar lógica post-construcción. Es mucho más explícito que el sistema anterior, donde básicamente estabas adivinando el orden de ejecución en algunos casos edge.&lt;/p&gt;

&lt;p&gt;Lo que me decepcionó: no hay compatibilidad automática con el código legacy. Si tienes decoradores propios escritos para el sistema experimental, los tienes que reescribir desde cero. En nuestro caso fueron cuatro decoradores internos — un par de horas de trabajo — pero conozco equipos con docenas de decoradores propios que tardaron semanas en la migración completa. Haz el inventario de decoradores propios &lt;em&gt;antes&lt;/em&gt; de comprometerte con una fecha. Y no hagas la migración el viernes por la tarde. Yo aprendí eso de la manera difícil.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;const&lt;/code&gt; en Genéricos y &lt;code&gt;NoInfer&lt;/code&gt;: Dos Cambios Pequeños, Mucho Menos Ruido
&lt;/h2&gt;

&lt;p&gt;¿Cuántas veces terminaste con un tipo más ancho de lo que querías, el código compiló, los tests pasaron, y seis semanas después alguien pasó un valor inválido que el tipo debería haber rechazado? Eso es exactamente lo que resuelven estos dos — &lt;code&gt;const&lt;/code&gt; type parameters en 5.0 y &lt;code&gt;NoInfer&lt;/code&gt; en 5.4 — aunque llegaron en versiones distintas.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const&lt;/code&gt; en genéricos es simple pero resuelve algo que antes requería un &lt;code&gt;as const&lt;/code&gt; en cada callsite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sin const: T se infiere como string[]&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createRoute&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// tipo inferido: string[] — demasiado amplio&lt;/span&gt;

&lt;span class="c1"&gt;// Con const: T se infiere como el tuple literal exacto&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createRoute&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// tipo inferido: readonly ['/', '/about', '/contact'] — perfecto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Llevaba años escribiendo &lt;code&gt;as const&lt;/code&gt; en cada callsite de funciones similares. Esto lo elimina. En nuestro sistema de configuración de rutas, donde necesitamos los tipos literales para generar breadcrumbs y validaciones, fue un cambio de calidad de vida enorme.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NoInfer&lt;/code&gt; es más sutil — tardé tiempo en ver cuándo usarlo, pensé que era un caso edge raro, y resulta que no. El caso principal: tienes un genérico &lt;code&gt;T&lt;/code&gt; que se infiere desde un argumento, pero no quieres que otro argumento influya en esa inferencia y la amplíe inadvertidamente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setDefault&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NoInfer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;setDefault&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// OK&lt;/span&gt;
&lt;span class="nf"&gt;setDefault&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Error: number no es asignable a string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antes de &lt;code&gt;NoInfer&lt;/code&gt;, teníamos que recurrir a técnicas como &lt;code&gt;T &amp;amp; {}&lt;/code&gt; o reestructurar la firma entera. Uno de los desarrolladores del equipo lo usó en una función de validación de feature flags y cerró dos bugs de runtime que llevaban meses en el backlog. Los bugs existían porque TypeScript aceptaba valores inválidos por cómo estaban estructuradas las firmas genéricas. Pequeño cambio, impacto real.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;using&lt;/code&gt; para la Gestión de Recursos: Más Útil de lo que Pensé
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.2 implementó &lt;code&gt;using&lt;/code&gt; y &lt;code&gt;await using&lt;/code&gt;, basados en la propuesta de Explicit Resource Management de TC39. Cuando lo vi por primera vez pensé "interesante, pero cuándo lo uso realmente en mi día a día". La respuesta: más seguido de lo que esperaba.&lt;/p&gt;

&lt;p&gt;Funciona así: cualquier objeto que implemente &lt;code&gt;Symbol.dispose&lt;/code&gt; (o &lt;code&gt;Symbol.asyncDispose&lt;/code&gt;) se limpia automáticamente al salir del scope. Como el &lt;code&gt;using&lt;/code&gt; de C# o los context managers de Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTransaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;committed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;committed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;committed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transferFunds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DatabaseTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Si algo lanza antes del commit, el rollback ocurre automáticamente al salir del scope&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo que me sorprendió genuinamente fue empezar a ver cuántos lugares en nuestra codebase teníamos bloques &lt;code&gt;try/finally&lt;/code&gt; para cleanup que se podían simplificar con &lt;code&gt;using&lt;/code&gt;. Conexiones a caches temporales, file handles en workers de procesamiento de CSV, clients de APIs externas que necesitan un &lt;code&gt;.close()&lt;/code&gt; explícito. No es que el código fuera incorrecto antes — pero era más verboso y, lo que es peor, más fácil de olvidar el cleanup en paths de error que nadie testea.&lt;/p&gt;

&lt;p&gt;En un proyecto de frontend puro probablemente no lo uses mucho. Pero si tienes workers, scripts de migración de datos, o código de servidor con gestión explícita de recursos, vale la pena adoptarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;isolatedDeclarations&lt;/code&gt; en Monorepos: El Antes y el Después de Nuestros Builds
&lt;/h2&gt;

&lt;p&gt;Esta es, para nuestro equipo específico, la funcionalidad más impactante de toda la serie 5.x. Llegó en TypeScript 5.5 y resolvió un problema que yo ni sabía que tenía nombre.&lt;/p&gt;

&lt;p&gt;El problema: en un monorepo con múltiples paquetes TypeScript, el compilador necesita procesar todos los archivos de un paquete para generar sus &lt;code&gt;.d.ts&lt;/code&gt; de declaraciones de tipos. Esto hace que el build sea inherentemente secuencial en ciertos puntos — un paquete no puede emitir sus tipos hasta que termina de compilar completamente, y los paquetes que dependen de él tienen que esperar.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;isolatedDeclarations: true&lt;/code&gt; en el tsconfig añade una restricción: todas las exportaciones públicas deben tener tipos explícitos anotados, no pueden depender solo de inferencia. A cambio, herramientas como &lt;code&gt;tsc&lt;/code&gt;, esbuild y otras pueden generar los &lt;code&gt;.d.ts&lt;/code&gt; en paralelo sin necesitar procesar los archivos de dependencias primero.&lt;/p&gt;

&lt;p&gt;Nuestro monorepo tiene doce paquetes. Con el setup anterior, el build completo en CI — incluyendo generación de tipos — tardaba entre 3 minutos 45 segundos y 4 minutos 10 segundos dependiendo del runner. Después de habilitar &lt;code&gt;isolatedDeclarations&lt;/code&gt; y ajustar nuestro pipeline de Turborepo para aprovechar la paralelización, bajamos a 2 minutos 20 segundos de forma consistente. No es lineal con el número de paquetes — el beneficio depende mucho de la topología de dependencias — pero el impacto en nuestro caso fue muy tangible.&lt;/p&gt;

&lt;p&gt;El coste es real: tienes que añadir anotaciones de tipo explícitas donde antes te apoyabas en inferencia para las exportaciones públicas. Nuestro linter marca automáticamente las violaciones con una regla propia, así que el burden en el día a día no es grande. El setup inicial requirió medio día de limpieza. Si tienes un monorepo TypeScript y no estás mirando esto, empieza por aquí antes de mirar cualquier otra optimización de build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predicados de Tipo Inferidos: El Bug que Llevaba Tres Meses en el Radar
&lt;/h2&gt;

&lt;p&gt;TypeScript 5.5 también trajo algo que parece menor pero que tiene una elegancia conceptual que me gusta: el compilador ahora puede inferir que una función es un type predicate sin que tú lo declares explícitamente, siempre que la lógica sea clara.&lt;/p&gt;

&lt;p&gt;Antes, si querías narrowing automático en el callsite, tenías que anotar el return type manualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Antes de 5.5: anotación explícita obligatoria para que funcione el narrowing&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript 5.5+: inferencia automática del type predicate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDefinedString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;definedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isDefinedString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// definedItems ahora es string[], no (string | undefined)[]&lt;/span&gt;
&lt;span class="c1"&gt;// antes necesitabas el as string[] o anotar isDefinedString explícitamente&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tenía un bug — o más bien, un &lt;code&gt;// @ts-ignore&lt;/code&gt; vergonzoso — en nuestro pipeline de procesamiento de eventos donde hacíamos &lt;code&gt;.filter(Boolean)&lt;/code&gt; y luego teníamos que castear manualmente porque TS no infería el narrowing. Era de esos parches que vives con durante semanas hasta que te molesta lo suficiente como para investigarlo de verdad. Cuando actualicé a 5.5, desapareció solo. No tuve que tocar el código.&lt;/p&gt;

&lt;p&gt;Una advertencia: si tienes predicados con lógica muy compleja, el compilador puede no inferirlo automáticamente y necesitarás la anotación explícita. Para los casos comunes de filtrado y narrowing básico, funciona sorprendentemente bien.&lt;/p&gt;




&lt;p&gt;Después de dos años con TypeScript 5.x en producción: no actualices en bloque esperando una transformación mágica. Actualiza de forma incremental y adopta activamente &lt;code&gt;isolatedDeclarations&lt;/code&gt; si tienes un monorepo, &lt;code&gt;using&lt;/code&gt; si manejas recursos con cleanup explícito, y los &lt;code&gt;const&lt;/code&gt; genéricos si tienes APIs que se benefician de inferencia más precisa. Los decoradores estables merecen la migración, pero planifícala — no la hagas el viernes por la tarde.&lt;/p&gt;

&lt;p&gt;TypeScript 5.x no reinventó el lenguaje. Lo que hizo fue cerrar brechas que llevaban años abiertas, y en producción eso vale más que cualquier feature nueva que suene impresionante en un changelog pero que rara vez tocas en el trabajo real.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>produccion</category>
      <category>monorepos</category>
    </item>
    <item>
      <title>Serverless vs Contenedores en 2026: Guía Práctica de Decisión para Equipos de Backend</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:19:42 +0000</pubDate>
      <link>https://dev.to/synsun/serverless-vs-contenedores-en-2026-guia-practica-de-decision-para-equipos-de-backend-ad7</link>
      <guid>https://dev.to/synsun/serverless-vs-contenedores-en-2026-guia-practica-de-decision-para-equipos-de-backend-ad7</guid>
      <description>&lt;p&gt;El año pasado mi equipo — éramos cinco ingenieros en ese momento — llevaba casi dos años con toda la infraestructura de backend en AWS Lambda. Funciones Python para procesar eventos, APIs síncronas, pipelines de datos. Todo serverless. Estábamos orgullosos de eso.&lt;/p&gt;

&lt;p&gt;Entonces empezamos a integrar modelos de ML propios. Y todo empezó a crujir.&lt;/p&gt;

&lt;p&gt;Este post no es un benchmark neutral. Es cómo tomamos la decisión, qué nos sorprendió en el camino, y qué haría diferente si empezara desde cero en 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos Años en Lambda: Lo que Funcionó y el Límite que No Vi Venir
&lt;/h2&gt;

&lt;p&gt;Antes de hablar de contenedores, tengo que ser honesto sobre lo bueno. Lambda funcionó muy bien durante bastante tiempo. Procesábamos unos 2-3 millones de eventos diarios provenientes de webhooks de tiendas — Shopify principalmente — y el modelo de pago por ejecución nos ahorraba mucho comparado con servidores corriendo 24/7 con cargas variables. En los meses de baja temporada, la factura de compute caía casi a cero. Eso es real y no hay que subestimarlo.&lt;/p&gt;

&lt;p&gt;El problema llegó cuando empezamos a correr modelos de embeddings para recomendaciones de producto. Nuestro primer modelo de sentence-transformers pesaba unos 420MB solo el checkpoint. Lambda tiene un límite de 250MB para el paquete de deployment sin comprimir, y aunque podés cargar modelos desde S3 al iniciar, eso disparaba los cold starts a entre 8 y 12 segundos. Para una API síncrona, eso es inaceptable.&lt;/p&gt;

&lt;p&gt;Intenté workarounds. Cargué el modelo de forma lazy, exploré Lambda SnapStart (tuvimos que reescribir parte del pipeline, no valió la pena), probé contenedores de Lambda que permiten hasta 10GB de imagen. Eso último ayudó un poco, pero el cold start seguía siendo entre 3 y 5 segundos para el modelo grande. Ninguna de las tres opciones era satisfactoria — y lo peor era que cada workaround generaba su propia deuda técnica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El patrón que me tardó en ver:&lt;/strong&gt; Lambda sigue siendo excelente para cargas event-driven con dependencias ligeras. En cuanto metés ML o cualquier proceso que requiera estado caliente persistente en memoria, empezás a pelear contra la plataforma en lugar de con ella.&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde los Contenedores Ganaron la Discusión Interna
&lt;/h2&gt;

&lt;p&gt;La migración a ECS Fargate para las cargas de ML no fue una decisión feliz. Fue una decisión forzada.&lt;/p&gt;

&lt;p&gt;Lo primero que noté al mover los pipelines de inferencia a Fargate fue el control. Aunque — déjame retroceder un segundo, porque Fargate no es simple. Tuve que escribir task definitions, ajustar los límites de CPU y memoria, configurar IAM roles específicos, y entender cómo funcionan los scaling policies en detalle. Eso tomó tiempo. Pero una vez listo, tener un contenedor con Python 3.12, CUDA, y todas las dependencias ML corriendo sin restricciones artificiales de tamaño era... alivio, honestamente.&lt;/p&gt;

&lt;p&gt;Acá va un ejemplo simplificado de cómo pasamos de una Lambda con carga de modelo a un servicio en Fargate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Antes: Lambda handler con cold start doloroso en cada invocación fría
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="c1"&gt;# Se ejecuta en cada cold start — entre 8-12s con el modelo grande
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;all-mpnet-base-v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 420MB en disco
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# guardar embeddings en DynamoDB...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Después: servicio FastAPI en Fargate — modelo cargado una vez, persiste en memoria
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Se carga una vez cuando arranca el contenedor, no en cada request
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;all-mpnet-base-v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="c1"&gt;# batch_size=32 para aprovechar paralelismo del modelo en memoria
&lt;/span&gt;    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La diferencia en latencia fue brutal. P99 bajó de ~9s a ~180ms para el mismo modelo en los mismos requests. El modelo cargado una vez en memoria versus recargado en cada cold start es una diferencia que parece obvia en retrospectiva, pero cuesta verla cuando llevás años pensando en términos de funciones stateless.&lt;/p&gt;

&lt;p&gt;Lo que no me gustó de Fargate: el costo base. Con Lambda pagás exactamente lo que usás. Con Fargate, si tenés un task corriendo 24/7 para mantener el modelo caliente en memoria, pagás por esas horas aunque el tráfico sea mínimo a las 3am. Para nuestro workload procesando unos 50k requests por día con picos horarios marcados, el costo mensual en Fargate fue entre 3x y 4x más caro que lo que habríamos pagado con Lambda sin el problema del cold start.&lt;/p&gt;

&lt;p&gt;Right, so — ¿cómo justificamos el gasto? Rendimiento medible. Nuestros clientes notaban la diferencia y los datos lo confirmaron: conversion rate en las páginas de recomendaciones subió un 12% cuando la latencia bajó a menos de 200ms. Eso cerró la discusión interna.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway práctico:&lt;/strong&gt; Fargate tiene sentido cuando necesitás estado caliente en memoria, dependencias pesadas, o procesos de más de 15 minutos. El costo base es real — hay que justificarlo con impacto concreto de negocio, no con argumentos técnicos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Hallazgo que Me Confundió Durante Semanas: Cloud Run
&lt;/h2&gt;

&lt;p&gt;Un colega me recomendó Google Cloud Run para un servicio nuevo — una API de procesamiento de imágenes que necesitaba OpenCV y algo de lógica custom. Cloud Run básicamente te permite correr contenedores Docker de forma serverless: escala a cero cuando no hay tráfico, pero al llegar requests escala contenedores completos.&lt;/p&gt;

&lt;p&gt;Mi primera reacción fue: "¿esto no es simplemente Lambda pero con Docker?" Y parcialmente sí. Pero la diferencia práctica me tomó tiempo entender, y creo que la confusión viene de que la distinción no es obvia en la documentación.&lt;/p&gt;

&lt;p&gt;Con Lambda container images, vos definís tu imagen pero Lambda gestiona el runtime con sus propias restricciones: timeouts máximos de 15 minutos, el modelo de ejecución de funciones, limitaciones de concurrencia que requieren configuración explícita. Con Cloud Run, el contrato es distinto: vos exponés un servidor HTTP y Cloud Run gestiona cuántas instancias corren. Tu código puede tener estado dentro de una instancia mientras esté viva. Podés usar WebSockets. No existe límite de 15 minutos por request.&lt;/p&gt;

&lt;p&gt;Probé Cloud Run para ese servicio de imágenes y el cold start fue de aproximadamente 600-900ms con una imagen de ~1.2GB. No tan rápido como un contenedor siempre encendido, pero mucho más barato en cargas variables. Lo que más me sorprendió fue el pricing model: Cloud Run cobra por tiempo de CPU y memoria durante el procesamiento de requests, no por tiempo de instancia activa (a menos que configures &lt;code&gt;min-instances &amp;gt; 0&lt;/code&gt;). Para cargas intermitentes medianas, eso puede ser significativamente más barato que Fargate.&lt;/p&gt;

&lt;p&gt;No lo probé a más de 800-1000 requests por segundo concurrentes, así que no puedo hablar de ese rango con seguridad. Pero para nuestro caso, Cloud Run resolvió el equilibrio entre costo y performance de una forma que ni Lambda pura ni Fargate podían ofrecer por separado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway práctico:&lt;/strong&gt; Si estás viendo serverless y contenedores como dos mundos completamente separados, te estás perdiendo opciones intermedias que en 2026 están bastante maduras. Cloud Run y AWS App Runner son probablemente el punto de partida correcto para más proyectos de los que la gente considera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los Números Reales Comparados (el Ejercicio que Me Pidió el CFO)
&lt;/h2&gt;

&lt;p&gt;Armé esto después de que me pidieran justificar una factura de AWS que había subido un 40% en tres meses. Empujé esa migración de infra un viernes por la tarde — error clásico — y el proceso de validación de costos duró dos semanas. En retrospectiva, debería haberlo hecho con más datos antes de mover nada.&lt;/p&gt;

&lt;p&gt;Tres workloads representativos, costos aproximados mensuales en us-east-1:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload A: Procesamiento de webhooks (2M eventos/día, avg 200ms de ejecución)&lt;/strong&gt;&lt;br&gt;
Lambda: ~$45/mes. Fargate (1 task 0.5 vCPU, 1GB RAM, 24/7): ~$22/mes. Cloud Run: ~$31/mes.&lt;br&gt;
Fargate ganó. Carga constante y predecible significa que el compute dedicado sale más barato que pagar por invocación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload B: API de inferencia ML (50k requests/día, distribución horaria con picos)&lt;/strong&gt;&lt;br&gt;
Lambda con cold starts: técnicamente no viable para nuestro SLA de latencia bajo 500ms. Fargate (1 task caliente, 2 vCPU, 4GB RAM): ~$110/mes. Cloud Run (1 vCPU, 2GB RAM, &lt;code&gt;min-instances=1&lt;/code&gt; para evitar cold starts): ~$62/mes.&lt;br&gt;
Cloud Run con una instancia mínima ganó. Mantenés el modelo caliente sin pagar por N contenedores inactivos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload C: ETL nocturno (30 minutos por noche, procesamiento intensivo)&lt;/strong&gt;&lt;br&gt;
Lambda: ~$2/mes. Fargate on-demand: ~$8/mes. Cloud Run: ~$3.50/mes.&lt;br&gt;
Lambda ganó fácil. Para trabajos cortos e infrecuentes, el modelo de pago por ejecución es imbatible.&lt;/p&gt;

&lt;p&gt;Lo que estos números muestran, más allá de los valores específicos: el patrón de tráfico importa tanto como el tipo de workload. No existe una respuesta universal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi Recomendación Real Según el Tipo de Proyecto
&lt;/h2&gt;

&lt;p&gt;Voy a ser directo porque "depende de tu caso de uso" no le sirve a nadie cuando está tomando una decisión concreta con fecha límite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless puro (Lambda, Cloud Functions)&lt;/strong&gt; es la elección correcta si tu equipo tiene menos de 5 ingenieros de backend, tus workloads son principalmente event-driven con dependencias ligeras (menos de 100MB), y la variabilidad de tráfico es alta o impredecible. El overhead operativo de gestionar contenedores no vale la pena si podés evitarlo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contenedores dedicados (Fargate, GKE, EKS)&lt;/strong&gt; cuando tenés ML en producción con modelos que pesan más de 500MB, procesos que corren por más de 15 minutos, o carga base predecible y sostenida que justifique instancias dedicadas. También si tu equipo ya tiene expertise sólido en Docker y Kubernetes — la curva de aprendizaje ya está amortizada y el beneficio operativo compensa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La opción intermedia (Cloud Run, App Runner, Azure Container Apps)&lt;/strong&gt; es donde yo comenzaría hoy para la mayoría de APIs medianas nuevas. Contenedores con billing serverless. La limitación principal es el vendor lock-in a la plataforma de cloud, que puede o no importarte según tu estrategia — aunque honestamente, para equipos de menos de 10 personas construyendo en un solo cloud, ese lock-in raramente es el problema real.&lt;/p&gt;

&lt;p&gt;Kubernetes gestionado lo dejaría para equipos con platform engineering dedicado. Con cinco personas, no podíamos darnos ese lujo. Aprendimos eso de la manera cara, no de un post.&lt;/p&gt;

&lt;p&gt;Mi veredicto después de este año: para un equipo de 4-6 personas en 2026, el punto de partida debería ser Cloud Run o App Runner, con Lambda reservado para event processing liviano y contenedores dedicados únicamente para workloads de ML con estado o procesamiento intensivo. La arquitectura puramente serverless que teníamos era elegante, pero nos limitó cuando escalamos en complejidad — no en tráfico. Esa es una distinción que no aparece en ningún comparison chart.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>containers</category>
      <category>awslambda</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Redis vs Valkey en 2026: El Fork que Nadie Pidió y Por Qué Ahora Importa</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:19:12 +0000</pubDate>
      <link>https://dev.to/synsun/redis-vs-valkey-en-2026-el-fork-que-nadie-pidio-y-por-que-ahora-importa-57dc</link>
      <guid>https://dev.to/synsun/redis-vs-valkey-en-2026-el-fork-que-nadie-pidio-y-por-que-ahora-importa-57dc</guid>
      <description>&lt;p&gt;Marzo de 2024. Estaba revisando una PR bastante aburrida cuando vi el anuncio de Redis Ltd.: a partir de la versión 7.4, Redis dejaba de ser BSD-3-Clause para pasar a un modelo dual con RSALv2 y SSPL. Cerré el anuncio, lo volví a abrir. Lo leí dos veces más.&lt;/p&gt;

&lt;p&gt;Llevaba cuatro años con Redis corriendo en producción —caché de sesiones, rate limiting, pub/sub para notificaciones en tiempo real— y de repente tenía que decidir si ese stack seguía teniendo sentido.&lt;/p&gt;

&lt;p&gt;Spoiler: terminé migrando a Valkey. Pero la decisión no fue tan obvia como parecía al principio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por Qué Redis Ltd. Cambió la Licencia (y Por Qué Duele)
&lt;/h2&gt;

&lt;p&gt;La explicación oficial fue que los proveedores cloud —principalmente AWS, Google y Azure— estaban ganando dinero ofreciendo Redis como servicio gestionado sin contribuir significativamente al proyecto. Hay cierta lógica ahí. El problema es cómo lo resolvieron.&lt;/p&gt;

&lt;p&gt;El SSPL (Server Side Public License) tiene una cláusula que básicamente dice: si ofreces el software como servicio, tienes que liberar también todo el código de tu infraestructura de servicio bajo SSPL. No solo el código que modificaste de Redis, sino todo lo que lo rodea. AWS claramente no iba a hacer eso. Ningún cloud provider iba a hacerlo.&lt;/p&gt;

&lt;p&gt;La OSI (Open Source Initiative) rechazó el SSPL cuando MongoDB lo propuso en 2018. No es open source bajo ninguna definición reconocida. Redis Ltd. lo sabía. Lo eligieron igual.&lt;/p&gt;

&lt;p&gt;Entonces, ¿qué pasó? En menos de dos semanas, la Linux Foundation anunció Valkey —un fork de Redis 7.2.4 bajo BSD-3-Clause— con el respaldo de AWS, Google Cloud, Oracle, Ericsson y otros. El 23 de marzo de 2024, Valkey tenía su propio repositorio. Para mayo, AWS ya había anunciado que ElastiCache migraría a Valkey. DigitalOcean y Aiven siguieron poco después.&lt;/p&gt;

&lt;p&gt;Yo me quedé pensando: ¿esto es un rescate legítimo del open source o simplemente los cloud providers protegiéndose a sí mismos?&lt;/p&gt;

&lt;p&gt;Honestamente, creo que es las dos cosas. Y eso no hace el fork menos válido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo Que el Cambio de Licencia Significa en la Práctica Para Tu Proyecto
&lt;/h2&gt;

&lt;p&gt;El análisis legal se complica rápido. Lo práctico es más simple.&lt;/p&gt;

&lt;p&gt;Si usas Redis en tu propio servidor o en contenedores propios, la nueva licencia de Redis 7.4+ &lt;strong&gt;no te afecta directamente&lt;/strong&gt; mientras no lo estés ofreciendo como servicio a terceros. Para la mayoría de startups y equipos de producto, eso significa que técnicamente podrías seguir usando Redis sin problema legal inmediato.&lt;/p&gt;

&lt;p&gt;El asunto se complica en tres casos:&lt;/p&gt;

&lt;p&gt;Primero, si dependes de un cloud provider que ahora ofrece Valkey en vez de Redis. Ya no tienes elección —estás en Valkey de facto. AWS empezó la migración automática de ElastiCache en 2025 y la mayoría de instancias ya están en Valkey 7.2.x o Valkey 8.x.&lt;/p&gt;

&lt;p&gt;Segundo, si tu empresa tiene política de "solo OSI-approved licenses" (muchas empresas medianas y grandes tienen esto como requisito legal). Redis 7.4+ no pasa esa barrera. Valkey sí.&lt;/p&gt;

&lt;p&gt;Tercero —y esto me tomó por sorpresa cuando lo investigué a fondo— algunas distribuciones de Linux ya eliminaron Redis de sus repos oficiales o lo marcaron como non-free. Debian, por ejemplo, movió Redis al área &lt;code&gt;non-free&lt;/code&gt;. Si tu pipeline de CI/CD instala Redis desde los repos del sistema, ya estás afectado aunque no lo sepas.&lt;/p&gt;

&lt;p&gt;Mi equipo de tres personas descubrió esto último de la peor manera posible: un pipeline de staging que instalaba Redis via apt dejó de funcionar un martes a las 11pm porque alguien hizo un apt upgrade en la imagen base. No fue prod, pero tampoco fue divertido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valkey 8.x: Lo Que Me Sorprendió Después de Migrar
&lt;/h2&gt;

&lt;p&gt;Tenía expectativas bajas. Los forks apresurados suelen tener deuda técnica, documentación inconsistente y una comunidad que se va apagando después del momento inicial de entusiasmo. Me preparé para el peor caso.&lt;/p&gt;

&lt;p&gt;No fue lo que encontré.&lt;/p&gt;

&lt;p&gt;Valkey 7.2.x (el fork inicial) es esencialmente Redis 7.2.4 con el nombre cambiado y la licencia corregida. La compatibilidad es casi perfecta —el protocolo RESP, la API de comandos, los archivos de configuración. Cambiar &lt;code&gt;redis-cli&lt;/code&gt; por &lt;code&gt;valkey-cli&lt;/code&gt; es literalmente todo lo que necesité hacer en la mayoría de mis scripts de utilidad.&lt;/p&gt;

&lt;p&gt;Pero Valkey 8.0, lanzado en septiembre de 2024, es donde se empezaron a ver las primeras divergencias reales:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verificar versión y algunas métricas nuevas en Valkey 8.x&lt;/span&gt;
valkey-cli INFO server | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"valkey_version|io_threads_active"&lt;/span&gt;

&lt;span class="c"&gt;# Output que vi en mi setup:&lt;/span&gt;
&lt;span class="c"&gt;# valkey_version:8.0.2&lt;/span&gt;
&lt;span class="c"&gt;# io_threads_active:4&lt;/span&gt;

&lt;span class="c"&gt;# En Redis 7.4, el mismo campo sería redis_version&lt;/span&gt;
&lt;span class="c"&gt;# y io_threads no está activo por defecto de la misma manera&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cambio más interesante en Valkey 8.0 fue la mejora en el I/O threading. Redis implementó multithreading para operaciones de red en Redis 6.0, pero el modelo seguía siendo single-threaded para la ejecución de comandos. Valkey 8.0 mejoró el pipeline del I/O threaded y, en mis pruebas con cargas de lectura intensiva (básicamente GET/SET con payloads de ~1KB), vi entre un 15% y un 22% de mejora en throughput contra Redis 7.2.&lt;/p&gt;

&lt;p&gt;No soy 100% seguro de que ese número se sostenga en todas las cargas de trabajo —mi setup específico favorece lecturas— pero es más de lo que esperaba de un proyecto con menos de un año de vida independiente.&lt;/p&gt;

&lt;p&gt;Lo que sí me decepcionó: la documentación en 2024 era bastante escasa. Muchas páginas eran copias directas de la doc de Redis con el nombre cambiado, sin actualizar ejemplos ni aclarar las diferencias nuevas. Para principios de 2025 había mejorado bastante. En 2026 ya está en un estado decente, aunque todavía hay secciones donde claramente falta trabajo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos Semanas Migrando en Producción: Lo que Realmente Pasó
&lt;/h2&gt;

&lt;p&gt;El plan inicial era simple: alias de red, mismo puerto, cambiar el binario, listo. Y en su mayor parte funcionó exactamente así.&lt;/p&gt;

&lt;p&gt;La arquitectura que migré: tres instancias Redis en un cluster de replicación primario/réplica, con Sentinel para failover automático. Aproximadamente 8GB de datos en memoria, mezcla de strings, hashes y sorted sets. Unas 12,000 operaciones por segundo en hora pico.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# El cambio más invasivo en código fue esto — casi ninguno
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;  &lt;span class="c1"&gt;# El cliente de Python siguió funcionando sin cambios
&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;valkey-primary.internal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Solo cambié el hostname
&lt;/span&gt;    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Todo lo demás igual. ZADD, HGET, SETEX, Pub/Sub — sin cambios.
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leaderboard&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user:1042&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;9850.5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leaderboard&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user:1042&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cliente de Python &lt;code&gt;redis-py&lt;/code&gt; funciona con Valkey sin modificaciones. Lo mismo con los clientes de Node.js, Go y Java que usamos. Valkey es intencionalmente compatible con el protocolo de Redis —eso era un requisito del fork desde el día uno.&lt;/p&gt;

&lt;p&gt;Donde sí tuve problemas: Valkey Sentinel tiene algunas diferencias de comportamiento en edge cases de failover. Específicamente, durante una prueba de failover intencional (déjame ser honesto: fue un &lt;code&gt;kill -9&lt;/code&gt; en el primario un viernes a las 4pm porque pensé que teníamos tiempo suficiente para resolver problemas antes del fin de semana), el tiempo de elección del nuevo primario fue más lento de lo esperado —unos 35 segundos vs los ~8 segundos que veía con Redis Sentinel.&lt;/p&gt;

&lt;p&gt;Después de investigarlo, resultó ser un parámetro de configuración que no había migrado correctamente: &lt;code&gt;sentinel down-after-milliseconds&lt;/code&gt;. El valor por defecto en mi instalación nueva de Valkey era diferente al que tenía en Redis. Cinco minutos de config fix, problema resuelto. Pero me costó esa tarde de viernes.&lt;/p&gt;

&lt;p&gt;Una advertencia que no puedes ignorar: los módulos. Si usas RedisSearch, RedisJSON, RedisTimeSeries o cualquier módulo de Redis Stack, esos son propietarios de Redis Ltd. y &lt;strong&gt;no son compatibles con Valkey&lt;/strong&gt;. Existen proyectos comunitarios alternativos, pero la paridad de features no es completa. Si dependes fuertemente de estos módulos, la migración es considerablemente más complicada.&lt;/p&gt;

&lt;p&gt;En mi caso no usaba ninguno. Suerte de principiante, supongo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi Veredicto: Cuándo Elegir Cada Uno (Sin Rodeos)
&lt;/h2&gt;

&lt;p&gt;Después de dos semanas de migración y seis meses más usando Valkey en producción, esto es lo que pienso —sin pretender que es una decisión neutral:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usa Valkey si:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tu infraestructura está en un cloud provider que ya migró (AWS, GCP, DigitalOcean, Aiven) — no tienes opción y no es un problema. Si estás empezando un proyecto nuevo hoy, Valkey es la decisión obvia: licencia limpia, respaldo institucional fuerte, y la distancia técnica con Redis en casos de uso comunes es mínima o favorable.&lt;/p&gt;

&lt;p&gt;El ecosistema de Valkey en 2026 ya tiene masa crítica. La mayoría de herramientas de observabilidad (Datadog, Prometheus exporters, etc.) soportan Valkey. Los frameworks que tenían integraciones con Redis las tienen con Valkey —algunos por compatibilidad directa, otros con soporte explícito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sigue con Redis si:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dependes de Redis Stack y los módulos propietarios (RedisSearch en particular) son parte central de tu arquitectura. Migrar eso hoy tiene un costo real. Además, si tienes un contrato de soporte con Redis Ltd., tiene sentido mantenerse en su ecosistema mientras dure.&lt;/p&gt;

&lt;p&gt;También hay un argumento de inercía legítimo: si Redis 7.2.x o anterior corre perfectamente en tu infraestructura propia y no tienes razones de licencia para cambiar, la presión de migrar no es urgente. Aunque el riesgo real permanece — Redis Ltd. ya demostró que puede cambiar las reglas cuando le convenga. El fork de Valkey existió exactamente porque ese riesgo se materializó.&lt;/p&gt;

&lt;p&gt;Personalmente, no volvería a Redis para un proyecto nuevo. La incertidumbre de licencia existe, el ecosistema de Valkey ya es lo suficientemente maduro, y los números de rendimiento en Valkey 8.x me gustaron más de lo que esperaba. No es una decisión emocional contra Redis Ltd. —es pragmatismo.&lt;/p&gt;

&lt;p&gt;La conclusión es sencilla: Valkey ganó el fork porque los cloud providers lo necesitaban, y eso resultó ser suficiente para llevarse el ecosistema también.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>valkey</category>
      <category>basesdedatos</category>
      <category>opensource</category>
    </item>
    <item>
      <title>TypeScript 5.x in 2026: Features That Actually Matter for Production Code</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:18:42 +0000</pubDate>
      <link>https://dev.to/synsun/typescript-5x-in-2026-features-that-actually-matter-for-production-code-27kp</link>
      <guid>https://dev.to/synsun/typescript-5x-in-2026-features-that-actually-matter-for-production-code-27kp</guid>
      <description>&lt;p&gt;Spent most of last winter doing something I should have done a year earlier: actually reading the TypeScript 5.x changelogs. Not skimming the headlines — reading them, then dropping each feature into a scratch project to see how it actually behaved. Our codebase sits at around 180k lines — a team of seven, a mix of Node.js inference services and React front-ends — and we'd been on TypeScript 5.x for over a year without meaningfully adopting anything new. We'd bumped the package version, confirmed the build didn't break, moved on.&lt;/p&gt;

&lt;p&gt;What I found: maybe six features that genuinely changed how I write TypeScript, and a longer tail of things that are technically interesting but haven't touched my day-to-day work. This isn't a changelog recap. It's what actually earned its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;using&lt;/code&gt; Declarations Fixed a Leak I'd Been Ignoring for Eight Months
&lt;/h2&gt;

&lt;p&gt;The explicit resource management proposal — &lt;code&gt;using&lt;/code&gt; and &lt;code&gt;await using&lt;/code&gt; — landed in TypeScript 5.2, and I'm genuinely annoyed it took me this long to use it. The thing that finally pushed me to look: a slow memory leak in one of our LLM inference services I'd been deferring for months.&lt;/p&gt;

&lt;p&gt;We were pooling inference sessions, and somewhere in the request-handling code, sessions weren't always being released. The &lt;code&gt;try/finally&lt;/code&gt; blocks were there — mostly. One code path through a batch endpoint was missing the cleanup call. The session sat there, held in memory, until the process restarted. I pushed a fix on a Friday afternoon after tracing it for two hours, and I thought: this is the kind of bug that shouldn't be possible.&lt;/p&gt;

&lt;p&gt;The old pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The before: try/finally that's correct until someone adds a code path&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runBatchInference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;batch inference failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// pool.release() was added here by a colleague — but not in the finally block&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// sometimes ran twice. sometimes not at all.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After implementing &lt;code&gt;Symbol.asyncDispose&lt;/code&gt; on the session class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InferenceSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asyncDispose&lt;/span&gt;&lt;span class="p"&gt;]():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;released&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;released&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runBatchInference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// No try/finally. Disposal is guaranteed at scope exit,&lt;/span&gt;
  &lt;span class="c1"&gt;// regardless of which path the function takes.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What surprised me was the disposal ordering. When you stack multiple &lt;code&gt;using&lt;/code&gt; declarations in the same scope, TypeScript disposes them in reverse order — last declared, first disposed, LIFO. I expected to have to verify this carefully and maybe work around edge cases. Nope. It just works the way you'd want it to if one resource depends on another.&lt;/p&gt;

&lt;p&gt;If you manage any resource in TypeScript — database connections, file handles, WebSocket sessions, anything with a &lt;code&gt;close()&lt;/code&gt; — implementing &lt;code&gt;Symbol.dispose&lt;/code&gt; or &lt;code&gt;Symbol.asyncDispose&lt;/code&gt; and switching to &lt;code&gt;using&lt;/code&gt; is the most immediately practical change in all of 5.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inferred Type Predicates Deleted About 300 Lines of Manual Guards
&lt;/h2&gt;

&lt;p&gt;Before TypeScript 5.5, getting &lt;code&gt;.filter()&lt;/code&gt; to actually narrow a type required an explicit type predicate function. We had a file of them: &lt;code&gt;isNonNull&lt;/code&gt;, &lt;code&gt;isLoaded&lt;/code&gt;, &lt;code&gt;isSuccessResponse&lt;/code&gt;, &lt;code&gt;isAPIError&lt;/code&gt;. About 300 lines across two utility modules, and someone would add a new one every couple of weeks. Every time we introduced a new union type, we'd forget to add the corresponding predicate, use the wrong one, or find out the type was wider than expected somewhere downstream.&lt;/p&gt;

&lt;p&gt;TypeScript 5.5 introduced automatic inference of type predicates — when the compiler can determine from the function body that a value is being narrowed, it infers the &lt;code&gt;value is T&lt;/code&gt; return type for you. The case that hit us hardest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before 5.5 — you wrote this (correctly) every time&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isNonNull&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;InferenceResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isNonNull&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// InferenceResult[]&lt;/span&gt;

&lt;span class="c1"&gt;// After 5.5 — TypeScript infers the predicate from the inline callback&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// results is InferenceResult[], not (InferenceResult | null)[]&lt;/span&gt;
&lt;span class="c1"&gt;// No helper. No import. Just correct.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deleted most of those utility files the same afternoon I confirmed this worked. Not all of them — there are still cases where the inference doesn't trigger. The rule of thumb I've built up: simple null and equality checks work reliably; anything with nested property access or custom logic still needs an explicit predicate.&lt;/p&gt;

&lt;p&gt;One thing I noticed: this pairs well with typed AI SDK responses, where you're often getting back something like &lt;code&gt;CompletionResult | RateLimitError | null&lt;/code&gt; from a batch call and need to split it into separate arrays. Used to be a predicate per type. Now it's an inline condition and the types just follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  NoInfer Is Nine Characters and It Stopped a Real Bug
&lt;/h2&gt;

&lt;p&gt;I'll be honest — I thought &lt;code&gt;NoInfer&amp;lt;T&amp;gt;&lt;/code&gt; (added in 5.4) was a library-author concern when I first read about it. I was wrong. I ran into the problem it solves within two weeks.&lt;/p&gt;

&lt;p&gt;The setup is — okay, let me back up a second. We have a config resolution function that looks up model configurations by key and falls back to a default. The default value was silently widening the inferred type, because TypeScript was using the fallback argument to infer &lt;code&gt;T&lt;/code&gt; rather than the caller's intended type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without NoInfer: the fallback widens T&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;  &lt;span class="c1"&gt;// TypeScript infers T partly from here — the problem&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// resolveConfig(myRegistry, 'gpt-4o', { temperature: 0.7 })&lt;/span&gt;
&lt;span class="c1"&gt;// infers T as { temperature: number }, not ModelConfig&lt;/span&gt;
&lt;span class="c1"&gt;// downstream code that expects ModelConfig now has no error&lt;/span&gt;

&lt;span class="c1"&gt;// With NoInfer: only the registry type informs T&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NoInfer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="c1"&gt;// can't influence T inference&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That function had been silently widening types for months. I'm not 100% sure we ever shipped a production bug because of it — but we had tests passing on wider types than they should have been, and that's a bad place to be.&lt;/p&gt;

&lt;p&gt;Reach for &lt;code&gt;NoInfer&lt;/code&gt; when you write generic utilities with default or fallback parameters. You'll know when you need it because you'll see the inferred type being wider than you intended, and you'll wonder why. Then it'll click immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  verbatimModuleSyntax Is the Price of Admission for ESM in 2026
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;verbatimModuleSyntax&lt;/code&gt; shipped in 5.0, but I keep seeing teams who've skipped it — usually because enabling it immediately breaks forty files and no one wants to deal with that mid-sprint. I deferred it for months too. But now that Node.js 22+ handles TypeScript natively via &lt;code&gt;--experimental-strip-types&lt;/code&gt;, and TypeScript 5.8 introduced &lt;code&gt;--erasableSyntaxOnly&lt;/code&gt; more cleanly, &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; is effectively required if you want your TypeScript to run without a transformation step.&lt;/p&gt;

&lt;p&gt;Here's the thing: when this flag is off, TypeScript can rewrite your imports. An &lt;code&gt;import { SomeInterface }&lt;/code&gt; that's type-only might get stripped, or it might get emitted, depending on whether the compiler thinks it's a value. That ambiguity is fine until you're on an edge runtime or a tool that doesn't do the same inference TypeScript does. Then you get subtle bundling issues — not crashes, usually, just slightly wrong output that's hard to trace back.&lt;/p&gt;

&lt;p&gt;The fix is boring: turn the flag on, let the compiler tell you which imports need &lt;code&gt;import type&lt;/code&gt;, run the VS Code quick-fix on each file. It took me about ninety minutes across our codebase. I haven't thought about import emission since.&lt;/p&gt;

&lt;p&gt;If you're still on a CommonJS Node.js setup with no edge runtime in sight, you can defer this. If you're deploying to Cloudflare Workers, Deno, or native Node.js strip mode — do it now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Features I Overhyped in Slack, and Two Small Wins That Earned Their Place
&lt;/h2&gt;

&lt;p&gt;After migrating the &lt;code&gt;using&lt;/code&gt; declarations and cleaning up the predicates, I made the mistake of posting "TypeScript 5.x is actually great" in our engineering channel and listing six more features I was excited to explore. Two of them did not pan out the way I expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decorator metadata (5.2).&lt;/strong&gt; I went in thinking we could annotate validation schemas directly on request classes, reflect on them at runtime, and eliminate some boilerplate in our API layer. You can do that. The problem is runtime support — you need either a polyfill or an environment that natively supports the TC39 Decorator Metadata proposal. For our Node.js services, fine. For the React front-end running in whatever browsers our users have, I didn't want to ship a polyfill for something Zod schemas solved in an afternoon. If you're building a framework where you control the runtime, worth evaluating. For application code, the cost-benefit didn't work out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Const type parameters (5.0).&lt;/strong&gt; I do use these, just much less often than I expected. When you declare &lt;code&gt;function foo&amp;lt;const T&amp;gt;()&lt;/code&gt;, TypeScript infers literal types for &lt;code&gt;T&lt;/code&gt; instead of widening. Useful for typed config builders and tuple utilities. I've reached for it maybe eight times in the past year. Good to know it exists; not a weekly-driver feature.&lt;/p&gt;

&lt;p&gt;The smaller wins that actually earned their place: preserved narrowing after last assignment (5.4) caught a real bug where a closure was capturing a variable I thought was permanently narrowed but could have been reassigned before the callback fired. The compiler surfaced it before it shipped. And regex syntax checking (5.5) has caught two invalid patterns that would have been silent runtime failures — the kind of thing that used to be completely invisible to the type system.&lt;/p&gt;




&lt;p&gt;Anyway — those four. &lt;code&gt;using&lt;/code&gt; declarations and inferred predicates are the ones I'd push on any TypeScript team right now, regardless of what kind of code they're writing. &lt;code&gt;verbatimModuleSyntax&lt;/code&gt; is a one-time cost you pay once and never think about again. &lt;code&gt;NoInfer&lt;/code&gt; you'll understand the second you hit the problem it solves.&lt;/p&gt;

&lt;p&gt;The rest of 5.x I'd skim when a release drops and learn on demand. The six features I was posting about excitedly in Slack? Genuinely cool. Not in my daily workflow.&lt;/p&gt;

&lt;p&gt;These four are.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>typescript5</category>
      <category>production</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Serverless vs Containers in 2026: Why I Stopped Treating It as a Binary Choice</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:18:12 +0000</pubDate>
      <link>https://dev.to/synsun/serverless-vs-containers-in-2026-why-i-stopped-treating-it-as-a-binary-choice-3fjl</link>
      <guid>https://dev.to/synsun/serverless-vs-containers-in-2026-why-i-stopped-treating-it-as-a-binary-choice-3fjl</guid>
      <description>&lt;p&gt;About 14 months ago, my team of four migrated our entire backend — a fairly standard Node.js/Python mix serving a B2B SaaS product — fully onto AWS Lambda. We'd read the same blog posts you probably have. Pay for what you use, infinite scale, no servers to babysit. We were sold.&lt;/p&gt;

&lt;p&gt;Six months later, two of our services were back in containers on ECS Fargate.&lt;/p&gt;

&lt;p&gt;Not because Lambda failed us. It's more complicated than that. This post is my attempt to be honest about what actually happened, what the tradeoffs look like in practice in early 2026, and what I'd tell a team starting fresh today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Went All-In on Serverless (And What Actually Worked)
&lt;/h2&gt;

&lt;p&gt;Before I get into the friction: serverless genuinely delivers for certain workloads, and I want to say that clearly before this turns into another "we went back to containers" post that implies the whole thing is overhyped. It isn't.&lt;/p&gt;

&lt;p&gt;Our webhook processing pipeline — where we ingest events from third-party integrations and fan them out to customer-specific handlers — is still on Lambda and I have zero plans to change that. It's processing about 2-3 million invocations a day now, and the cost is roughly $40/month. The same workload on containers would require careful autoscaling configuration, and we'd almost certainly be over-provisioned most of the time because the traffic pattern is genuinely spiky: bursts of thousands of events followed by minutes of nothing.&lt;/p&gt;

&lt;p&gt;The other thing serverless got right for us: the team doesn't have to think about it. Lambda functions deploy in under two minutes, they scale, they recover from errors automatically. For a four-person team where nobody has "DevOps" in their title, that operational simplicity is worth real money.&lt;/p&gt;

&lt;p&gt;The tooling also got genuinely better between 2024 and now. AWS SAM plus GitHub Actions is a clean deployment story. The old pain of local Lambda testing has mostly been solved — &lt;code&gt;sam local invoke&lt;/code&gt; is workable, not perfect, but I stopped complaining about it months ago.&lt;/p&gt;

&lt;p&gt;The sweet spot for Lambda: irregular or unpredictable traffic, cold invocations that are tolerable for the use case, discrete bounded work, and a team small enough that operational overhead eats into actual product time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Serverless Story Started to Break
&lt;/h2&gt;

&lt;p&gt;We ran our main user-facing API on Lambda for about four months. Authentication, data fetching, the synchronous endpoints our SaaS customers hit directly. And it worked — until 2am on a Tuesday, when our largest customer kicked off a bulk export job that slammed our database connection pool.&lt;/p&gt;

&lt;p&gt;Lambda functions don't maintain persistent connections. Every cold start means a new connection. When 200 functions spun up simultaneously and each tried to grab a database handle from our RDS instance, we had a very fun morning.&lt;/p&gt;

&lt;p&gt;RDS Proxy helped. We implemented it and it did solve the immediate connection storm. But it added 3-5ms of latency per query in our benchmarks, and it added another managed service to reason about. Our connection pooling logic — which had been invisible when we ran a containerized API server — was now something we actively had to debug and configure.&lt;/p&gt;

&lt;p&gt;The deeper issue was architectural. Lambda encourages stateless, short-lived compute, which is correct and good engineering, but our API had accumulated a few stateful patterns we hadn't noticed until serverless made them painful. In-memory caching with a warm LRU cache we'd been relying on without realizing it. Some SDK client initialization done lazily that assumed a long-lived process. You could argue we should have caught these earlier — fair — but the migration surfaced a whole class of assumptions we'd made about "the server" that didn't hold anymore. These weren't Lambda problems exactly. They were invisible debt that Lambda forced us to pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cold Start Problem in 2026 Is Better, But Not Gone
&lt;/h2&gt;

&lt;p&gt;I genuinely thought cold starts were a solved problem when we made our migration decision. That was wrong.&lt;/p&gt;

&lt;p&gt;Lambda SnapStart — originally Java-only — extended to Node.js 22 and Python 3.13 runtimes in late 2025. The basic idea: AWS snapshots your initialized function and restores from that snapshot instead of initializing from scratch. In practice, this brought our cold starts from 600-900ms down to 80-150ms for most functions. Real improvement.&lt;/p&gt;

&lt;p&gt;But there are edge cases. SnapStart doesn't play nicely with certain SDK initialization patterns. We hit a weird issue where the AWS SDK v3 client caching behavior caused stale credential state in restored snapshots — silent auth failures for about 0.1% of cold-start invocations. Took us two days to track down. It's documented in a GitHub issue thread (aws/aws-lambda-snapstart-java #89, though the Node behavior lives in a comment thread rather than its own issue, which is... typical).&lt;/p&gt;

&lt;p&gt;For Python-heavy ML inference, cold starts are still brutal. A Lambda function loading a scikit-learn model plus its dependencies is going to take 3-8 seconds on a cold start depending on model size. Lambda container image support helps — you can package up to 10GB now — but you're still paying the initialization cost every time a new instance spins up. I moved our ML inference endpoints to containers for exactly this reason: a persistent ECS service that keeps the model warm is just better for that use case, full stop.&lt;/p&gt;

&lt;p&gt;Here's what the contrast looks like in actual code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lambda: webhook handler — stateless, spiky, exactly the right use case
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tracer&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Initialized once per lifecycle — SnapStart snapshots this state
&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@tracer.capture_lambda_handler&lt;/span&gt;
&lt;span class="nd"&gt;@logger.inject_lambda_context&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;process_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ECS: ML inference service — persistent process, model stays loaded in memory
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;joblib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# This is the whole point: loads ONCE at container start, not on every invocation
# On Lambda, a cold start would re-load this 3-8 seconds every time
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;joblib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/app/models/churn_predictor_v3.pkl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/predict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)]])&lt;/span&gt;
    &lt;span class="n"&gt;probability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;churn_probability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probability&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is obvious when you lay it out this way. Lambda's cold start cost only becomes a real problem when you have heavy initialization — but "heavy initialization" turns out to describe a lot of production workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container Economics: When the Math Actually Flips
&lt;/h2&gt;

&lt;p&gt;I spent too long assuming serverless was inherently cheaper. "Pay per invocation" sounds obviously better than always-running containers. The math gets interesting.&lt;/p&gt;

&lt;p&gt;Our API sits at roughly 8 million requests per day. With 512MB functions averaging 50ms execution time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request charges: 240M/month → essentially negligible (~$0.05)&lt;/li&gt;
&lt;li&gt;Compute: 240M × 0.05s × 0.5GB = 6M GB-seconds → ~$100/month&lt;/li&gt;
&lt;li&gt;Supporting services (RDS Proxy, NAT Gateway egress, X-Ray): ~$65/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: roughly $165/month for Lambda.&lt;/p&gt;

&lt;p&gt;Two Fargate tasks (1 vCPU, 2GB RAM each) running 24/7: about $140/month. With a simple autoscaling policy that steps to four tasks during business hours, you land at ~$170/month.&lt;/p&gt;

&lt;p&gt;Basically the same cost. At 2x our current scale, containers get cheaper — warm instances mean consistent p95 latency, persistent connection pools eliminate the RDS Proxy overhead, and we can be more precise about autoscaling than Lambda's concurrency model allows.&lt;/p&gt;

&lt;p&gt;This math assumes someone actively tuning Fargate task sizes and autoscaling thresholds, though. That's real work. If your team doesn't have the bandwidth for it, the serverless model's operational simplicity is itself worth something — I wouldn't optimize purely for raw infrastructure cost if the alternative is your engineers spending their afternoons staring at CloudWatch dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Actually Recommend
&lt;/h2&gt;

&lt;p&gt;I pushed our team to go all-serverless partly because I was excited and partly because I'd read too many AWS blog posts written by people whose job is to sell you AWS services. That's not a knock on the technology — just useful context for how those blog posts are framed.&lt;/p&gt;

&lt;p&gt;For event-driven async workloads — webhooks, queue consumers, scheduled jobs, file processing pipelines — Lambda is genuinely the right default. Traffic is irregular, the work is discrete, and the operational overhead is low enough that a small team can mostly forget it exists. That's a real win.&lt;/p&gt;

&lt;p&gt;For user-facing synchronous APIs, it depends. Latency requirements under 100ms p99, heavy in-memory state, ML model serving, or traffic steady enough to keep instances warm — containers are probably the right call. ECS Fargate is my default recommendation there. You don't need to manage EC2 instances unless your infra team is actually sized for that work; Fargate hits the sweet spot.&lt;/p&gt;

&lt;p&gt;The hybrid architecture isn't a cop-out. It's how most mature backend systems actually end up, because different workloads have genuinely different characteristics. My mistake was treating this as either/or — that's a framing problem, not a technical one.&lt;/p&gt;

&lt;p&gt;The "no servers to manage" promise of serverless is real, but it trades server management for function management: cold start tuning, concurrency limits, timeout edge cases, VPC routing. Lower stakes, but not zero stakes. A Lambda function silently timing out at 29 seconds on an edge case is harder to notice than a container dropping out of your load balancer's health check rotation. I've experienced both, and neither is fun at 2am.&lt;/p&gt;

&lt;p&gt;Our current setup: Lambda for async pipelines, Fargate for synchronous APIs, shared infrastructure (VPC, RDS, ElastiCache) that both can reach. Four engineers, two environments, one YAML-heavy afternoon to wire up the networking. That's the architecture I'd start with if I were doing it again.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>containers</category>
      <category>awslambda</category>
      <category>fargate</category>
    </item>
    <item>
      <title>Redis vs Valkey in 2026: What the License Fork Actually Changed</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 20:17:42 +0000</pubDate>
      <link>https://dev.to/synsun/redis-vs-valkey-in-2026-what-the-license-fork-actually-changed-1kni</link>
      <guid>https://dev.to/synsun/redis-vs-valkey-in-2026-what-the-license-fork-actually-changed-1kni</guid>
      <description>&lt;p&gt;When Redis Ltd announced the license change in March 2024, I was in the middle of planning a caching layer for a mid-sized SaaS product — four engineers, roughly 80k daily active users, nothing hyperscale but enough that infrastructure decisions have real consequences. My first reaction was basically: &lt;em&gt;this is annoying, but probably fine.&lt;/em&gt; I figured the open source community would grumble for a few weeks and move on.&lt;/p&gt;

&lt;p&gt;I was wrong. The fork that came out of it — Valkey, now a Linux Foundation project — turned out to be more interesting than I expected.&lt;/p&gt;

&lt;p&gt;Here's what I actually learned after running both in staging, migrating one production service, and spending way too many evenings reading GitHub issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  March 2024: Why the SSPL Switch Was a Bigger Deal Than It Looked
&lt;/h2&gt;

&lt;p&gt;Redis Ltd moved to dual-license Redis under RSAL (Redis Source Available License) and SSPL (Server Side Public License). Both sound fine until you notice that the Open Source Initiative does not consider SSPL an open source license. MongoDB used it first, and the controversy followed them too.&lt;/p&gt;

&lt;p&gt;The practical implication: any cloud provider building a managed Redis-compatible service would now need a commercial agreement with Redis Ltd. The old BSD license let them run Redis however they wanted. SSPL did not.&lt;/p&gt;

&lt;p&gt;So within weeks, Amazon, Google, Oracle, Ericsson, and others backed a fork. The Linux Foundation accepted it in late March 2024. They called it Valkey — and started from Redis 7.2.4, the last version under the BSD license.&lt;/p&gt;

&lt;p&gt;What struck me was how fast this moved. Fork announcement, Linux Foundation acceptance, first release (7.2.5) — all within about two months. Compare that to the years-long drama around other high-profile forks. Someone had clearly been planning for this scenario before it was announced publicly.&lt;/p&gt;

&lt;p&gt;The right way to think about Valkey is not "Redis but free." The governance model is genuinely different. Redis Ltd controls Redis. Valkey is steered by a Technical Steering Committee across multiple companies with no single controlling entity. Whether that matters depends on how much you care about vendor lock-in at the infrastructure layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valkey Is Not the Same Project Anymore — and Neither Is Redis
&lt;/h2&gt;

&lt;p&gt;I assumed Valkey would just track Redis feature-for-feature, staying compatible but staying behind. That's not what happened.&lt;/p&gt;

&lt;p&gt;By late 2024, Valkey 8.0 shipped with meaningful performance work around I/O threading. Redis had always had a reputation for being single-threaded on commands (even though I/O threading was added in Redis 6.0), and Valkey's team pushed further on that in 8.0. In my own synthetic benchmarks — 8 CPU cores, mostly GET/SET workloads with some sorted set operations — Valkey 8.0 was measurably faster than Redis 7.4 at high connection counts. Maybe 15-20% throughput improvement in the scenarios I tested. Not nothing.&lt;/p&gt;

&lt;p&gt;The config that mattered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# valkey.conf — enable threaded I/O (Valkey 8.x)
&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;-&lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;io&lt;/span&gt;-&lt;span class="n"&gt;threads&lt;/span&gt;-&lt;span class="n"&gt;do&lt;/span&gt;-&lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;yes&lt;/span&gt;

&lt;span class="c"&gt;# This was available in Redis 6.0+ too, but Valkey's default
# behavior and threading model changed in 8.0
# Check your cpu count: don't set io-threads &amp;gt; (cpu_count - 1)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not confident this scales beyond the specific workload I tested — scripting-heavy or transaction-heavy workloads behave differently. But the point stands: Valkey is making its own performance bets now, not just merging Redis commits.&lt;/p&gt;

&lt;p&gt;On the Redis side, and this genuinely surprised me: Redis 8 added vector set support and kept iterating on Redis Query Engine (the search/vector work from the old RediSearch module). That's real differentiation. If you're building anything combining caching with vector similarity search, Redis has a more integrated story than Valkey does right now.&lt;/p&gt;

&lt;p&gt;By early 2026, the two projects have genuinely diverged. Not dramatically — still about 90% compatible at the command level — but enough that you can't pick one on inertia alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Client Library Situation Nobody Warned Me About
&lt;/h2&gt;

&lt;p&gt;This is where I hit an actual wall.&lt;/p&gt;

&lt;p&gt;I was migrating a Node.js service from ElastiCache (which AWS quietly switched to Valkey by default for new clusters in 2024) to self-hosted Valkey for cost reasons. Our client was &lt;code&gt;ioredis&lt;/code&gt;, which we'd used for years.&lt;/p&gt;

&lt;p&gt;I pushed the config change on a Friday afternoon. Forty minutes later, the service started throwing intermittent connection errors — not enough to page anyone, but enough to show up in our error rate dashboard, which I happened to be watching for a completely unrelated reason. We rolled back. I spent the weekend reading ioredis GitHub issues and found the actual problem buried in a thread from mid-2025.&lt;/p&gt;

&lt;p&gt;The issue was how certain client libraries handled &lt;code&gt;CLIENT INFO&lt;/code&gt; and &lt;code&gt;HELLO&lt;/code&gt; commands during connection setup — version negotiation behavior that had diverged between Valkey 8.x and what ioredis expected based on Redis 7.x behavior. Not exactly a Valkey bug, more of a "the ecosystem assumed Redis forever" problem. The fix existed in an ioredis release that had already shipped, but our lock file had pinned us to an older version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your client library's Valkey compatibility explicitly, and check recent release notes before migrating.&lt;/strong&gt; "Redis-compatible" does not mean "tested against Valkey 8." Some libraries are explicit about this now; many aren't.&lt;/p&gt;

&lt;p&gt;The managed service picture is cleaner. AWS MemoryDB and ElastiCache both support Valkey and handle client library abstraction for you. If you're on a managed offering, the migration path is more straightforward than self-hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Managed Services Landscape Settled
&lt;/h2&gt;

&lt;p&gt;The cloud picture shifted more decisively than I expected.&lt;/p&gt;

&lt;p&gt;AWS made Valkey the default for new ElastiCache and MemoryDB instances in 2024. You can still choose Redis — they have a commercial agreement with Redis Ltd — but the default changed. That's operationally significant: environments spun up from templates or Terraform modules defaulted to Valkey unless someone explicitly overrode it.&lt;/p&gt;

&lt;p&gt;Google Cloud Memorystore offers Valkey as well. Azure still has Azure Cache for Redis, backed by actual Redis under their own commercial arrangement. The big three have split: AWS and GCP leaning Valkey, Azure leaning Redis.&lt;/p&gt;

&lt;p&gt;Redis Insight (the GUI tool) works fine with Valkey — RESP protocol is shared, so most tooling in the ecosystem still functions. The divergence shows up in edge cases: specific command behaviors, module support, vector search.&lt;/p&gt;

&lt;p&gt;Which brings me to the most important decision factor right now. If you're using Redis Modules heavily — RedisSearch, RedisJSON, RedisTimeSeries — your decision is mostly already made. Those are Redis Ltd products, not part of Valkey. Community-driven Valkey equivalents are emerging but aren't at the same maturity level yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Actual Recommendation
&lt;/h2&gt;

&lt;p&gt;I thought about hedging this and decided not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On managed AWS or GCP: use Valkey.&lt;/strong&gt; The operational complexity is identical, the defaults are already pointing there, and there's no reason to route Redis Ltd licensing costs through your cloud provider for standard caching workloads. Session storage, rate limiting, pub/sub, leaderboards with sorted sets — Valkey handles all of it, and the performance characteristics are at least comparable, often better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted with heavy Redis Stack module usage: stay on Redis.&lt;/strong&gt; Valkey doesn't have mature equivalents yet, and the compatibility gap is real if you've built on top of RedisSearch or RedisJSON. Watch the Valkey module ecosystem closely — that's where the gap closes or doesn't over the next year or so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting a new project from scratch in 2026?&lt;/strong&gt; Valkey is my default. The governance model is more stable long-term — Linux Foundation backing means no surprise license changes. The performance work is real. For typical web application use cases, you're not giving anything up.&lt;/p&gt;

&lt;p&gt;The one caveat I'd offer: Redis has 15 years of tutorials, Stack Overflow answers, and tribal knowledge behind it. Valkey is two years old. That documentation gap is real, and for smaller teams without deep Redis expertise, it shows. Plan for that.&lt;/p&gt;

&lt;p&gt;For my team, we migrated session caching and rate limiting to Valkey 8.x on managed infrastructure. The search service stayed on Redis — we're using Redis Query Engine heavily and I don't want to rewrite that integration. Both are running fine. The migration I thought would take a weekend took about three weeks once you factor in the client library audit, testing, and the Friday rollback incident.&lt;/p&gt;

&lt;p&gt;Worth it. The fact that a fork can happen, get Linux Foundation backing, and reach production-grade maturity in two years says something — and it's a better outcome than everyone just swallowing the SSPL change and moving on.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>valkey</category>
      <category>opensource</category>
      <category>caching</category>
    </item>
    <item>
      <title>LangChain vs LlamaIndex vs Haystack: Lo que aprendí construyendo RAG en producción</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 18:23:04 +0000</pubDate>
      <link>https://dev.to/synsun/langchain-vs-llamaindex-vs-haystack-lo-que-aprendi-construyendo-rag-en-produccion-2f63</link>
      <guid>https://dev.to/synsun/langchain-vs-llamaindex-vs-haystack-lo-que-aprendi-construyendo-rag-en-produccion-2f63</guid>
      <description>&lt;p&gt;Pasé las últimas dos semanas migrando un sistema RAG entre tres frameworks — y no fue una decisión voluntaria. Empezamos con LangChain, las abstracciones se volvieron difíciles de mantener, alguien del equipo sugirió LlamaIndex, probamos eso, y al final terminé revisando Haystack casi de casualidad mientras buscaba una solución a otro problema. Así que lo hice bien: monté un benchmark real con nuestros datos reales y medí lo que importa.&lt;/p&gt;

&lt;p&gt;Trabajo en un equipo de seis personas, construimos un sistema RAG para un cliente fintech. El corpus tiene alrededor de 480k documentos — PDFs escaneados (los peores, siempre), HTML de sus portales internos, y algo de Markdown de sus wikis. Presupuesto máximo de inferencia: $800/mes. Eso descartó varias opciones antes de llegar siquiera a cuestiones de arquitectura.&lt;/p&gt;

&lt;p&gt;Las versiones que probé: LangChain 0.3.15, LlamaIndex 0.12.3, Haystack 2.7.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  LangChain 0.3.15 — El ecosistema que a veces juega en tu contra
&lt;/h2&gt;

&lt;p&gt;LangChain fue mi punto de partida porque ya lo conocía. Y ese mismo conocimiento fue parte del problema — teníamos código de un proyecto de hace ocho meses que hubo que reescribir parcialmente porque las interfaces habían cambiado. Otra vez.&lt;/p&gt;

&lt;p&gt;La API ha mejorado, seré honesto. LCEL (LangChain Expression Language) funciona bien cuando lo entiendes de verdad. La composición de cadenas con el operador &lt;code&gt;|&lt;/code&gt; queda limpia, y el tracing con LangSmith nos ahorró horas de debugging en staging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.runnables&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RunnablePassthrough&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.output_parsers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrOutputParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;

&lt;span class="c1"&gt;# El retriever con score_threshold fue la parte que más tardamos en afinar.
# 0.72 fue nuestro número después de ~200 consultas de evaluación manual.
&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# LCEL en acción — se lee bien y en general funciona bien
&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;format_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RunnablePassthrough&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;StrOutputParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El problema no es la calidad técnica del framework. LangChain intenta cubrir todo — agentes, memoria, herramientas, cadenas, RAG — y el precio de esa amplitud es que la profundidad en retrieval específicamente no está a la altura de la competencia. Cuando quise implementar sentence window retrieval o hierarchical node parsing, tuve que construirlo yo mismo o buscar en la comunidad. Hay soluciones, sí, pero en cinco versiones distintas de la librería, no siempre queda claro cuál aplica a la tuya.&lt;/p&gt;

&lt;p&gt;Un momento concreto que me sacó de quicio: en diciembre intenté usar &lt;code&gt;MultiVectorRetriever&lt;/code&gt; con documentos largos y me topé con un bug reportado en el GitHub de LangChain (issue #12847) donde los document IDs se sobreescribían silenciosamente. Funcionaba en local pero no en producción — diferencia en la versión de Chroma, resultó ser. Dos días perdidos en eso.&lt;/p&gt;

&lt;p&gt;Para proyectos donde necesitas máxima flexibilidad y tienes equipo dispuesto a mantener código propio, LangChain tiene sentido. Para RAG puro, hay mejores opciones.&lt;/p&gt;

&lt;h2&gt;
  
  
  LlamaIndex 0.12.3 — Cuando la calidad del retrieval realmente importa
&lt;/h2&gt;

&lt;p&gt;Cambié a LlamaIndex con escepticismo. Me sorprendió.&lt;/p&gt;

&lt;p&gt;Está mucho más enfocado en el problema de indexing y retrieval, y eso se nota en los detalles de implementación. La combinación que más movió nuestros números fue &lt;code&gt;SentenceWindowNodeParser&lt;/code&gt; con &lt;code&gt;MetadataReplacementPostProcessor&lt;/code&gt;. La idea: indexas oraciones individuales para tener precisión en el retrieval, pero al momento de generar la respuesta reemplazas ese fragmento con una ventana de contexto mayor. Bajamos el hallucination rate de ~12% a aproximadamente 4% con este cambio solo. Eso es lo que quiero decir con "profundidad en retrieval" — no una feature de marketing, sino herramientas concretas para un problema concreto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core.node_parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceWindowNodeParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core.postprocessor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MetadataReplacementPostProcessor&lt;/span&gt;

&lt;span class="c1"&gt;# Esta combinación fue la que realmente cambió nuestras métricas de evaluación
&lt;/span&gt;&lt;span class="n"&gt;node_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SentenceWindowNodeParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;# 3 oraciones de contexto por lado
&lt;/span&gt;    &lt;span class="n"&gt;window_metadata_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;original_text_metadata_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;original_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node_parser&lt;/span&gt;
&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;  &lt;span class="c1"&gt;# más pequeño de lo que usábamos antes con LangChain
&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;show_progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# El postprocessor reemplaza el nodo recuperado con su ventana de contexto
# justo antes de armar el prompt — la magia está aquí
&lt;/span&gt;&lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_query_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;similarity_top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node_postprocessors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;MetadataReplacementPostProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_metadata_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo que no me convenció: el sistema de configuración global con &lt;code&gt;Settings&lt;/code&gt; se siente torpe cuando tienes múltiples pipelines en el mismo proceso. Cada vez que necesitábamos cambiar algo para un subíndice específico, sobreescribíamos configuraciones globales y rezábamos para que los tests de integración no se pisaran entre sí. El &lt;code&gt;ServiceContext&lt;/code&gt; directo era una opción pero está deprecado en 0.12.x, y la historia de migración no es del todo limpia.&lt;/p&gt;

&lt;p&gt;También: la documentación asume que usas sus abstracciones de punta a punta. Cuando quisimos integrar LlamaIndex solo para el retrieval y usar nuestro propio sistema de generación, hubo más fricción de lo esperado. No imposible, pero tampoco el camino documentado.&lt;/p&gt;

&lt;p&gt;Igual, si el retrieval de calidad es tu prioridad principal, LlamaIndex tiene la mejor caja de herramientas de los tres. Por bastante margen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Haystack 2.7.1 — El que nadie menciona en los tutoriales
&lt;/h2&gt;

&lt;p&gt;Seré directo: no esperaba que Haystack me sorprendiera tanto.&lt;/p&gt;

&lt;p&gt;Tiene una fracción del mindshare de los otros dos, y eso es una lástima porque para ambientes de producción tiene ventajas reales. El modelo mental es distinto al de LangChain y LlamaIndex: todo es un pipeline de componentes conectados, y ese pipeline es un ciudadano de primera clase — lo puedes serializar a YAML, versionarlo, desplegarlo con configuración externa.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pipeline.yaml — esto es real, no pseudocódigo de blog&lt;/span&gt;
&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;retriever&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;haystack.components.retrievers.InMemoryEmbeddingRetriever&lt;/span&gt;
    &lt;span class="na"&gt;init_parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;document_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!component&lt;/span&gt; &lt;span class="s"&gt;document_store&lt;/span&gt;
      &lt;span class="na"&gt;top_k&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;prompt_builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;haystack.components.builders.PromptBuilder&lt;/span&gt;
    &lt;span class="na"&gt;init_parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;Contexto: {% for doc in documents %}{{ doc.content }}{% endfor %}&lt;/span&gt;
        &lt;span class="s"&gt;Pregunta: {{ question }}&lt;/span&gt;
        &lt;span class="s"&gt;Respuesta:&lt;/span&gt;
&lt;span class="na"&gt;connections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;sender&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;retriever.documents&lt;/span&gt;
    &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prompt_builder.documents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso suena aburrido hasta que estás en tu tercer deployment del día y alguien de QA quiere correr la versión exacta del pipeline que falló en staging. Ser capaz de versionar el pipeline como un artefacto separado del código de aplicación es algo que los otros dos no tienen de serie.&lt;/p&gt;

&lt;p&gt;Donde me costó caro: la comunidad es significativamente más pequeña. Pasé cuatro horas un martes intentando entender por qué mi &lt;code&gt;DocumentSplitter&lt;/code&gt; ignoraba los saltos de página en PDFs. Nada en Stack Overflow. El Discord de Haystack tenía una pregunta similar sin respuesta de hace tres meses. Al final leí el código fuente — hay un parámetro &lt;code&gt;split_by="page"&lt;/code&gt; que no está en la guía de inicio rápido pero sí en el API reference si sabes dónde buscar. Ese tipo de fricción se acumula cuando hay plazos encima.&lt;/p&gt;

&lt;p&gt;La observabilidad está mejor pensada, especialmente si ya usas OpenTelemetry. El tracing sale de caja con detalle sobre qué componente tomó cuánto tiempo, sin pagar por herramientas externas.&lt;/p&gt;

&lt;p&gt;No sé si Haystack escala más allá de lo que nosotros probamos — alrededor de 2,000 queries diarios en producción. Mi intuición dice que sí, pero no tengo datos propios para afirmarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Viernes que Casi Me Hace Cambiar de Opinión
&lt;/h2&gt;

&lt;p&gt;Estaba casi decidido por LlamaIndex cuando pasó algo que me hizo repensar el criterio de evaluación completo.&lt;/p&gt;

&lt;p&gt;Un viernes por la tarde — clásico — empujé una actualización al servicio de indexación. El proceso de background para indexar documentos nuevos empezó a consumir memoria de forma inconsistente. No todos los runs, tal vez uno de cada cuatro. El problema: en LlamaIndex 0.12.x, hay edge cases en el manejo de memoria al indexar documentos con embeddings de páginas muy largas — PDFs de más de 200 páginas en nuestro caso. Encontré la issue en GitHub pero no había fix todavía.&lt;/p&gt;

&lt;p&gt;La solución que terminé usando fue procesar esos documentos en batches más pequeños con un wrapper propio. Funcionó, pero me dejó pensando: ¿cuánto tiempo debería gastar parchando comportamiento de framework versus construyendo producto?&lt;/p&gt;

&lt;p&gt;Pensaba que la calidad del retrieval era el criterio más importante, pero en producción la operabilidad importa tanto o más. La pregunta que no me había hecho al inicio del benchmark.&lt;/p&gt;

&lt;p&gt;Con Haystack, el mismo escenario hubiera sido más predecible — el pipeline explícito hace más obvio dónde puede fallar y dónde intervenir. Con LangChain hubiera tenido más opciones de configuración pero también más superficie de error. No hay respuesta perfecta. Solo hay tradeoffs que vale la pena conocer antes de comprometer el stack de un cliente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi Recomendación (sin el "depende de tu caso de uso")
&lt;/h2&gt;

&lt;p&gt;Voy directo: si en 2026 estás arrancando un nuevo proyecto RAG de cero con un corpus de escala media o grande, te recomiendo LlamaIndex para la capa de indexación y retrieval. La toolbox de retrieval avanzado no tiene competencia real en los otros dos frameworks, y esa diferencia se traduce en métricas concretas.&lt;/p&gt;

&lt;p&gt;Si tu equipo valora la operabilidad y la reproducibilidad de pipelines sobre la velocidad de prototipado inicial — y especialmente si ya tienen cultura de infraestructura-como-código — considera Haystack. La curva de adopción es más alta, pero lo que ganas en predictibilidad en producción compensa.&lt;/p&gt;

&lt;p&gt;LangChain tiene sentido en un escenario específico: si necesitas construir agentes con herramientas complejas, o si tu equipo ya lo conoce bien y el costo de migración supera el beneficio. El ecosistema de integraciones es el más amplio de los tres. Pero para RAG puro, es la opción con más overhead de mantenimiento.&lt;/p&gt;

&lt;p&gt;Aquí lo que no esperaba aprender: el framework que eliges afecta cómo piensas el problema, no solo cómo lo implementas. LangChain te hace pensar en cadenas. LlamaIndex te hace pensar en nodos y retrieval. Haystack te hace pensar en pipelines y componentes. Si tu modelo mental ya encaja con uno de esos paradigmas, eso es información válida para la decisión — más válida que cualquier benchmark sintético.&lt;/p&gt;

&lt;p&gt;Mi setup actual: LlamaIndex para indexación y retrieval, FastAPI por encima, métricas propias con Prometheus. Ninguno de los tres me convenció con su historia de observabilidad nativa, así que ahí construí algo propio. En los próximos meses voy a mirar si la integración OpenTelemetry de Haystack madura lo suficiente para reemplazar eso — pero por ahora, lo que tenemos funciona.&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>llamaindex</category>
      <category>haystack</category>
      <category>rag</category>
    </item>
    <item>
      <title>Docker Compose vs Kubernetes en 2026: Cuándo Usar Cuál (Y Cuándo te Estás Complicando la Vida)</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 18:22:34 +0000</pubDate>
      <link>https://dev.to/synsun/docker-compose-vs-kubernetes-en-2026-cuando-usar-cual-y-cuando-te-estas-complicando-la-vida-2p4k</link>
      <guid>https://dev.to/synsun/docker-compose-vs-kubernetes-en-2026-cuando-usar-cual-y-cuando-te-estas-complicando-la-vida-2p4k</guid>
      <description>&lt;p&gt;Voy a ser directo: durante el último año cambié de opinión dos veces sobre este tema. Empecé convencido de que Kubernetes era la respuesta correcta para casi todo. Después migré tres proyectos de vuelta a Docker Compose. Y ahora, con la cabeza más fría, creo que puedo darte algo más útil que un vago "depende del caso de uso".&lt;/p&gt;

&lt;p&gt;Trabajo con un equipo de cuatro personas. Manejamos una plataforma SaaS con entre 8.000 y 12.000 usuarios activos según el mes, con picos bastante predecibles los martes y jueves. No somos Netflix. Tampoco somos un side project de fin de semana. Estamos exactamente en esa zona gris donde la elección importa de verdad.&lt;/p&gt;




&lt;h2&gt;
  
  
  Por Qué Docker Compose Llega Más Lejos de lo que la Mayoría Cree
&lt;/h2&gt;

&lt;p&gt;Hay una narrativa muy extendida que dice que Compose es "solo para desarrollo local" y que en cuanto quieres hacer algo serio tienes que saltar a Kubernetes. Eso es, con todo el respeto, una simplificación bastante dañina.&lt;/p&gt;

&lt;p&gt;Corrí Docker Compose en producción durante 14 meses. Un VPS de Hetzner, 8 vCPUs, 32 GB de RAM, backups diarios con restic y un nginx actuando de reverse proxy delante de todo. El uptime fue del 99.7%. Los deployments tardaban unos 40 segundos. Y el &lt;code&gt;docker-compose.yml&lt;/code&gt; que manejaba todo esto tenía menos de 120 líneas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml (producción, versión simplificada)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/api:${IMAGE_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=${DATABASE_URL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://cache:6379&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/api:${IMAGE_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker.js"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16.3&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7.4-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis_data:/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nada glamoroso. Pero funcionaba. Los deployments eran &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d --no-deps api worker&lt;/code&gt; y listo. Cero downtime si lo hacías bien — rolling update manual, sí, pero en 14 meses solo metí la pata una vez. Un viernes por la tarde, cómo no.&lt;/p&gt;

&lt;p&gt;Lo que sí me costó: cuando empezamos a necesitar más de una instancia del servicio &lt;code&gt;api&lt;/code&gt;, las cosas se pusieron incómodas. Compose no tiene balanceo de carga nativo entre réplicas del mismo servicio de forma que realmente te fíes. Puedes usar &lt;code&gt;--scale api=3&lt;/code&gt; pero entonces tienes que configurar nginx manualmente para distribuir la carga entre los contenedores, y eso se vuelve frágil. Ese fue el primer momento en que empecé a mirar k8s con más interés.&lt;/p&gt;

&lt;p&gt;Dicho esto: si tienes una sola máquina, un equipo pequeño y menos de 50.000 usuarios, Compose en producción no es una locura. Es una decisión pragmática perfectamente válida.&lt;/p&gt;




&lt;h2&gt;
  
  
  El Momento en que Kubernetes Pasa de Solución a Problema
&lt;/h2&gt;

&lt;p&gt;Migré a Kubernetes en octubre del año pasado. Usamos EKS en AWS (en aquel momento con Kubernetes 1.31, aunque ya vamos por 1.33). El razonamiento era sólido: necesitábamos escalar horizontalmente de forma automática, hacer deployments sin downtime con más control, y prepararnos para añadir más servicios sin que la infraestructura se volviera un spaghetti.&lt;/p&gt;

&lt;p&gt;Lo que no calculé bien fue el coste de operación.&lt;/p&gt;

&lt;p&gt;El clúster mínimo viable en EKS nos salía por unos 180–220 dólares al mes solo en nodos (sin contar el control plane, que en AWS son 70 dólares fijos). Añade el Application Load Balancer, los volúmenes EBS, las transferencias de datos, y llegas fácil a 350–400 dólares al mes solo de infraestructura. En el VPS de Hetzner estábamos pagando 39 euros. La diferencia es significativa cuando eres una startup pequeña.&lt;/p&gt;

&lt;p&gt;Pero el coste económico es casi lo de menos. El coste en tiempo de ingeniería fue lo que realmente dolió.&lt;/p&gt;

&lt;p&gt;El primer mes post-migración lo pasé — okay, lo pasamos (somos cuatro, pero el tema infra recayó principalmente en mí) — básicamente apagando fuegos de configuración. El cluster RBAC, los secrets con External Secrets Operator porque los nativos de k8s son demasiado básicos, los PodDisruptionBudgets para que los deployments no bajaran el servicio, las NetworkPolicies, los resource limits que había que afinar porque sin ellos un worker se comía toda la memoria del nodo y desalojaba pods de producción...&lt;/p&gt;

&lt;p&gt;El error que más me dolió, porque era evitable: estuve dos horas debuggeando por qué un pod se reiniciaba cada 15 minutos. El &lt;code&gt;kubectl describe pod&lt;/code&gt; mostraba &lt;code&gt;OOMKilled&lt;/code&gt;. Pensé que era un memory leak. Resultó que el límite de memoria del pod era demasiado bajo (512Mi) para un servicio que necesitaba 700Mi en los picos. Lo habría visto antes si hubiera tenido Prometheus + Grafana bien configurados desde el día uno. No los tenía.&lt;/p&gt;

&lt;p&gt;No digo que Kubernetes sea malo. Digo que tiene una superficie de configuración enorme que hay que gestionar activamente, y para un equipo pequeño eso tiene un coste real que los tutoriales no mencionan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Las Diferencias que Importan en 2026: Networking, Secrets y Rollbacks
&lt;/h2&gt;

&lt;p&gt;Pasado el trauma inicial, hay tres áreas donde la comparación entre Compose y k8s no está tan clara como parece.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Networking.&lt;/strong&gt; En Compose, todos los servicios del mismo fichero comparten una red por defecto y se resuelven por nombre de servicio. Punto. En Kubernetes tienes namespaces, Services de tipo ClusterIP/NodePort/LoadBalancer, Ingress controllers (elige el tuyo: nginx-ingress, Traefik, Cilium Gateway API...), NetworkPolicies opcionales pero recomendadas. Más potencia, más cosas que configurar. Lo que sí te da k8s que Compose no puede igualar: service discovery entre múltiples equipos en el mismo clúster, y routing sofisticado tipo canary releases con Argo Rollouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets.&lt;/strong&gt; Aquí Kubernetes tiene fama de hacer las cosas bien, pero los Secrets nativos son simplemente base64 — no es cifrado. En 2026 ya no tienes excusa para no usar External Secrets Operator con AWS Secrets Manager o HashiCorp Vault, pero eso añade más piezas al puzzle. Con Compose en producción, nosotros pasábamos los secrets como variables de entorno desde un fichero &lt;code&gt;.env&lt;/code&gt; que no subíamos al repositorio. Menos elegante, pero funciona y es comprensible al 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rollbacks.&lt;/strong&gt; Este es donde Kubernetes gana sin discusión. &lt;code&gt;kubectl rollout undo deployment/api&lt;/code&gt; y en 30 segundos estás en la versión anterior. Con Compose, el rollback es &lt;code&gt;docker compose up -d&lt;/code&gt; con el tag de imagen anterior — lo cual funciona, pero requiere que tengas ese tag anotado en algún sitio y lo ejecutes manualmente. El domingo a las 3am esto marca la diferencia.&lt;/p&gt;




&lt;h2&gt;
  
  
  La Sorpresa que No Esperaba: k3s Cambia el Cálculo
&lt;/h2&gt;

&lt;p&gt;Aquí viene la parte que la verdad no había anticipado cuando escribí mi primer draft de este artículo.&lt;/p&gt;

&lt;p&gt;A principios de este año empecé a experimentar con k3s — la distribución ligera de Kubernetes de Rancher — en un VPS de Hetzner de 8 vCPUs y 16 GB de RAM. Lo que encontré fue que la experiencia de operación es considerablemente más cercana a Compose de lo que imaginaba. Un solo binario. Control plane y worker en la misma máquina. Incluye Traefik como ingress por defecto, SQLite en lugar de etcd para clústeres pequeños, y el consumo de recursos en idle es sorprendentemente bajo — alrededor de 500MB de RAM para el control plane completo.&lt;/p&gt;

&lt;p&gt;El coste: el mismo VPS que usábamos con Compose, a 39 euros al mes, corriendo k3s en nodo único. Y ya tienes &lt;code&gt;kubectl rollout undo&lt;/code&gt;, HPA (Horizontal Pod Autoscaler), health checks declarativos, y la posibilidad de añadir nodos workers cuando los necesites.&lt;/p&gt;

&lt;p&gt;No estoy 100% seguro de que esto escale bien más allá de dos o tres nodos workers sin empezar a necesitar etcd en alta disponibilidad — que ya es otra conversación. Pero para ese punto dulce entre "Compose se queda corto" y "EKS es demasiado", k3s en 2026 merece estar en la conversación.&lt;/p&gt;




&lt;h2&gt;
  
  
  El Filtro Real: Las Tres Preguntas que Yo Me Haría
&lt;/h2&gt;

&lt;p&gt;Después de haber migrado proyectos en ambas direcciones, este es el proceso que ahora aplico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Necesitas escalar horizontalmente de forma automática?&lt;/strong&gt;&lt;br&gt;
Si no — si un servidor bien especificado aguanta tu carga con margen, si tus picos son predecibles y manejables manualmente — entonces Compose. Punto. No te vendas la complejidad de k8s como una inversión en el futuro si ese futuro no existe todavía en tus métricas actuales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tienes más de un servicio que necesita escalar de forma independiente?&lt;/strong&gt;&lt;br&gt;
Si la API puede tener diez instancias pero el worker de procesamiento de PDFs solo necesita dos, Kubernetes gestiona esto de forma natural. Con Compose empiezas a hacer malabares con nginx y scripts bash. Si estás en este punto, el salto tiene sentido — pero mira k3s antes de ir directo a EKS o GKE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tu equipo tiene ancho de banda para aprender y mantener esto?&lt;/strong&gt; Esta es la que más gente ignora. Kubernetes no es un deployment de un día — es un sistema que requiere atención continua. Los upgrades de versión (k8s depreca APIs con bastante regularidad, mira lo que pasó con PodSecurityPolicy en 1.25), los certificados que caducan, los nodos que necesitan mantenimiento. Si tu equipo de DevOps eres tú solo, o si el backend es un tercio de lo que tu equipo hace, ese tiempo tiene un coste de oportunidad alto que se subestima mucho.&lt;/p&gt;

&lt;p&gt;Mi recomendación sin rodeos: menos de 50.000 usuarios activos, equipo de menos de seis personas y un solo servidor o VPS — usa Docker Compose en producción. Si ya se te queda pequeño o necesitas multi-nodo de verdad, prueba k3s antes de comprometerte con EKS. Y si tienes un equipo de plataforma dedicado, múltiples servicios con requisitos de escala distintos y presupuesto para la complejidad operativa, entonces Kubernetes managed tiene todo el sentido.&lt;/p&gt;

&lt;p&gt;Nosotros acabamos usando k3s en dos nodos en Hetzner para producción y Docker Compose para todos los entornos de desarrollo y staging. El equipo está contento. Los deployments funcionan. Y yo ya no me despierto pensando en &lt;code&gt;etcd snapshots&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>kubernetes</category>
      <category>dockercompose</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deno 2.0 en Producción 2026: Migración desde Node.js y Qué Cambió Realmente</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 18:22:04 +0000</pubDate>
      <link>https://dev.to/synsun/deno-20-en-produccion-2026-migracion-desde-nodejs-y-que-cambio-realmente-52kf</link>
      <guid>https://dev.to/synsun/deno-20-en-produccion-2026-migracion-desde-nodejs-y-que-cambio-realmente-52kf</guid>
      <description>&lt;p&gt;Empecemos con honestidad: migré a Deno 2.0 porque me lo pidieron en una retro de equipo y yo, con toda la confianza del mundo, dije "dos semanas máximo". Spoiler: fueron cuatro semanas, un incidente en producción a las 11pm de un miércoles, y una cantidad de tabs de GitHub Issues que prefiero no recordar.&lt;/p&gt;

&lt;p&gt;Trabajo en un equipo de seis personas construyendo APIs para una plataforma de analítica de contenido. Tres microservicios en Node.js 22, todos en TypeScript, todos usando ESM, con un montón de dependencias de npm que llevaban años ahí sin que nadie los tocara. El candidato perfecto para la migración, en teoría.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tres Microservicios, Cuatro Semanas, y un Miércoles Muy Complicado
&lt;/h2&gt;

&lt;p&gt;Elegí empezar con el servicio más pequeño: un worker que procesa webhooks entrantes, transforma los payloads y los encola en Redis. Unas 800 líneas de TypeScript, cuatro dependencias directas en npm. Pensé que sería el caso ideal para validar el proceso antes de tocar los servicios más críticos.&lt;/p&gt;

&lt;p&gt;Lo que no calculé fue que "cuatro dependencias directas" significa, con el árbol completo, algo así como 340 paquetes. Y Deno 2.0, aunque mejoró enormemente la compatibilidad con npm respecto a versiones anteriores, sigue teniendo sus opiniones sobre ciertas cosas.&lt;/p&gt;

&lt;p&gt;La migración inicial fue sorprendentemente fluida. Cambias el &lt;code&gt;package.json&lt;/code&gt; por un &lt;code&gt;deno.json&lt;/code&gt;, reemplazas las importaciones de npm con el prefijo &lt;code&gt;npm:&lt;/code&gt;, y listo. En teoría. Esto funciona bien:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Antes (Node.js)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Después (Deno 2.0)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm:ioredis@5.3.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm:zod@3.22.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí empezó lo interesante. Deno 2.0 con &lt;code&gt;"nodeModulesDir": "auto"&lt;/code&gt; resuelve la mayoría de los paquetes sin problema. Pero &lt;code&gt;ioredis&lt;/code&gt; usaba internamente &lt;code&gt;node:tls&lt;/code&gt; con opciones que Deno maneja de forma ligeramente distinta, y el resultado era que la conexión se establecía, enviabas un comando, y a los 90 segundos exactos el socket se cerraba silenciosamente. Sin error. Sin log. Nada.&lt;/p&gt;

&lt;p&gt;Lo encontré porque vi que mis métricas de latencia tenían spikes cada 90 segundos exactos — ese patrón tan regular fue la pista. Abrí el issue #24891 en el repositorio de Deno y resultó que ya estaba reportado desde noviembre de 2024. La solución: pin de &lt;code&gt;ioredis@5.3.3-rc.1&lt;/code&gt; que parcheó el comportamiento, y configurar &lt;code&gt;keepAlive: true&lt;/code&gt; explícitamente en el cliente.&lt;/p&gt;

&lt;p&gt;Esto lo empujé un miércoles a las 7pm. El error volvió a las 11pm porque en staging no se nota — el volumen es demasiado bajo. Me enteré por un alert de Datadog.&lt;/p&gt;

&lt;h2&gt;
  
  
  La Compatibilidad con npm en 2026: Mejor, Pero No Perfecta
&lt;/h2&gt;

&lt;p&gt;Seré directo porque he visto muchos posts que pintan esto de color de rosa.&lt;/p&gt;

&lt;p&gt;Deno 2.x mejoró mucho la compatibilidad con el ecosistema npm. La mayoría de paquetes que no dependen de APIs nativas de Node.js funcionan sin cambios: &lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;date-fns&lt;/code&gt;, &lt;code&gt;lodash&lt;/code&gt;, &lt;code&gt;fastify&lt;/code&gt; (sí, fastify en Deno) — sin problemas. Pero hay categorías donde las cosas se complican.&lt;/p&gt;

&lt;p&gt;Paquetes que usan &lt;code&gt;__dirname&lt;/code&gt; o &lt;code&gt;__filename&lt;/code&gt; son los primeros candidatos a romper. Deno los polyfilla, pero si el paquete hace algo creativo con esas rutas para cargar archivos relativos, el comportamiento puede diferir. Me pasó con un SDK interno que cargaba templates desde el sistema de archivos — tres horas depurando algo que no debería haber tardado más de veinte minutos.&lt;/p&gt;

&lt;p&gt;Después están los paquetes con addons nativos o binarios de C++. &lt;code&gt;sharp&lt;/code&gt; para procesamiento de imágenes funciona vía &lt;code&gt;npm:sharp&lt;/code&gt;, pero el tiempo de instalación en CI subió de 45 segundos a 3 minutos en nuestro caso porque Deno no cachea los binarios compilados igual que npm.&lt;/p&gt;

&lt;p&gt;El que más me sorprendió: algunos paquetes que detectan el entorno hacen &lt;code&gt;typeof process !== 'undefined'&lt;/code&gt; para saber si están en Node — y Deno 2.0 expone un objeto &lt;code&gt;process&lt;/code&gt; para compatibilidad, así que eso está bien. El problema viene cuando el paquete luego consulta &lt;code&gt;process.versions.node&lt;/code&gt; y espera una versión específica. Deno devuelve un valor emulado que no siempre cuadra con las expectativas internas del paquete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Esto puede romper paquetes que verifican la versión de Node&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// En Deno: "22.0.0" (emulado)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deno&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// "2.2.4"&lt;/span&gt;

&lt;span class="c1"&gt;// Para detectar si estás en Deno:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDenoRuntime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSR (el registro de paquetes de Deno) creció bastante en 2025. A principios de 2026 hay paquetes como &lt;code&gt;@std/http&lt;/code&gt;, &lt;code&gt;@std/async&lt;/code&gt;, &lt;code&gt;@std/encoding&lt;/code&gt; que son de primera clase y funcionan perfectamente. Si puedes sustituir dependencias npm por equivalentes de JSR, la experiencia mejora notablemente.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Sistema de Permisos: Pesadilla Operacional o Ventaja Real
&lt;/h2&gt;

&lt;p&gt;Cuando empecé a migrar pensé que el sistema de permisos iba a ser un dolor de cabeza. Que terminaría dando &lt;code&gt;--allow-all&lt;/code&gt; en producción porque quién sabe qué permisos necesita &lt;code&gt;ioredis&lt;/code&gt; internamente, o cualquier otro paquete npm.&lt;/p&gt;

&lt;p&gt;Me equivoqué, y de forma interesante.&lt;/p&gt;

&lt;p&gt;El proceso de descubrir los permisos que necesita tu aplicación es incómodo al principio — ejecutas sin permisos y Deno te dice exactamente qué necesita — pero esa incomodidad te da algo valioso: un mapa de lo que tu aplicación realmente hace. Nuestro worker de webhooks necesitaba exactamente esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// deno.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deno run --allow-net=redis.internal:6379,api.externa.com:443 --allow-env=REDIS_URL,WEBHOOK_SECRET,LOG_LEVEL --allow-read=/etc/ssl/certs src/main.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nodeModulesDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@std/async"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jsr:@std/async@^1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo. Un servicio que en Node.js corría con acceso total al sistema, en Deno corre con permisos explícitos a una IP de Redis, un dominio externo, tres variables de entorno, y los certificados SSL. Si alguien introduce una dependencia que intenta leer &lt;code&gt;/etc/passwd&lt;/code&gt; o hacer una petición a un dominio desconocido, Deno lo bloquea en runtime.&lt;/p&gt;

&lt;p&gt;No sé si esto escala a aplicaciones con 50 dependencias y comportamientos dinámicos complejos — probablemente hay casos donde terminas en &lt;code&gt;--allow-all&lt;/code&gt; de todas formas. Pero para servicios pequeños y bien definidos, cambió cómo pienso sobre la seguridad de lo que pongo en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Tooling Integrado: Lo Que No Calculé
&lt;/h2&gt;

&lt;p&gt;Yo asumía que el formatter, linter y test runner integrados de Deno eran un nice-to-have para proyectos pequeños. Después de cuatro meses, es lo que más valoro del ecosistema — y no lo vi venir.&lt;/p&gt;

&lt;p&gt;No porque &lt;code&gt;deno fmt&lt;/code&gt; sea mejor que Prettier (honestamente son bastante similares en resultado). Sino porque elimina una categoría completa de discusiones en el equipo. Sin archivo &lt;code&gt;.prettierrc&lt;/code&gt;. Sin conflictos de versión entre &lt;code&gt;eslint&lt;/code&gt; y &lt;code&gt;typescript-eslint&lt;/code&gt;. Sin "¿qué configuración de eslint usamos?" El standard es el runtime.&lt;/p&gt;

&lt;p&gt;La primera semana, un compañero que lleva seis años en Node.js me preguntó cómo configurar el linter. Le respondí que no había configuración. Se quedó callado un momento y luego dijo "ah, ¿entonces funciona?" Sí, funciona.&lt;/p&gt;

&lt;p&gt;El test runner también. &lt;code&gt;deno test&lt;/code&gt; soporta cobertura de código nativa, watch mode, y el mismo formato de &lt;code&gt;describe/it&lt;/code&gt; al que estás acostumbrado si vienes de Jest o Vitest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assertRejects&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsr:@std/assert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;processWebhook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./webhook.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;procesa payload válido correctamente&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mi-repo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;job_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rechaza payload sin firma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assertRejects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;processWebhook&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skipSignatureVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin instalar nada. Sin configurar nada. &lt;code&gt;deno test&lt;/code&gt; y ya.&lt;/p&gt;

&lt;p&gt;Deno Deploy merece una mención aparte: lo usé para un cuarto servicio, este sí desde cero, y el workflow de deploy es genuinamente rápido — push al repo, deploy en menos de 30 segundos, edge computing global. Pero tiene sus propias restricciones (algunas APIs de filesystem no están disponibles, el modelo de permisos cambia), así que no es directamente equivalente a correr Deno en tu propio servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuatro Meses Después: El Veredicto
&lt;/h2&gt;

&lt;p&gt;Los tres microservicios están en producción. Funcionan. No extraño Node.js en ninguno de ellos.&lt;/p&gt;

&lt;p&gt;Pero te sería deshonesto si dijera que la migración fue solo positiva. El tiempo real fue el doble de lo estimado. Las incompatibilidades de npm son reales y no siempre están documentadas — a veces simplemente tienes que probar y ver qué explota. El ecosistema de JSR, aunque crece, todavía no tiene la cobertura de npm para casos de uso especializados.&lt;/p&gt;

&lt;p&gt;Lo que sí cambió: el tiempo de setup de proyectos nuevos bajó bastante. El onboarding de un desarrollador nuevo es más simple porque hay menos configuración que explicar. Los containers de Docker son más pequeños porque no llevamos &lt;code&gt;node_modules&lt;/code&gt;. Y el código TypeScript se siente más limpio cuando Deno es tu target, porque usas las APIs web estándar (&lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;) directamente, sin polyfills.&lt;/p&gt;

&lt;p&gt;Mi recomendación concreta: si tienes un servicio Node.js con pocas dependencias npm, bien tipado en TypeScript, y estás dispuesto a invertir una o dos semanas en la migración, hazlo. Si tienes un monolito con 200 dependencias npm, código legacy sin tipos, y deadlines apretados, no lo hagas todavía. No porque Deno sea malo, sino porque la fricción de compatibilidad va a comerte vivo.&lt;/p&gt;

&lt;p&gt;Para proyectos nuevos en 2026, ya no consideraría Node.js como primera opción por defecto. Deno ganó ese puesto en mi stack.&lt;/p&gt;

</description>
      <category>deno</category>
      <category>node</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>LangChain vs LlamaIndex vs Haystack: What Two Weeks in Production Actually Taught Me</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 18:21:34 +0000</pubDate>
      <link>https://dev.to/synsun/langchain-vs-llamaindex-vs-haystack-what-two-weeks-in-production-actually-taught-me-1kl6</link>
      <guid>https://dev.to/synsun/langchain-vs-llamaindex-vs-haystack-what-two-weeks-in-production-actually-taught-me-1kl6</guid>
      <description>&lt;p&gt;My team got handed a RAG project earlier this year — 40,000 documents, mix of PDFs and Confluence exports, users who would notice if answers were wrong. I'd used LangChain for smaller stuff before, but this was the first time I actually ran all three major frameworks against real data, under real pressure, with a client watching the error rates.&lt;/p&gt;

&lt;p&gt;Quick context: four-person eng team, Qdrant running on-prem, Claude as the LLM. The client's tolerance for hallucinated answers was basically zero. Not a toy project.&lt;/p&gt;




&lt;h2&gt;
  
  
  LangChain's Composition Model Is Great Until Something Goes Quietly Wrong
&lt;/h2&gt;

&lt;p&gt;I've been using LangChain off and on since early 2023, and by now — v0.3+, LCEL as the standard — it genuinely is good at what it promises. The expression language makes wiring things together fast and readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatAnthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.output_parsers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrOutputParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.runnables&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RunnablePassthrough&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_qdrant&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QdrantVectorStore&lt;/span&gt;

&lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorstore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mmr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch_k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer using only the context below.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{context}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{question}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# This part is clean. The problem shows up later.
&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;format_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RunnablePassthrough&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ChatAnthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;StrOutputParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the refund policy for enterprise contracts?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code is clean. I actually like it.&lt;/p&gt;

&lt;p&gt;The trouble showed up on day four. A retrieval step was returning empty results for certain query types, but only intermittently — maybe 8% of requests. The chain kept running. Returned a confident, fully hallucinated answer with zero retrieved context, and nothing in the output flagged it. I spent half an afternoon chasing this before realizing LangChain was silently passing an empty string as context to the prompt template.&lt;/p&gt;

&lt;p&gt;You can guard against this. Callbacks exist. LangSmith is genuinely useful for tracing if you're paying for it. But the default behavior when something fails upstream in a chain is to carry on — and for production RAG that's a real problem I hadn't budgeted time to solve. I ended up writing a custom runnable that validates retrieval counts before the context hits the prompt. Not hard, but it's defensive scaffolding you don't anticipate until it bites you.&lt;/p&gt;

&lt;p&gt;The ecosystem advantage is real, though. When I hit a weird edge case with metadata filtering on Qdrant, there was a GitHub issue with a working fix posted five days earlier. That's community size, not luck. If you're integrating anything unusual — a niche vector store, custom document loaders, tool use patterns — LangChain almost certainly has it already.&lt;/p&gt;

&lt;p&gt;LangChain is fast to start with, and the integrations will save you. Just write explicit failure guards around your retrieval steps, because the framework won't.&lt;/p&gt;




&lt;h2&gt;
  
  
  LlamaIndex's Node Model Finally Clicked for Me in Week Two
&lt;/h2&gt;

&lt;p&gt;I'll admit: I bounced off LlamaIndex about eighteen months ago. The "index everything" abstraction felt strange coming from LangChain's chain-centric thinking, and the docs had this habit of showing four different ways to accomplish something without indicating which was current or preferred.&lt;/p&gt;

&lt;p&gt;The v0.12 line is much better. But the real shift was accepting that LlamaIndex thinks in &lt;em&gt;nodes&lt;/em&gt;, not documents — each chunk carries metadata forward through the whole pipeline. Once I stopped fighting that model and started working with it, things that had felt awkward suddenly made sense.&lt;/p&gt;

&lt;p&gt;What genuinely surprised me — stopped me for a moment, honestly — was the &lt;code&gt;SentenceWindowNodeParser&lt;/code&gt;. Found it while looking for something else, almost by accident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core.node_parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceWindowNodeParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.llms.anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.postprocessor.cohere_rerank&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CohereRerank&lt;/span&gt;

&lt;span class="n"&gt;node_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SentenceWindowNodeParser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;window_metadata_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;original_text_metadata_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;original_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node_parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node_parser&lt;/span&gt;

&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_vector_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qdrant_store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_query_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;similarity_top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node_postprocessors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;CohereRerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What changed in the Q3 enterprise pricing tier?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# response.source_nodes — exact retrieval, no hunting around
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SentenceWindowNodeParser stores a small chunk for embedding but retrieves a larger surrounding window at query time. You get the precision of small embeddings with the readability of larger context. I had been implementing something like this manually in LangChain. It worked fine. But this was already built in, already tuned, and it took about three minutes to add to the pipeline.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;response.source_nodes&lt;/code&gt; access was something I also didn't realize I'd care about until the client asked for citations in the UI. In LangChain I was doing gymnastics with callbacks to surface source metadata. Here it's just... on the response object. Saved probably half a day of plumbing work.&lt;/p&gt;

&lt;p&gt;Where it frustrated me: the query engine abstraction goes opaque fast when you need to customize retrieval logic significantly. I spent a day confused about why my custom retriever wasn't applying a metadata filter I'd set — turned out to be a precedence issue in how the query engine assembles its retrieval components internally. Found the answer in a GitHub issue (#14337, two months old), but that hidden behavior cost me real time. When LlamaIndex misbehaves, the error usually isn't the helpful kind.&lt;/p&gt;

&lt;p&gt;That said: if the core of your project is document-heavy retrieval with complex chunking requirements, the built-in primitives here are ahead of the defaults in the other frameworks. You'll feel the difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Haystack Is Boring and I Mean That as High Praise
&lt;/h2&gt;

&lt;p&gt;Before this project, I associated Haystack with enterprise teams who'd chosen it because procurement required something with a company behind it. I was wrong, and I'm correcting that publicly.&lt;/p&gt;

&lt;p&gt;Haystack 2.x restructured around explicit, typed pipelines — every component declared, connections explicit, nothing implicit. Setting it up felt verbose. More boilerplate than either of the others. I figured I'd move through the eval phase quickly and move on.&lt;/p&gt;

&lt;p&gt;Then something broke in all three frameworks on the same day (my fault — I'd changed the Qdrant schema without updating the retriever configs). In LangChain, I got a runtime error deep in the chain with a stack trace pointing at internal LangChain code, not mine. In LlamaIndex, it silently returned empty results — I only caught it because I was checking source_nodes counts. In Haystack: component name, expected input type, received input type, and the line in my pipeline definition where the mismatch was. Fixed in under ten minutes.&lt;/p&gt;

&lt;p&gt;That's not an accident. The Haystack architecture is designed for exactly this — you can inspect the pipeline graph, each component logs its inputs and outputs clearly, and the type system catches mismatches before they become runtime surprises. For teams maintaining this code six months from now, that's worth a lot.&lt;/p&gt;

&lt;p&gt;The deepset team also ships Hayhooks, which wraps your pipeline in a REST API with minimal extra work. For this specific project — where the eventual owners are not Python developers — that mattered during handoff. Showing up with a running API and readable pipeline graphs is a different conversation than handing someone a Python repo and wishing them luck.&lt;/p&gt;

&lt;p&gt;What I didn't love: the community is smaller, and if you need an integration that LangChain has but Haystack doesn't, you're writing a custom component. I needed to pull data from an internal API with non-standard auth, and the LangChain loader already existed. In Haystack I wrote it from scratch — maybe three hours, not catastrophic, but real time.&lt;/p&gt;

&lt;p&gt;For long-lived projects, regulated environments, or teams where the codebase needs to be maintainable by people who didn't build it — Haystack's verbosity pays dividends. For move-fast prototyping, it costs you upfront.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Retrieval Numbers Actually Looked Like
&lt;/h2&gt;

&lt;p&gt;I ran an eval against 200 questions the client's domain expert had written — real questions about real content. Not a rigorous academic study, but real enough to be useful. All three frameworks used identical Qdrant backends.&lt;/p&gt;

&lt;p&gt;Retrieval precision (did the right document appear in the top 5?):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LangChain (recursive text splitter, default settings): 71%&lt;/li&gt;
&lt;li&gt;LlamaIndex (SentenceWindowNodeParser + Cohere rerank): 84%&lt;/li&gt;
&lt;li&gt;Haystack (BM25 hybrid + Cohere rerank): 82%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LangChain's number was dragged down by document categories where the default splitter was cutting badly — a smarter node parser probably closes most of that gap. The retrieval quality difference between frameworks is mostly about defaults, not fundamental architecture. Which means: don't pick a framework because you think it retrieves better. Pick it based on your team's ability to tune the retrieval configuration you actually need.&lt;/p&gt;

&lt;p&gt;The more useful metric was time-to-working-pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LangChain: 3 days (fast start, debugging tax after)&lt;/li&gt;
&lt;li&gt;Haystack: 4 days (slower setup, then very stable)&lt;/li&gt;
&lt;li&gt;LlamaIndex: 4.5 days (steeper start, paid off during tuning)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm genuinely not sure these numbers scale to a larger team — the debugging tax on LangChain probably distributes across more engineers and gets less painful. Your mileage will vary.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Actually Running in Prod
&lt;/h2&gt;

&lt;p&gt;LlamaIndex.&lt;/p&gt;

&lt;p&gt;Not because it's perfect — it isn't — but because for this specific problem (document-heavy RAG, retrieval quality as the primary metric, citation UI as a hard requirement), its built-in primitives were a better fit than what I assembled elsewhere. The node model matches how I was already thinking about the chunking problem. Source attribution is clean enough to build on directly. The retrieval pipeline felt less fragile than my equivalent LangChain setup.&lt;/p&gt;

&lt;p&gt;If this had been a general-purpose AI application — agents, tool use, lots of different LLM calls, light retrieval — I'd probably still be on LangChain. The ecosystem advantage is real for that class of problem.&lt;/p&gt;

&lt;p&gt;And if I were handing this project to a team that didn't build it, or if we had a compliance requirement around logging every retrieval step, I'd have chosen Haystack and not second-guessed it. The verbosity is a feature in those contexts.&lt;/p&gt;

&lt;p&gt;One thing none of these frameworks solved cleanly: eval tooling. I ended up running RAGAS externally regardless of which framework I was using. None of them have a good embedded eval story yet, and that gap keeps showing up in production. That's a separate post — but worth knowing going in.&lt;/p&gt;

&lt;p&gt;Pick the framework that maps to how you think about your problem, get a working pipeline running in a day, and then spend your optimization budget on retrieval strategy and eval. That's where the quality actually comes from.&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>llamaindex</category>
      <category>haystack</category>
      <category>rag</category>
    </item>
    <item>
      <title>Docker Compose vs Kubernetes: What I Actually Learned Running Both in Production</title>
      <dc:creator>Moon Robert</dc:creator>
      <pubDate>Mon, 09 Mar 2026 18:21:04 +0000</pubDate>
      <link>https://dev.to/synsun/docker-compose-vs-kubernetes-what-i-actually-learned-running-both-in-production-18me</link>
      <guid>https://dev.to/synsun/docker-compose-vs-kubernetes-what-i-actually-learned-running-both-in-production-18me</guid>
      <description>&lt;p&gt;Eighteen months ago I inherited a mess. A four-person team had built a reasonably capable ML inference service — three Python microservices, a Redis queue, a Postgres instance, an Nginx reverse proxy — all wired together with a &lt;code&gt;docker-compose.yml&lt;/code&gt; that had clearly been written in a hurry and never revisited. The team lead had left a sticky note in the README that said, verbatim: "we should probably move this to Kubernetes at some point."&lt;/p&gt;

&lt;p&gt;That sticky note started a long argument with myself.&lt;/p&gt;

&lt;p&gt;I ended up running both. Not as an experiment — as an actual business decision I had to defend, twice, to different stakeholders. What follows is what I learned, what I got wrong, and where I landed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Compose in 2026 Is Not What You Used Five Years Ago
&lt;/h2&gt;

&lt;p&gt;The version of Compose I inherited was using some 3.x syntax with deprecated options. First thing I did was migrate to Compose v2.32 (which ships bundled with Docker Desktop and the Docker CLI now — no separate install needed). That alone fixed several subtle networking headaches.&lt;/p&gt;

&lt;p&gt;Thing is, Compose has gotten genuinely good at what it was always meant to do. &lt;code&gt;compose watch&lt;/code&gt; has been stable for a while now, and it changed how I think about local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml — inference service, 2026&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api/src&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/src&lt;/span&gt;
        &lt;span class="c1"&gt;# Rebuild only when dependencies change, not on every save&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rebuild&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api/requirements.txt&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MODEL_PATH=/models/bert-base&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./models:/models:ro&lt;/span&gt;  &lt;span class="c1"&gt;# mount model weights read-only, not baked into image&lt;/span&gt;

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./worker&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync+restart&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./worker/src&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/src&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7.4-alpine&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;sync+restart&lt;/code&gt; action for the worker is something I use constantly — it syncs files then restarts the process without a full image rebuild. Saves probably 40 seconds per iteration cycle when you're deep in debugging.&lt;/p&gt;

&lt;p&gt;For a team our size (four engineers, two of whom are ML researchers who don't want to think about infrastructure), Compose has a near-zero learning curve. I can write a &lt;code&gt;docker-compose.yml&lt;/code&gt;, push it to the repo, and anyone can &lt;code&gt;docker compose up&lt;/code&gt; without reading a manual. That matters more than people admit.&lt;/p&gt;

&lt;p&gt;On a single host — even a beefy one like an EC2 &lt;code&gt;m7i.4xlarge&lt;/code&gt; — Compose handles more than you'd think. I've run services doing 400 req/s on a single host with Compose and it was fine. The constraint is the host, not Compose.&lt;/p&gt;

&lt;p&gt;If your service fits on one host and your team is small, defaulting to Compose isn't laziness — it's a reasonable engineering decision with real payoff in operational simplicity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Kubernetes Actually Earns Back Its Complexity Tax
&lt;/h2&gt;

&lt;p&gt;I did eventually move part of the system to Kubernetes. Not all of it — more on that in a moment — but the inference serving component specifically, because we started getting requests for GPU-backed endpoints and that's where Compose genuinely hits a wall.&lt;/p&gt;

&lt;p&gt;Running GPU workloads across multiple nodes is one of those things K8s is legitimately built for. The NVIDIA GPU Operator on K8s 1.35 has become much more stable than it was back in the 1.28 era — I remember hitting a specific issue where device plugin pods would crash on node drain (somewhere around kubernetes/kubernetes#118506, I'd have to dig). By 1.33 that class of issue was mostly sorted. GPU scheduling on multi-node K8s is now a solved problem in a way it genuinely wasn't two years ago.&lt;/p&gt;

&lt;p&gt;The second payoff: HorizontalPodAutoscaler against custom metrics. We pipe inference latency from Prometheus into KEDA, and the autoscaler responds to queue depth and p95 latency — not just CPU. That's not something you replicate with Compose without building significant custom tooling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hpa.yaml — scales inference pods on queue depth + latency&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keda.sh/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ScaledObject&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inference-scaler&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaleTargetRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inference-deployment&lt;/span&gt;
  &lt;span class="na"&gt;minReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
  &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;serverAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://prometheus:9090&lt;/span&gt;
        &lt;span class="na"&gt;metricName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inference_queue_depth&lt;/span&gt;
        &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15"&lt;/span&gt;  &lt;span class="c1"&gt;# scale up when &amp;gt;15 items queued per pod&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum(inference_queue_depth) / count(kube_pod_info{pod=~"inference.*"})&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;serverAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://prometheus:9090&lt;/span&gt;
        &lt;span class="na"&gt;metricName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inference_p95_latency_ms&lt;/span&gt;
        &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;800"&lt;/span&gt;
        &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;histogram_quantile(0.95, rate(inference_duration_bucket[2m])) * &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolling deployments are the other thing worth mentioning. With Compose, &lt;code&gt;docker compose up --force-recreate&lt;/code&gt; on a single host means downtime — or you're writing your own health-check loop. K8s rolling updates with a proper &lt;code&gt;readinessProbe&lt;/code&gt; mean zero-downtime deploys without having to think about it. I pushed a model update on a Friday afternoon once (yes, I know) and the rollout was fine because the cluster waited for new pods to be healthy before draining the old ones. I would not have taken that risk with Compose on a single host.&lt;/p&gt;

&lt;p&gt;That said — and I want to be direct about this — the K8s cluster costs us roughly $340/month more than a comparable Compose deployment on a single large instance would. That's real money for a side project or an early-stage product. The break-even only works if you're at a scale where the autoscaling savings outweigh the base cluster cost, or if you genuinely need multi-node availability.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ML Workload Angle I Didn't Anticipate
&lt;/h2&gt;

&lt;p&gt;I thought I'd have a clear answer here. I didn't.&lt;/p&gt;

&lt;p&gt;I assumed moving ML inference to K8s would also mean moving training jobs there. Same cluster, same GPU nodes, everything in one place — seemed logical. What I actually found was that training jobs are weird. They're batch, they're stateful in an awkward way, they need specific environment setup that changes frequently, and the feedback loop when something goes wrong is slow.&lt;/p&gt;

&lt;p&gt;I ran training jobs as K8s Jobs with &lt;code&gt;ttlSecondsAfterFinished&lt;/code&gt; for a few months. Fine in theory. In practice, every time an ML researcher wanted to tweak the data pipeline or swap a tokenizer, they were waiting on me to update a ConfigMap or rebuild an image. I had become a gatekeeper for changes that had nothing to do with infrastructure — which is a bad sign.&lt;/p&gt;

&lt;p&gt;So I moved training back to Compose — on a dedicated GPU box, not the K8s cluster. Training runs as &lt;code&gt;docker compose -f compose.train.yml up&lt;/code&gt; with the model checkpoint directory mounted as a volume. Researchers can modify it directly. Inference serving stays on K8s where the availability and scaling story matters.&lt;/p&gt;

&lt;p&gt;I genuinely didn't see that split coming. I thought "K8s for ML" was the obvious move. The reality: K8s is great for serving (stateless, latency-sensitive, scaling matters) and overkill for training (stateful, batch, where iteration speed matters more than orchestration).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Signals I Now Actually Use to Decide
&lt;/h2&gt;

&lt;p&gt;After 18 months of this, the heuristic I've landed on is less about features and more about team and workload shape.&lt;/p&gt;

&lt;p&gt;Compose is the right call when your service runs on one host without strain, your team has fewer than six or seven engineers touching infrastructure, and you're iterating fast enough that deployment simplicity directly affects development speed. Also — and I feel strongly about this — if the people running the service are primarily not infrastructure engineers, Compose's operational model is far more forgiving. A &lt;code&gt;docker compose logs -f worker&lt;/code&gt; is something anyone can run. A &lt;code&gt;kubectl logs -n production -l app=worker --since=1h&lt;/code&gt; is a command you need to look up, at least at first.&lt;/p&gt;

&lt;p&gt;Kubernetes makes sense when you need to schedule across multiple nodes (GPUs, memory isolation, availability zones), when you have autoscaling requirements that respond to custom signals, when your team has dedicated platform or SRE capacity to own the cluster, or when your availability requirements are strict enough that single-host failure isn't acceptable.&lt;/p&gt;

&lt;p&gt;One thing I want to push back on: the idea that Kubernetes is automatically "more production-ready." I've seen Compose deployments that were stable and well-monitored, and K8s clusters that were a disaster of misconfigured RBAC, stale CRDs, and nobody who actually understood the control plane. The tool doesn't make you production-ready. The operational discipline does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Actually Tell You to Do
&lt;/h2&gt;

&lt;p&gt;Start with Compose. Not because K8s is bad — it isn't — but because you'll hit the limits of Compose in very specific, recognizable ways. You'll know when you need multi-node scheduling because you'll be staring at a GPU allocation problem that Compose can't solve. You'll know when you need cluster-level autoscaling because you'll have just manually scaled your single host twice in a week and you're annoyed about it.&lt;/p&gt;

&lt;p&gt;When you hit those specific walls, migrate that specific component. Not everything at once.&lt;/p&gt;

&lt;p&gt;The worst outcome I've seen is teams migrating entirely to K8s before they have the scale to justify it, then spending their first six months of product development fighting cluster configuration instead of shipping features. Kubernetes is powerful and I use it every day, but complexity has a real cost and that cost lands on your team's velocity.&lt;/p&gt;

&lt;p&gt;Anyway. The sticky note in the README — I never did "move everything to Kubernetes." I moved the inference serving layer and kept the rest on Compose. The system is faster, more reliable, and cheaper to operate than a full K8s migration would have been. Sometimes the boring answer is the right one.&lt;/p&gt;

</description>
      <category>dockercompose</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>containers</category>
    </item>
  </channel>
</rss>
