Node.js 26 LTS has been official since October 28, 2026, under the codename “Lithium”. For me, an LTS switch like this is always the moment to stop eyeing the next release and write an upgrade plan instead. If you only touch your Lambda functions once the deprecation email from AWS lands in your inbox, you work under time pressure. And time pressure is a pretty bad advisor for runtime upgrades.
On AWS Lambda, the nodejs26.x runtime is already available as a public preview, and according to the documentation AWS is targeting general availability for November 2026. That window is valuable: you can test today without having to switch production today. I will show you what Node.js 26 brings, what breaks when you move from Node.js 22 or 24, and how a small probe function for a fictional bike rental tells you what the new Lambda runtime can really do.
In short
Node.js 26 became LTS on October 28, 2026 and receives updates until the end of April 2029, while
nodejs26.xon Lambda is still a preview with a planned launch in November 2026. The most useful addition for everyday work is the Temporal API for dates and times. The biggest pitfall: what works under Node 26 on your laptop is not necessarily enabled the same way in the Lambda runtime, so check features there directly instead of assuming them.
What you will learn in this article
- What Node.js 26 LTS means for your support timeline on Lambda
- Which additions such as Temporal and Map.getOrInsert you can use right away
- Which changes can break when you upgrade from Node.js 22 or 24
- How to test nodejs26.x next to nodejs24.x with AWS CDK
- How to find every Node runtime in your AWS account
What Node.js 26 LTS means for your timeline
LTS stands for Long Term Support: a version receives security and bug fixes over a long period, and new features only arrive cautiously. Release 26.13.0, which officially starts Node.js 26 LTS, is purely formal. According to the release notes, it only switches metadata such as process.release to LTS and contains nothing else beyond 26.12.1. Updates for Node.js 26 LTS continue until the end of April 2029.
The Node.js project is also changing its rhythm. Until now, only even-numbered versions became LTS, while odd ones such as Node.js 25 were done after six months (25 reached EOL, or “end of life”, at the end of March 2026). Starting with Node.js 27, there will be one major version per year, and each of them becomes LTS after its six-month Current phase. For teams, that means fewer versions and a predictable cadence.
Here is where Lambda stands at the end of October 2026:
| Runtime | Node.js line | Status on Lambda |
|---|---|---|
nodejs22.x | LTS “Jod” | Deprecated on Apr 30, 2027, updates blocked from Aug 31, 2027 |
nodejs24.x | LTS “Krypton” | Deprecated on Apr 30, 2028 |
nodejs26.x | LTS “Lithium” | Public preview, general availability planned for November 2026 |
For many teams, the date that matters most is the end of nodejs22.x in April 2027.
My take: for many teams, the real deadline is not the launch of Node.js 26 but the deprecation of nodejs22.x. That leaves roughly six months. If you plan now, you can fit the upgrade into your normal sprint cadence.
Node.js 26 LTS features you will notice day to day
Coming from Node.js 24, you get not only the changes in Node.js 26 LTS but also everything from Node.js 25. That intermediate version was never LTS, so many teams skipped it. A few points stand out for daily work.
Temporal: dates and times without the headache
Node.js 26 LTS ships the Temporal API enabled by default, no flag required. Temporal is the new standard for dates and times in JavaScript and fixes the weaknesses of Date: months that start at 0, no real time zone arithmetic, and objects you can mutate by accident. Instead, you get clear types. Temporal.Instant is an exact point in time, Temporal.ZonedDateTime is a point in time in a specific time zone, and Temporal.PlainDate is a plain calendar date.
const pickup = Temporal.ZonedDateTime.from("2026-10-24T10:00[Europe/Berlin]"); const due = pickup.add({ days: 2 }); console.log(due.toString()); // 2026-10-26T10:00:00+01:00[Europe/Berlin] (same wall-clock time, even though the DST change happens in between) We will use exactly this example in the probe function later. With Date and “plus 48 hours”, you end up at 9 a.m. instead of 10 a.m. on October 26, because the clocks go back during the night before October 25. I cover how to use Temporal day to day for deadlines, time zones and DynamoDB in detail in Temporal API in TypeScript: deadlines and time zones.
Small helpers: getOrInsert, Iterator.concat and Base64
V8 14.6 adds two small but pleasant functions. Map.prototype.getOrInsert() and getOrInsertComputed() read a value or create it if it is missing. The classic “does this key exist yet?” pattern for grouping shrinks to a single line. Iterator.concat() chains several iterators without building intermediate arrays.
const rentals = [ { station: "Central Station", bikeId: "city-bike-17" }, { station: "Old Town", bikeId: "e-bike-03" }, { station: "Central Station", bikeId: "cargo-bike-02" }, ]; const byStation = new Map<string, string[]>(); for (const rental of rentals) { byStation.getOrInsert(rental.station, []).push(rental.bikeId); } // Map { "Central Station" => ["city-bike-17", "cargo-bike-02"], "Old Town" => ["e-bike-03"] } Node.js 25 contributed, for example, Uint8Array.fromBase64() and toBase64() (plus hex variants) and a noticeably faster JSON.stringify. In Lambda functions that build lots of JSON responses or encode binary data for API Gateway, that is a nice bonus for free. One observation from my test: with "lib": ["esnext"], TypeScript 7.0.2 already knows the types for Temporal, getOrInsert and fromBase64, but still reports an error for Iterator.concat.
Platform: Undici 8, Web Storage and permissions
Undici, the built-in HTTP client behind fetch, moves to version 8. Since Node.js 25, the Web Storage API is enabled by default, but in Node.js 26 localStorage returns undefined as long as no storage file is configured. Since Node.js 25, the permission model also knows --allow-net, which lets you restrict a process’s network access. On Lambda, that matters more for local tools and CI scripts, because isolation there is handled by IAM and VPC anyway.
What breaks when you move to Node.js 26 LTS
Most projects keep running after the upgrade to Node.js 26 LTS. Still, there are a handful of changes worth checking deliberately before you switch the runtime:
- The old internal modules
_stream_readable,_stream_writableand their siblings are removed in Node.js 26. Older libraries sometimes still import them directly. -
http.Server.prototype.writeHeader()is gone, usewriteHead(). - Since Node.js 25,
fs.F_OK,fs.R_OK,fs.W_OKandfs.X_OKare no longer exported directly from the module. The constants live underfs.constants. -
SlowBufferandassert.CallTrackerhave reached end of life since Node.js 25. - Corepack is no longer bundled since Node.js 25. If you enable pnpm or Yarn via Corepack, install it separately in CI.
-
module.register()emits a runtime deprecation warning in Node.js 26, and--experimental-transform-typeswas removed. - Native addons need a rebuild, because
NODE_MODULE_VERSIONchanges to 147. - Since Node.js 25, the official Linux binaries are linked against
libatomic. Slim container images for CI may be missing that package.
On top of that on Lambda: what has applied since nodejs24.x
If you jump from nodejs22.x straight to 26, you also pick up the Lambda-specific changes AWS introduced with nodejs24.x. Callback handlers are no longer supported, context.callbackWaitsForEmptyEventLoop is removed, and so are context.succeed, fail and done. In addition, Lambda no longer waits for pending promises once your handler returns. Anything you still want to finish has to be completed with await first.
// Before: callback handler (no longer works from nodejs24.x) export const handler = (event, context, callback) => { loadBike(event.bikeId) .then((bike) => callback(null, bike)) .catch((err) => callback(err)); }; // After: async handler, every piece of work is awaited export const handler = async (event: { bikeId: string }) => { const bike = await loadBike(event.bikeId); await auditLog.write({ action: "lookup", bikeId: bike.id }); // without await this would get lost return bike; }; One more point is easy to miss: Lambda disables experimental Node.js features by default, such as require() of ES modules and automatic module detection. You can turn them back on via NODE_OPTIONS, but AWS covers such cases with neither support nor the SLA.
nodejs26.x on Lambda: the state at the end of October 2026
The nodejs26.x runtime is available as a public preview, both as a ZIP runtime and as the container base image public.ecr.aws/lambda/nodejs:26-preview. Preview means no SLA, no technical support, and breaking changes are possible. It is explicitly not meant for production workloads. AWS names November 2026 as the launch of the stable version, but only as a non-binding estimate.
The feedback issue for the preview on GitHub is revealing. Users report, among other things, that Temporal is missing in the preview and that process.features.require_module is false. These are community observations, not an official AWS statement, and a lot can change before the stable release. I still think the lesson matters: a Lambda runtime is not automatically identical to the Node.js on your machine. So let us check directly in Lambda.
🎬 Probe function on two Lambda runtimes – this animated diagram only runs on my site: watch it on sascha-ms.de
Step by step: testing nodejs26.x next to nodejs24.x
Our example is a fictional bike rental. A function calculates when a rented bike has to be back: same time of day, a given number of days later, in the time zone of the rental station. It also returns a small report on which features the runtime really provides. If Temporal is built in, the function uses it, otherwise a polyfill steps in. A polyfill is a retrofitted implementation of a standard API for environments that do not support it yet.
Prerequisites
- Node.js 22 or newer locally (thanks to the polyfill, the tests run without Node.js 26 too)
- AWS CLI v2 with credentials for a test or sandbox account
- An account bootstrapped with
npx cdk bootstrapin your region - A region where
nodejs26.xis already selectable. You can see that in the Lambda console when creating a function.
Step 1: create the project
mkdir bike-rental-runtime-check && cd bike-rental-runtime-check npm init -y npm install @js-temporal/polyfill@0.5.1 npm install -D typescript@7.0.2 @types/node@26 @types/aws-lambda tsx esbuild aws-cdk-lib@2.275.0 constructs aws-cdk@2.1146.1 mkdir -p bin lib src test Switch package.json to ES modules and add a few scripts:
{ "name": "node26-lambda-upgrade", "version": "1.0.0", "scripts": { "typecheck": "tsc --noEmit -p .", "test": "node --import tsx --test test/*.test.ts", "synth": "cdk synth", "deploy": "cdk deploy" }, "license": "MIT", "devDependencies": { "@types/aws-lambda": "^8.10.164", "@types/node": "^26.6.6", "aws-cdk": "^2.1146.1", "aws-cdk-lib": "^2.275.0", "constructs": "^10.8.1", "esbuild": "^0.28.2", "tsx": "^4.23.15", "typescript": "^7.0.2" }, "dependencies": { "@js-temporal/polyfill": "^0.5.1" }, "private": true, "type": "module" } Step 2: configure TypeScript
With "lib": ["esnext"], TypeScript knows the new APIs such as Temporal. noEmit is enough, because esbuild bundles during deployment and tsx runs the tests directly.
// tsconfig.json { "compilerOptions": { "target": "es2024", "lib": ["esnext"], "module": "nodenext", "moduleResolution": "nodenext", "strict": true, "types": ["node"], "noEmit": true, "skipLibCheck": true }, "include": ["bin", "lib", "src", "test"] } Step 3: write the runtime report
The function checks every feature directly instead of inferring it from the version number. That is more reliable, because a runtime can have a feature disabled despite a matching version.
// src/runtime-report.ts // Reports which runtime features are actually available in the current Node.js environment. export interface RuntimeReport { nodeVersion: string; ltsName: string | null; temporal: boolean; mapGetOrInsert: boolean; iteratorConcat: boolean; uint8ArrayBase64: boolean; requireModule: boolean; lambdaRuntime: string | null; } export function collectRuntimeReport(env: NodeJS.ProcessEnv = process.env): RuntimeReport { const g = globalThis as Record<string, unknown>; const iteratorCtor = g.Iterator as { concat?: unknown } | undefined; return { nodeVersion: process.version, ltsName: process.release.lts ?? null, temporal: typeof g.Temporal === "object" && g.Temporal !== null, mapGetOrInsert: typeof (Map.prototype as { getOrInsert?: unknown }).getOrInsert === "function", iteratorConcat: typeof iteratorCtor?.concat === "function", uint8ArrayBase64: typeof (Uint8Array as { fromBase64?: unknown }).fromBase64 === "function", requireModule: process.features.require_module === true, // In Lambda, AWS_EXECUTION_ENV looks like this, for example: AWS_Lambda_nodejs24.x lambdaRuntime: env.AWS_EXECUTION_ENV?.replace("AWS_Lambda_", "") ?? null, }; } Step 4: load Temporal with a fallback
// src/temporal.ts // Uses the built-in Temporal if the runtime provides it, otherwise the polyfill. export type TemporalApi = typeof Temporal; export async function loadTemporal(): Promise<{ api: TemporalApi; source: "runtime" | "polyfill" }> { const builtIn = (globalThis as { Temporal?: TemporalApi }).Temporal; if (builtIn) { return { api: builtIn, source: "runtime" }; } const polyfill = await import("@js-temporal/polyfill"); return { api: polyfill.Temporal as unknown as TemporalApi, source: "polyfill" }; } The dynamic import ensures that the polyfill is only loaded when it is needed. esbuild still bundles it, which makes the package about 150 KB larger. Once Temporal is reliably available in the stable runtime, you can remove the fallback.
Step 5: the rental logic
// src/rental.ts // Fictional bike rental: when does a bike have to be back? import type { TemporalApi } from "./temporal.js"; export interface RentalRequest { bikeId: string; pickupAt: string; // pickup time as an instant, e.g. "2026-10-24T08:00:00Z" days: number; // number of rental days timeZone: string; // time zone of the rental station, e.g. "Europe/Berlin" } export function validateRental(input: Partial<RentalRequest>): RentalRequest { const { bikeId, pickupAt, days, timeZone } = input; if (!bikeId || !pickupAt || !timeZone) { throw new Error("bikeId, pickupAt and timeZone are required"); } if (typeof days !== "number" || !Number.isInteger(days) || days < 1 || days > 30) { throw new Error("days must be an integer between 1 and 30"); } return { bikeId, pickupAt, days, timeZone }; } // With Temporal, "2 days" means the same wall-clock time two calendar days later, even across a DST change. export function returnDeadline(T: TemporalApi, req: RentalRequest): string { return T.Instant.from(req.pickupAt) .toZonedDateTimeISO(req.timeZone) .add({ days: req.days }) .toString({ smallestUnit: "minute" }); } // The classic Date approach just adds milliseconds. Across a DST change it is off by one hour. export function naiveReturnDeadline(req: RentalRequest): string { const start = new Date(req.pickupAt).getTime(); return new Date(start + req.days * 24 * 60 * 60 * 1000).toISOString(); } Step 6: the Lambda handler
// src/handler.ts // Lambda handler: returns the runtime report and the return deadline for a rental. import { collectRuntimeReport } from "./runtime-report.js"; import { loadTemporal } from "./temporal.js"; import { naiveReturnDeadline, returnDeadline, validateRental, type RentalRequest } from "./rental.js"; const DEMO_RENTAL: RentalRequest = { bikeId: "city-bike-17", pickupAt: "2026-10-24T08:00:00Z", days: 2, timeZone: "Europe/Berlin", }; // Deliberately async without a callback: since nodejs24.x, Lambda no longer supports callback handlers. export const handler = async (event: Partial<RentalRequest> = {}) => { const rental = validateRental({ ...DEMO_RENTAL, ...event }); const { api, source } = await loadTemporal(); return { runtime: collectRuntimeReport(), temporalSource: source, rental: { bikeId: rental.bikeId, returnAt: returnDeadline(api, rental), naiveReturnAtUtc: naiveReturnDeadline(rental), }, }; }; Step 7: tests with the Node test runner
The tests cover the interesting case: a rental across the DST change on October 25, 2026. This shows in black and white why Temporal is worth the switch.
// test/rental.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { loadTemporal } from "../src/temporal.js"; import { naiveReturnDeadline, returnDeadline, validateRental } from "../src/rental.js"; const overDstChange = validateRental({ bikeId: "city-bike-17", pickupAt: "2026-10-24T08:00:00Z", // 10:00 in Berlin (summer time) days: 2, timeZone: "Europe/Berlin", }); test("Temporal keeps the wall-clock time across the DST change", async () => { const { api } = await loadTemporal(); assert.equal(returnDeadline(api, overDstChange), "2026-10-26T10:00+01:00[Europe/Berlin]"); }); test("Date arithmetic ends up one hour early after the DST change", () => { // 08:00 UTC on Oct 26 is 09:00 winter time in Berlin, not 10:00 assert.equal(naiveReturnDeadline(overDstChange), "2026-10-26T08:00:00.000Z"); }); test("without a DST change both approaches agree", async () => { const { api } = await loadTemporal(); const normalWeek Source:https://dev.to/sascha-ms/nodejs-26-lts-planning-the-lambda-upgrade-5e5e 