Node 26 reached Active LTS on 28 October 2026, and MSVC's linker has started refusing a flag that nobody's project asked for:
LINK : fatal error LNK1117: syntax error in option 'opt:lldltojobs=2'
The 2 is not from your binding.gyp, your runner, or your Visual Studio install. It came from the machine that built the Node binary you downloaded, and it travels to you inside the header bundle node-gyp reads. Teams that pin by channel rather than by version picked this up the moment node:lts moved, and the addons compiling from source at all are the ones whose prebuilds went stale under NODE_MODULE_VERSION 147. The common advice in the GitHub threads is "upgrade node-gyp", which for most readers does nothing at all.
Where the 2 actually comes from
Two different Windows build arguments produce two different scopes of LTO, and only one of them ships to you.
vcbuild.bat maps the release target onto set ltcg=1, which passes --with-ltcg. configure.py documents that as Thin LTO scoped to node.exe and libnode only, which touches nothing a consumer compiles. The distribution path is separate: inside if defined build_release (, vcbuild.bat sets thin_lto=1, which passes --enable-thin-lto, applied globally. A contributor building Node for Windows the usual way never reproduces the failure. Only the official binary on nodejs.org carries it, which is a reasonable explanation for how the change got through review in 26.3.0 without anyone noticing the blast radius.
Why zeroing clang was never going to help
The Windows block in common.gypi on the v26.x branch sits under ['OS=="win"', {...}] and holds three conditions. enable_lto=="true" adds -flto=full to VCCLCompilerTool, VCLibrarianTool and VCLinkerTool. enable_thin_lto=="true" adds -flto=thin to the same three. The third one is fatal:
['(enable_thin_lto=="true" or enable_lto=="true") and lto_jobs!=""', {
'msvs_settings': {
'VCLinkerTool': {
'AdditionalOptions': ['/opt:lldltojobs=<(lto_jobs)'],
},
},
},],
Every gate there is an LTO variable. The clang variable appears elsewhere in the file and only to pick the flag spelling for Unix builds (['clang==1', { 'lto': ' -flto ' }, { 'lto': ' -flto=4 -ffat-lto-objects ' }]). The Windows path never consults it.
That matters because node-gyp has long carried, inside the process.platform === 'win32' branch of lib/create-config-gypi.js, the line if (config.variables.clang === 1) { config.variables.clang = 0 }. An addon built with the default MSVC toolchain is not a clang build, so the reset is correct. It is also beside the point. The generated build/config.gypi pasted by the reporter of nodejs/node#64674 shows both halves in one file:
"clang": 0,
"enable_lto": "false",
"enable_thin_lto": "true",
"lto_jobs": "2",
An addon declared MSVC, still asking the linker for lld's job-count option. Fixing the compiler identity does not retract the request.
Why the same Node version only warns on a colleague's box
configure.py sets o['variables']['lto_jobs'] = options.lto_jobs or '', so the value stays empty unless --lto-jobs was passed to the build. Empty lto_jobs fails the third condition, and the addon gets -flto=thin with no linker option attached. cl.exe rejects that as warning D9002 and ignores it, per the issue text. The build then succeeds, produces no LTO at all, and leaves behind a warning that scrolls past in CI logs. Populated lto_jobs is the crash case: MSVC's /OPT: accepts REF, ICF, LBR and their negations, so /opt:lldltojobs=2 aborts the link.
One command tells you which side you are on, no build required:
node -p "JSON.stringify(process.config.variables, ['clang','enable_lto','enable_thin_lto','lto_jobs'])"
Run that on the image, not on your laptop. The silent-warning variant is arguably the worse outcome of the two, because a shop that believed it was shipping link-time-optimised addons has been shipping ordinary ones since 26.3.0 with a D9002 as the only receipt.
Who actually gets node-gyp 13?
nodejs/node-gyp#3331, merged 10 June 2026 by legendecas, adds five lines to that same win32 branch, setting variables.enable_lto = 'false' and variables.enable_thin_lto = 'false' with a comment naming the leak. StefanStojanovic, who added Thin LTO to Node on Windows, called it "an oversight from my side" in review. The fix landed in node-gyp 13, nearly five months before Node 26 went LTS.
The delivery is the problem. npm install runs the node-gyp bundled inside npm, not whatever node-gyp is on your PATH. npm 11 bundles node-gyp 12, and both Node 26 and Node 24 ship npm 11. The reporter in #64674 records stock Node 26.5.0 as npm 11.18.0 with node-gyp 12.4.0, failing. So the circulating answer is correct about the source tree and wrong about what executes, a pattern worth recognising from the rest of the npm 12 rollout. node-gyp 13.0.0 also narrowed its supported Node range to 22.22.2+, 24.15.0+ and 26.0.0+, so a package that still supports older lines cannot simply declare the dependency and move on.
The Node 24 release line is now blocked on npm's dependency tree
Node wants the same Thin LTO on the v24 Windows releases, which is nodejs/node#66170, a draft since 6 October 2026. richardlau, on 26 October: "I would be uncomfortable landing it in that situation, so yes I think it would make this impossible to land if the bundled node-gyp in npm doesn't work with it."
A release-toolchain decision for Node is waiting on a transitive dependency of npm. That ordering constraint will recur every time Node's build flags move faster than the node-gyp npm carries, and the gap is structural: npm's bundled copy is pinned by npm's own release cadence, while the flags are chosen by whoever builds the binaries. Naming it is the only way it gets designed around rather than rediscovered.
The part I could not confirm
node-gyp's reset is gated on the host platform being win32, and no equivalent exists for Linux or macOS. By reading, a Node built or vendored with --enable-lto should therefore push -flto into every addon compiled on that host, with whatever ABI consequences follow. I have not found a distro that ships one, so treat this as a plausible second exposure rather than a reported bug, and settle it on your own base image with node -p "process.config.variables.enable_lto".
What to set before your next Windows build
- Check the image, not the version string. Run the four-key
node -pabove in the Dockerfile or runner setup. Anything from 26.3.0 onward reportsenable_thin_lto: "true"; the presence of a non-emptylto_jobsseparates the fatal case from the silent one. On Brotli patch day I ran a behavioural probe instead of trusting the release string and goterrno=-9back from an EOL Node 20 host, the same reason the version string lies applies to build flags as much as to CVEs. - Pin the fix at the environment level, for every Windows job, permanently:
env:
npm_config_enable_thin_lto: "false"
npm_config_enable_lto: "false"
npm forwards both as gyp defines that outrank the values in config.gypi. This is what node-libcurl shipped in v5.1.2, and it needs no toolchain change on any developer machine. Set both keys, not just the thin one, so a future --enable-lto build does not reopen the hole.
- If you maintain a published addon, filter the flags in your own
binding.gypwithAdditionalOptions/exclusions, as node-gtk does. That protects your consumers without asking them to set anything, and it covers the users who will never read your release notes. - Stop telling people to
npm i -g node-gyp@13. It fixes nothing undernpm install, and the 13.0.0 engine floor (22.22.2, 24.15.0, 26.0.0) rules it out for anyone supporting older lines. - Key the prebuild matrix on
config.gypi, not on the ABI number alone. Buildability now depends on which Node binary's configuration the addon saw, and 26.0 through 26.2 behave differently from 26.3 and later at the same ABI 147.
Pinning Node 22 also works, with an LTS clock running against it, so spend the two environment variables instead.
Comments
Be the first to comment.