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ínea | Nombre | Estado | Última | Maintenance desde | Fin de vida |
|---|---|---|---|---|---|
| 20 | Iron | Fin de vida | 20.20.2 (24 mar 2026) | 22 oct 2024 | 30 abr 2026 |
| 22 | Jod | Maintenance LTS | 22.23.4 | 21 oct 2025 | 30 abr 2027 |
| 24 | Krypton | Maintenance LTS | 24.22.2 | 20 oct 2026 | 30 abr 2028 |
| 26 | Lithium | Active LTS | 26.13.0 | 20 oct 2027 | 30 abr 2029 |
| 27 | — | Alpha | 27.0.0-alpha.0.0.0 (29 oct) | Ninguna: solo Active LTS | 30 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.0Lo 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 LithiumCI: 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
| Plataforma | Estado de Node 26 | Cómo se elige | Ojo con |
|---|---|---|---|
| Vercel | GA para builds y Functions desde el 29 oct; default para proyectos nuevos | engines.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 |
| Netlify | Los 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 UI | Las Functions siguen la versión del build salvo que definas AWS_LAMBDA_JS_RUNTIME; las páginas que leí no mencionan 26 |
| AWS Lambda | nodejs26.x en preview pública; disponibilidad general prevista para noviembre de 2026 | El identificador de runtime, nodejs26.x | Los 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 oct | Un tag explícito como node:26.13.0-bookworm-slim, idealmente con su digest | Los tags default y slim son Debian trixie; Alpine solo para amd64 y arm64; sin Yarn v1 |
| GitHub Actions | setup-node@v7 resuelve 26 y lts/* a 26.13.0 | node-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
| Cambio | Desde | Qué se rompe | Solución |
|---|---|---|---|
| Se eliminaron _stream_readable, _stream_writable, _stream_duplex, _stream_transform, _stream_passthrough y _stream_wrap (DEP0193, DEP0125) | 26.0.0 | require() 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.0 | TypeError: ya no es una función | res.writeHead(), con los mismos argumentos |
| read() de un Readable devuelve un buffer por vez | 26.0.0 | El 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-types | 26.0.0 | “bad option” al arrancar; los enums y namespaces fallan con ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | Escribí 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.0 | decipher.setAuthTag() rechaza tags AES-GCM de menos de 16 bytes | Pasá authTagLength a crypto.createDecipheriv() |
| Los archivos sin extensión en paquetes “type”: “module” cargan como ESM | 26.0.0 | ReferenceError: require is not defined in ES module scope, por ejemplo con yargs 16.2.0 y 17.7.2 | Actualizá 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.CallTracker | 25.0.0 | Valores 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 runtime | Warnings hoy; la documentación dice que van a tirar error o desaparecer en una versión futura | module.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 256. 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.000ZTemporal 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 desplegar7. 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ínea | Alpha | Current (x.0.0) | LTS | Fin de vida |
|---|---|---|---|---|
| 26 (modelo viejo) | — | 5 may 2026 | 28 oct 2026 | 30 abr 2029 |
| 27 | oct 2026 | 22 abr 2027 | 20 oct 2027 | 30 abr 2030 |
| 28 | oct 2027 | abr 2028 | oct 2028 | abr 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
- Node.js — release notes de 26.13.0 ‘Lithium’ (LTS) (28 oct 2026)
- Node.js — post de la release 26.0.0 (5 may 2026)
- nodejs/node — changelog completo de v26.0.0
- nodejs/Release — calendario y fases de releases
- Node.js — Evolving the Node.js release schedule
- Node.js — Releases anteriores
- Documentación de Node.js v26 — Deprecaciones
- nodejs/node — PR #60441, stream: readable read one buffer at a time
- yargs — issue #2509, Node.js 25.7 y 26 rompen yargs 16 y 17
- Vercel — Node.js 26 LTS ya está disponible en general (29 oct 2026)
- Vercel — Versiones de Node.js soportadas
- Netlify — Manage build dependencies
- AWS Lambda — Runtimes (preview de nodejs26.x)
- nodejs/docker-node — Yarn v1 Classic bundling
- docker-library/official-images — tags de library/node
- actions/setup-node
- nodejs/userland-migrations — codemods oficiales
- Undici — Migrar de v7 a v8
- nodejs/corepack — README



