Node.jsDevOps

Guía de Migración a Node.js 26 LTS: Pasá de 22 o 24 sin Sorpresas

3 de noviembre de 2026
14 min de lectura
Primer plano de servidores negros en un rack, con LEDs de estado verdes y una manija de acero bajo una luz azul fría
Compartir:

El 28 de octubre de 2026, Node.js 26.13.0 ‘Lithium’ pasó la línea 26.x a Active LTS, con soporte hasta fines de abril de 2029. La release solo cambia metadatos como process.release: el código es el mismo de 26.12.1, con el fix de Node2Shell incluido. Lo que importa es todo lo que se mueve detrás de la etiqueta LTS. Ese mismo día el tag oficial node:lts de Docker saltó de 24 a 26, el lts/* de setup-node empezó a resolver a 26.13.0, y el 29 de octubre Vercel hizo de 26 el default para proyectos nuevos.

Esta guía lleva un proyecto de Node 22 o 24 (o del 20, que ya no tiene soporte) a 26 en cinco pasos: inventariar cada versión fijada, actualizar, arreglar lo que se rompe, adoptar lo que vale la pena y planificar el nuevo calendario anual. El 3 de noviembre bajé el build oficial 26.13.0 para Windows, verifiqué su SHA-256 y probé cada snippet de JavaScript de este post en ese build y en mi 22.22.0 local. Los pasos de CI, Docker y plataformas salen de la documentación de cada proyecto ese día; no corrí ningún workflow ni ningún build de Docker.

1. El Problema: Tu Versión de Node Vive en una Docena de Lugares

Las release notes de 26.13.0 tienen cuatro oraciones, y ese es el punto: pasar a LTS es una promesa de soporte, no un paquete de features. Los cambios que rompen llegaron el 5 de mayo con Node.js 26.0.0 (V8 14.6, Temporal activado por defecto, Undici 8 y una tanda de eliminaciones), más todo lo que eliminó Node 25 si venís de 24.

Lo difícil de actualizar Node casi nunca es el código. Es la versión fijada en .nvmrc, en package.json, en tres Dockerfiles, en una matriz de CI, en un compose, en el string de runtime de una Lambda y en una opción del dashboard que nadie recuerda. Si se te escapa una, te queda lo peor de los dos mundos: los tests pasan en 26 mientras producción sigue en 22, o al revés.

🟢

Active LTS hasta

20 oct 2027

Después Maintenance LTS; fin de vida el 30 abr 2029

⚙️

Motor

V8 14.6

Más Undici 8.12.2 y npm 11.20.0 en 26.13.0

🧩

ABI nativa

147

NODE_MODULE_VERSION: 127 en 22, 137 en 24. Recompilá los addons

🪦

Node.js 20

EOL

Desde el 30 abr 2026: sin más fixes, Node2Shell incluido

Las versiones flotantes ya se movieron sin vos

Si usás alguna de estas, parte de tu infraestructura puede estar corriendo 26 ya: node:lts, node:lts-slim y node:latest de Docker (26.13.0 desde el 28 de octubre, y los tags default y slim además cambiaron la base de Debian 12 bookworm a Debian 13 trixie); lts/* en setup-node o nvm; y un rango de engines en Vercel como >=20, que buildea con 26 en el próximo deploy. Decidí si eso es lo que querés antes de que tu próximo deploy lo decida por vos.

¿Seguís en Node 20? Esta parte es urgente

Node 20 llegó al fin de su vida el 30 de abril de 2026. Nunca recibió el fix de CVE-2026-111497, el bug de Brotli de mi guía de parche de Node2Shell; Vercel dejó de aceptar deploys nuevos en Node 20 el 1 de octubre, y AWS deprecó su runtime nodejs20.x de Lambda el 30 de abril. Salteate 22, al que le quedan unos seis meses, y andá directo a 26, o a 24 si alguna dependencia te frena.

2. Los Conceptos Mínimos

Cinco ideas explican cada paso que sigue.

📅

Active vs Maintenance LTS

Active LTS recibe backports auditados, features nuevas incluidas; Maintenance solo recibe fixes críticos y actualizaciones de seguridad. Con el modelo viejo, cada línea par pasaba 12 meses en Active y 18 en Maintenance. Según nodejs.org, producción solo debería usar una de las dos.

🏷️

Qué es realmente 26.13.0

Una release semver-minor sin cambios de código respecto de 26.12.1. La diferencia visible es process.release.lts, que dice ‘Lithium’ en el build que descargué: la forma más rápida de distinguir un build LTS de uno Current.

🧱

Cruzás cada major intermedio

Desde 24 también recibís los semver-major de Node 25: SlowBuffer, fs.F_OK y compañía, la opción recursive de rmdir y assert.CallTracker ya no existen, y Corepack dejó de venir incluido. Desde 22, sumá la lista de 24 de la guía oficial de v22 a v24; por ejemplo, el nivel de seguridad 2 de OpenSSL rechaza claves RSA, DSA y DH de menos de 2048 bits.

⚠️

Tres niveles de deprecación

Solo en documentación (sin warning), en runtime (un warning con código DEP) y End-of-Life (desaparece). Node 26 pasó cuatro deprecaciones a End-of-Life y sumó cuatro en runtime, module.register() entre ellas. --throw-deprecation convierte los warnings de runtime en crashes, que es lo que querés en CI.

🔌

La ABI nativa

Los addons compilados dependen de NODE_MODULE_VERSION, 147 en Node 26, así que todo lo compilado para 22 o 24 hay que recompilarlo. Los paquetes hechos sobre Node-API sobreviven a un major sin recompilar, porque esa ABI es estable.

Dónde está cada línea de releases (3 nov 2026)

LíneaNombreEstadoÚltimaMaintenance desdeFin de vida
20IronFin de vida20.20.2 (24 mar 2026)22 oct 202430 abr 2026
22JodMaintenance LTS22.23.421 oct 202530 abr 2027
24KryptonMaintenance LTS24.22.220 oct 202630 abr 2028
26LithiumActive LTS26.13.020 oct 202730 abr 2029
27—Alpha27.0.0-alpha.0.0.0 (29 oct)Ninguna: solo Active LTS30 abr 2030

Fechas del calendario de nodejs/Release y su schedule.json; últimas versiones según el índice de dist de nodejs.org. 24.22.2 es una actualización normal con backports de V8. Cualquier versión igual o superior a 22.23.4, 24.22.1 o 26.12.1 trae el fix de Node2Shell.

3. Paso 1: Inventariá Cada Versión Fijada

Empezá por una lista, no por la actualización. Este script lee solo los archivos que versiona git e imprime cada versión de Node que encuentra: archivos de versión, campos de package.json, Dockerfiles y archivos compose, configs de CI, el NODE_VERSION de Netlify y los strings de runtime de serverless. Corre con el Node que tenés hoy y no cambia nada.

node-pins.mjs (Node 18+, solo lectura)

// node-pins.mjs: lista cada lugar donde un repo fija una versión de Node.js (solo lectura).
// Correlo desde la raíz del repo con el Node.js que tenés hoy (18+): node node-pins.mjs
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { basename } from "node:path";

const files = execFileSync("git", ["ls-files"], { encoding: "utf8" }).split("\n").filter(Boolean);
const hits = [];
const add = (file, where, text) => hits.push(`${file}:${where}  ${text.trim()}`);

// [qué archivos, qué líneas dentro de ellos]
const RULES = [
  [/(^|\/)(\.tool-versions|\.?mise\.toml)$/, /^\s*node(js)?\b/],
  [/(^|\/)(Dockerfile[^/]*|[^/]*\.dockerfile|(docker-)?compose[^/]*\.ya?ml)$/i, /\bnode:[\w.-]+/],
  [/\.(ya?ml|toml|json|tf)$/, /node-version|NODE_VERSION|nodejs\d+\.x|\bnode:\s*\[|image:\s*["']?node:/],
];

for (const file of files) {
  const name = basename(file);
  if (file.includes("node_modules/") || name === "package-lock.json") continue;
  let text;
  try { text = readFileSync(file, "utf8"); } catch { continue; }

  if (name === ".nvmrc" || name === ".node-version") { add(file, 1, text.split(/\r?\n/)[0]); continue; }

  if (name === "package.json") {
    let pkg;
    try { pkg = JSON.parse(text); } catch { continue; }
    if (pkg.engines?.node) add(file, "engines.node", pkg.engines.node);
    if (pkg.volta?.node) add(file, "volta.node", pkg.volta.node);
    const rt = [pkg.devEngines?.runtime].flat().find((r) => r?.name === "node");
    if (rt) add(file, "devEngines.runtime", rt.version ?? "(any)");
    continue;
  }

  const rule = RULES.find(([fileRe]) => fileRe.test(file));
  if (!rule) continue;
  text.split(/\r?\n/).forEach((line, i) => rule[1].test(line) && add(file, i + 1, line));
}

console.log(hits.length ? hits.join("\n") : "No Node.js pins found in tracked files.");

Su salida en un repo de prueba que armé con versiones fijadas (recortada)

$ node node-pins.mjs
.github/workflows/ci.yml:5  node: [20, 22, 24]
.github/workflows/ci.yml:9  node-version: ${{ matrix.node }}
.gitlab-ci.yml:1  image: node:22
.nvmrc:1  24
Dockerfile:1  FROM node:lts-slim AS build
Dockerfile:3  FROM node:24-alpine
api/Dockerfile.dev:1  FROM --platform=linux/amd64 node:20.20.2-bookworm
api/package.json:engines.node  22.x
compose.yaml:3  image: node:24-bookworm-slim
mise.toml:2  node = "24"
netlify.toml:2  NODE_VERSION = "24"
package.json:engines.node  >=20
package.json:volta.node  24.22.1
serverless.yml:3  runtime: nodejs20.x
web/.node-version:1  v22.20.0

Lo que un script no puede ver

Las opciones de los dashboards viven fuera de git: la Node.js Version de Vercel, el Dependency management de Netlify, las funciones Lambda creadas a mano, los runners de CI con un Node del sistema. En el repo de este sitio el script no encontró ninguna versión fijada, así que decide la configuración del proyecto en Vercel. Sumá esas a mano y commiteá una única fuente de verdad: .nvmrc (lo leen nvm, fnm, Netlify y el node-version-file de setup-node) más engines.node para Vercel.

4. Paso 2: Actualizá Computadoras, CI e Imágenes

Actualizá en este orden: tu computadora, después CI con 26 corriendo al lado de lo que hoy corre producción, y al final imágenes y plataformas. Cada bloque de abajo cambia una sola capa. Para una app, engines.node en 26.x es la versión que lee Vercel; una librería debería mantener un rango que coincida con lo que testea.

Local: elegí tu gestor de versiones y después fijá el proyecto

# nvm (macOS, Linux, WSL): instalá 26 y copiá tus paquetes globales de npm desde 24
nvm install --reinstall-packages-from=24 26
nvm alias default 26

# fnm (también en Windows)
fnm install 26
fnm default 26

# mise: escribe node = "26" en el mise.toml del proyecto
mise use node@26

# Volta: escribe volta.node en package.json (su README dice que ya no se mantiene)
volta pin node@26

# Después fijá el proyecto para el resto de las herramientas y revisá qué quedó
echo "26" > .nvmrc
npm pkg set engines.node="26.x"
npm install -g corepack            # solo si usás pnpm o Yarn 4: Node 25+ lo dejó de incluir
node -p "process.version + ' ' + process.release.lts"   # v26.13.0 Lithium

CI: sumá 26 a la matriz antes de cambiar producción

# .github/workflows/node-26.yml: la misma suite en cada línea con soporte
name: node-26
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false        # ver el resultado de cada línea, no solo la primera falla
      matrix:
        node: [22, 24, 26]    # 20 ya no tiene soporte: sacalo
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test
        env:
          # Solo en 26: cada deprecación en runtime pasa a ser un crash
          NODE_OPTIONS: ${{ matrix.node == 26 && '--throw-deprecation' || '' }}

Docker: cambiá una variable a la vez

# Antes: FROM node:24-bookworm-slim, o node:lts-slim (26 sobre Debian trixie desde el 28 oct)
# 1. Cambiá una cosa a la vez: misma versión de Debian, nuevo major de Node
FROM node:26.13.0-bookworm-slim
# 2. En otro PR, mové también el sistema operativo:  FROM node:26.13.0-trixie-slim
#    Fijá también el digest: docker buildx imagetools inspect node:26.13.0-bookworm-slim

# Las imágenes de Node 26 ya no traen Yarn v1. Elegí UNA de estas si necesitás un gestor:
RUN npm install --global yarn@1.22.22
# RUN npm install --global corepack@0.36.0 && corepack enable   # pnpm / Yarn 4

# Que el build falle temprano si la base no es la que pensás
RUN node -p "process.version + ' ' + process.release.lts"

Plataformas, revisadas el 3 nov 2026

PlataformaEstado de Node 26Cómo se eligeOjo con
VercelGA para builds y Functions desde el 29 oct; default para proyectos nuevosengines.node “26.x”, o Settings → Build and Deployment → Node.js Version (gana engines)Los rangos como >=20 pasan a 26 en el próximo deploy; 22.x y 24.x siguen disponibles
NetlifyLos builds aceptan cualquier versión que instale nvm; la imagen de build trae 24 por defecto.nvmrc, después .node-version, después NODE_VERSION, después la UILas Functions siguen la versión del build salvo que definas AWS_LAMBDA_JS_RUNTIME; las páginas que leí no mencionan 26
AWS Lambdanodejs26.x en preview pública; disponibilidad general prevista para noviembre de 2026El identificador de runtime, nodejs26.xLos runtimes en preview no tienen el SLA de Lambda: por ahora dejá producción en nodejs24.x
Docker Hub (node oficial)node:26, node:lts y node:latest apuntan a 26.13.0 desde el 28 octUn tag explícito como node:26.13.0-bookworm-slim, idealmente con su digestLos tags default y slim son Debian trixie; Alpine solo para amd64 y arm64; sin Yarn v1
GitHub Actionssetup-node@v7 resuelve 26 y lts/* a 26.13.0node-version, o node-version-file (.nvmrc, .node-version, package.json, mise.toml)lts/* se movió solo el 28 oct

Corepack y Yarn v1 ya no están

Node dejó de incluir Corepack en 25.0.0, así que una computadora o un runner que salta de 22 o 24 a 26 pierde el comando corepack, y con él los shims de pnpm y Yarn. Instalalo desde npm (npm install -g corepack), como dice el README de Corepack. Aparte, las imágenes oficiales ya no traen Yarn v1 desde Node 26: comparé los Dockerfiles de 24 y 26 en nodejs/docker-node, y solo el de 24 instala Yarn 1.22.22.

5. Paso 3: Encontrá lo que se Rompe y Arreglalo

Dos pasadas atrapan casi todo: una búsqueda estática de APIs que ya no existen y después tu suite de tests en 26 con las deprecaciones convertidas en errores. El changelog de 26.0.0 y la página de deprecaciones tienen las listas completas; la tabla se queda con lo que probablemente toque una app o una librería.

Pasada estática: buscá en tu código y en tus dependencias

# 1. APIs eliminadas en 25 y 26 (git grep solo busca en archivos versionados)
git grep -nE "_stream_(readable|writable|duplex|transform|passthrough|wrap)|\.writeHeader\(|\bSlowBuffer\b|fs\.[FRWX]_OK\b|rmdir(Sync)?\(.*recursive|experimental-transform-types"

# 2. Deprecadas en runtime en 26: un warning hoy, un error en un major futuro
git grep -nE "module\.register\(|\{[^}]*\bregister\b[^}]*\} from ['\"](node:)?module|Duplex\.toWeb\("

# 3. El cambio de streams: revisá cada read() sin argumentos sobre un Readable
git grep -nE "\.read\(\)" -- "*.js" "*.mjs" "*.cjs" "*.ts"

# 4. Dependencias: una rotura conocida y addons nativos para recompilar en 26
npm ls yargs                     # necesita 16.2.2+, 17.7.3+ o 18
find node_modules -name "*.node" -not -path "*/test/*"

Cambios que rompen y que conviene revisar camino a 26

CambioDesdeQué se rompeSolución
Se eliminaron _stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough y _stream_wrap (DEP0193, DEP0125)26.0.0require() tira MODULE_NOT_FOUND, o ERR_UNKNOWN_BUILTIN_MODULE con el prefijo node:Importá Readable, Writable, Duplex, Transform o PassThrough desde node:stream
Se eliminó res.writeHeader() (DEP0063)26.0.0TypeError: ya no es una funciónres.writeHead(), con los mismos argumentos
read() de un Readable devuelve un buffer por vez26.0.0El código que llama a read() una vez por evento ‘readable’ recibe un fragmento y el stream puede trabarse; for await entrega un chunk por push()Vaciá el buffer en un loop, llamá a read(readable.readableLength) o juntá todo con node:stream/consumers
Se eliminó --experimental-transform-types26.0.0“bad option” al arrancar; los enums y namespaces fallan con ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXEscribí TypeScript borrable (erasableSyntaxOnly de tsc marca el resto) o mantené un loader o un paso de build
Tags GCM cortos sin authTagLength (DEP0182)26.0.0decipher.setAuthTag() rechaza tags AES-GCM de menos de 16 bytesPasá authTagLength a crypto.createDecipheriv()
Los archivos sin extensión en paquetes “type”: “module” cargan como ESM26.0.0ReferenceError: require is not defined in ES module scope, por ejemplo con yargs 16.2.0 y 17.7.2Actualizá la dependencia: yargs 16.2.2, 17.7.3 o 18
Se eliminaron SlowBuffer, fs.F_OK/R_OK/W_OK/X_OK, la opción recursive de rmdir y assert.CallTracker25.0.0Valores undefined, o ERR_INVALID_ARG_VALUE en rmdirSync()Buffer.allocUnsafeSlow(), fs.constants.F_OK, fs.rm(path, { recursive: true }); codemods más abajo
module.register() (DEP0205), CryptoKey en node:crypto (DEP0203, DEP0204), type en Duplex.toWeb() (DEP0201)26.0.0, en runtimeWarnings hoy; la documentación dice que van a tirar error o desaparecer en una versión futuramodule.registerHooks(), KeyObject.from() para claves extraíbles o Web Crypto, readableType

El cambio de streams, reproducido

Metí tres buffers en un Readable y lo leí como lo hacen muchos parsers escritos a mano: un read() por evento ‘readable’. En 22.22.0 esa lectura devolvió los nueve bytes y el stream terminó. En 26.13.0 devolvió los tres primeros, dejó seis en el buffer y ‘end’ nunca se disparó. for await pasó de un único chunk concatenado a tres, mientras que los eventos ‘data’ fueron idénticos en las dos versiones. El cambio viene del PR #60441, que evita concatenar buffers en cada lectura.

Antes y después (los tres fixes probados en 26.13.0)

import { text } from "node:stream/consumers";

// Antes: un read() por evento 'readable'. En 26 recibe UN buffer y puede quedar colgado
stream.on("readable", () => {
  const chunk = stream.read();
  if (chunk) handle(chunk);
});

// Fix 1: vaciá el buffer en cada evento (el patrón que usa la documentación de streams)
stream.on("readable", () => {
  let chunk;
  while ((chunk = stream.read()) !== null) handle(chunk);
});

// Fix 2: pedí todo lo que hay en el buffer, como sugiere el PR para el comportamiento viejo
const all = stream.read(stream.readableLength);

// Fix 3: for await ahora entrega un chunk por push(); juntalo si necesitás el body completo
const body = await text(stream);   // también buffer(), json(), arrayBuffer()

Pasada dinámica: tests en 26 con las deprecaciones como errores

Con NODE_OPTIONS=--throw-deprecation, mi test de module.register() imprimió su salida y después salió con código 1 por DEP0205, así que un paso de CI falla en vez de esconder un warning en el log. En local, --trace-deprecation muestra el stack de quien llama y --pending-deprecation muestra los warnings que Node deja apagados por defecto. El tsx actual (4.23.15) no disparó DEP0205 cuando corrí un archivo .ts con él y ese flag.

Codemods oficiales: sirven para lo que eliminó 25, no para lo de 26

El repo nodejs/userland-migrations publica codemods en el registry de Codemod. Lo cloné el 3 de noviembre: hay una meta-receta v22-to-v24 y recetas sueltas para lo que eliminó 25, pero todavía nada para writeHeader, los módulos _stream_* o module.register(), y ninguna receta v24-to-v26. nodejs.org tampoco tiene una guía de migración de 24 a 26. No los corrí; reescriben archivos, así que hacé commit antes.

Recetas que aplican camino a 26

# Codemods oficiales de Node.js (nodejs/userland-migrations). Hacé commit antes: reescriben archivos.
npx codemod @nodejs/v22-to-v24                                # si venís de 22
npx codemod @nodejs/slow-buffer-to-buffer-alloc-unsafe-slow   # DEP0030, eliminada en 25
npx codemod @nodejs/fs-access-mode-constants                  # DEP0176, eliminada en 25
npx codemod @nodejs/rmdir                                     # DEP0147, eliminada en 25

6. Paso 4: Adoptá lo que Vale la Pena

Nada de esto es obligatorio para migrar, pero hay tres cambios que afectan el código que vas a escribir la semana que viene.

Temporal sin flag y sin polyfill (temporal.mjs)

// temporal.mjs: node temporal.mjs en Node.js 26 (sin flag, sin polyfill)
if (typeof Temporal === "undefined") {
  throw new Error(`Temporal is not available on ${process.version}: use Node.js 26+`);
}

// 1. Una fecha de calendario: sin hora, sin zona horaria, sin errores de un día a medianoche UTC
const due = Temporal.PlainDate.from("2027-04-30");            // fin de vida de Node.js 22
const today = Temporal.Now.plainDateISO("UTC");
console.log("days left on Node 22:", today.until(due, { largestUnit: "day" }).days);
// 178 cuando lo corrí el 3 nov 2026

// 2. La aritmética de meses se ajusta en vez de desbordarse a marzo como Date
console.log(Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString());
// 2026-02-28

// 3. Una reunión en una zona horaria, mostrada en otra
const call = Temporal.ZonedDateTime.from("2026-11-10T09:00[America/Montevideo]");
console.log(call.withTimeZone("Europe/Madrid").toString());
// 2026-11-10T13:00:00+01:00[Europe/Madrid]

// 4. Respeta el horario de verano: +1 día mantiene la hora de reloj, +24 horas no
const beforeDst = Temporal.ZonedDateTime.from("2027-03-27T12:00[Europe/Madrid]");
console.log(beforeDst.add({ days: 1 }).toString());    // 2027-03-28T12:00:00+02:00[Europe/Madrid]
console.log(beforeDst.add({ hours: 24 }).toString());  // 2027-03-28T13:00:00+02:00[Europe/Madrid]

// 5. Interop: entra un Date legado, sale Temporal, y vuelta
const instant = new Date("2026-10-28T16:57:41Z").toTemporalInstant();
console.log(instant.toZonedDateTimeISO("UTC").toPlainDate().toString()); // 2026-10-28
console.log(new Date(instant.epochMilliseconds).toISOString());          // 2026-10-28T16:57:41.000Z

Temporal reemplaza las peores mañas de Date con tipos explícitos: PlainDate para fechas de calendario, ZonedDateTime para un momento en una zona horaria con nombre, Instant para un punto en el tiempo, Duration para intervalos. Los comentarios muestran lo que imprimió 26.13.0 el 3 de noviembre; en 22.22.0 la guarda tira error, porque Temporal no viene activado por defecto antes de 26. El código que todavía corre en 22 o 24 necesita un polyfill o esa guarda.

V8 14.6: upsert e iterator sequencing

// Upsert: se terminó el "if (!map.has(k)) map.set(k, [])"
const byStatus = new Map();
for (const [status, url] of [[200, "/a"], [404, "/b"], [200, "/c"]]) {
  byStatus.getOrInsert(status, []).push(url);
}
console.log(byStatus); // Map(2) { 200 => [ '/a', '/c' ], 404 => [ '/b' ] }

// getOrInsertComputed solo ejecuta el callback cuando la clave no está
const cache = new Map();
const slow = (k) => { console.log("computing", k); return k.length; };
cache.getOrInsertComputed("lithium", slow); // imprime "computing lithium"
cache.getOrInsertComputed("lithium", slow); // en caché: no imprime nada
console.log(cache.get("lithium"));          // 7

// Iterator sequencing: encadená iterables de forma lazy, sin volcarlos antes a arrays
const lines = Iterator.concat(["# header"], new Set(["a", "b"]), ["# footer"]);
console.log([...lines]); // [ '# header', 'a', 'b', '# footer' ]

fetch() ahora habla HTTP/2

Undici 8 ofrece h2 durante el handshake TLS, y el fetch() de Node 26 lo usa cada vez que el servidor acepta: contra el mismo host HTTPS, 26.13.0 negoció h2 y 22.22.0 http/1.1. El anuncio de Vercel lo vende como conexiones compartidas para funciones que llaman a APIs o bases de datos. Si algún upstream se porta mal con HTTP/2, la llave es allowH2: false, documentada en la guía de v7 a v8 de Undici. Comprobé que configurarlo en un Agent global de undici 8 devuelve el propio fetch() de Node a http/1.1.

Revisá el protocolo y desactivalo solo si hace falta

// check-h2.mjs: ¿qué protocolo negocia fetch()?
import dc from "node:diagnostics_channel";
dc.subscribe("undici:client:connected", ({ socket }) => console.log(socket.alpnProtocol));
await fetch("https://www.cloudflare.com/", { method: "HEAD" });
// h2 en 26.13.0 (undici 8.12.2), http/1.1 en 22.22.0 (undici 6.23.0)

// force-h1.mjs: volvé a HTTP/1.1 en todos los fetch() del proceso (npm i undici@8)
import { Agent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new Agent({ allowH2: false }));
// En Vercel Functions: en cambio, configurá VERCEL_FETCH_DISABLE_H2=1 y volvé a desplegar

7. Paso 5: Planificá un Major por Año

Node 26 es la última línea del viejo modelo par/impar. Desde 27, según el anuncio del nuevo calendario, cada octubre se abre una línea que pasa seis meses en Alpha (con cambios semver-major permitidos), saca la x.0.0 en abril y se vuelve LTS en octubre por 30 meses: 36 meses en total. Todas las líneas pasan a LTS y ya no hay fase de Maintenance. Las razones del equipo de Node: las versiones impares casi no se adoptaban, y cuatro o cinco líneas activas eran demasiado para que los voluntarios las mantuvieran parcheadas.

LíneaAlphaCurrent (x.0.0)LTSFin de vida
26 (modelo viejo)—5 may 202628 oct 202630 abr 2029
27oct 202622 abr 202720 oct 202730 abr 2030
28oct 2027abr 2028oct 2028abr 2031

Cómo planificaría las actualizaciones de acá en adelante

  • Cada noviembre: la línea más nueva acaba de pasar a LTS: mové producción a ella, o al menos sumala a CI.
  • Cada abril: la línea LTS más vieja llega al fin de su vida (22 el 30 de abril de 2027, 24 el 30 de abril de 2028). Nada debería seguir corriendo ahí.
  • Apps: saltar cada dos años todavía funciona (26 y 28 se superponen seis meses), pero los saltos anuales son más chicos.
  • Librerías: testeá la Alpha en CI como falla permitida. La primera Alpha de 27 ya trae V8 15.6 y OpenSSL 4.1.0, según el índice de dist de nodejs.org: un año de aviso para el código de TLS y crypto.
  • Versiones fijadas: versiones exactas donde desplegás (tag más digest, engines.node 26.x); alias flotantes como lts/* solo donde una sorpresa sale barata.

Alpha no es para producción

Según el README de Release, las Alphas se firman, se taguean y se prueban con CITGM, pero su API y su ABI pueden cambiar entre releases; son para autores de librerías y pipelines de CI. La documentación que leí de las versiones publicadas de setup-node y nvm todavía no cubre Alphas, así que por ahora es una descarga manual. Y todas estas fechas están “sujetas a cambios”, en palabras del equipo de Release.

8. Errores Comunes (y la Solución de Cada Uno)

Las fallas que esperaría en la primera semana, cada una respaldada por un changelog, un Dockerfile o un test que corrí.

yarn: not found en un build de Docker

Síntoma: RUN yarn install falla justo después de cambiar FROM node:24 por FROM node:26, o después de que node:lts se movió el 28 de octubre.

Solución: Las imágenes de Node 26 no traen Yarn v1. Instalalo de forma explícita (npm install --global yarn@1.22.22) o pasate a Corepack con un campo packageManager.

corepack: command not found

Síntoma: pnpm o Yarn 4 dejan de funcionar en una computadora o un runner de CI después del cambio, porque Node 25 y posteriores no incluyen Corepack.

Solución: npm install -g corepack y después corepack enable. En CI, ponelo como un paso antes de instalar dependencias.

La imagen cambió más que Node

Síntoma: Los pasos de apt-get o las librerías del sistema se comportan distinto después de rebuildear sobre node:lts o node:26, porque los tags default y slim pasaron de Debian 12 bookworm a Debian 13 trixie.

Solución: Fijá el sistema operativo en el tag (node:26.13.0-bookworm-slim) para la actualización de Node, y pasá a trixie en un cambio aparte.

NODE_MODULE_VERSION no coincide

Síntoma: “was compiled against a different Node.js version using NODE_MODULE_VERSION 137. This version of Node.js requires NODE_MODULE_VERSION 147.”

Solución: Reinstalá en 26 (npm ci o npm rebuild) en vez de copiar node_modules entre imágenes, e incluí el major de Node en cualquier clave de caché de node_modules.

require is not defined in ES module scope

Síntoma: Una dependencia CommonJS se cae al cargar en 26. Lo reproduje con yargs 16.2.0 y 17.7.2: su archivo de entrada sin extensión vive en un paquete “type”: “module”, que Node 26 ahora carga como ESM.

Solución: npm ls yargs y después actualizá a 16.2.2, 17.7.3 o 18. Para otros paquetes, buscá archivos de entrada sin extensión y reportalos upstream.

Node no arranca en un host Linux mínimo

Síntoma: El tarball oficial para Linux no corre en un host slim o una imagen propia donde 22 o 24 andaban bien.

Solución: Desde Node 25 los binarios de Linux enlazan contra libatomic: instalá el paquete libatomic o libatomic1. Las imágenes oficiales de Alpine ya lo agregan.

Un upstream falla solo en 26

Síntoma: Las llamadas con fetch() a un proxy o una API empiezan a fallar o a colgarse después de actualizar, mientras todo lo demás anda.

Solución: Confirmalo con el chequeo de ALPN del paso 4. Si es HTTP/2, configurá allowH2: false en un Agent global de undici (o VERCEL_FETCH_DISABLE_H2=1 en Vercel) y avisale a quien mantiene ese upstream.

Mi regla para esta migración

Fijá versiones exactas donde desplegás, cambiá una capa a la vez y dejá que CI falle por deprecaciones en 26 antes de que producción las vea. La etiqueta LTS no cambió ni una línea de código el 28 de octubre; cambió qué versión eligen tus versiones flotantes.

Conclusión

Para la mayoría de las apps, Node.js 26 LTS es una actualización fácil a nivel de código: lo que se rompe es acotado (cuatro eliminaciones, un cambio de comportamiento en streams y un flag de TypeScript), y un grep más una corrida de CI lo encuentran. El trabajo de verdad es operativo: Corepack y Yarn v1 desaparecieron, hay una base de Debian nueva detrás de node:lts, una ABI nativa nueva y HTTP/2 dentro de fetch().

Trabajá en el orden de esta guía: inventario, actualización con 26 al lado de tu línea actual en CI, arreglos y después Temporal donde las fechas duelen. Si seguís en 20, empezá hoy. Y anotá dos fechas en el calendario: el 30 de abril de 2027, cuando 22 llega al fin de su vida, y octubre de 2027, cuando 27 pase a ser el próximo LTS.

Sello de verificación

Revisado el 3 nov 2026: las release notes de 26.13.0 y 26.0.0, nodejs/Release, el índice de dist de nodejs.org, la documentación de la API de v26.13.0 y el código de Undici que trae, los archivos de las imágenes oficiales de Docker, setup-node v7.1.0, nvm v0.40.8, la documentación de fnm, mise y Volta, y la de Vercel, Netlify y AWS Lambda. Los snippets de JavaScript, o archivos de prueba equivalentes para los fragmentos de streams, corrieron en el build oficial 26.13.0 win-x64 (SHA-256 verificado) y en 22.22.0. Sin correr: GitHub Actions, builds de Docker, los codemods y nada en Linux.

Fuentes

Diego Rodriguez

Diego Rodriguez

Ingeniero Senior Full-Stack & AI

Diego tiene mas de 9 anos de experiencia construyendo aplicaciones potenciadas por IA de produccion, desde orquestacion de LLMs y pipelines RAG hasta deteccion de riesgos con ML y sistemas de trading algoritmico.

Conoce mas sobre Diego →