STATUS_DLL_NOT_FOUND (0xC0000135) when the DLL is right there
Short version: 0xC0000135 means Windows could not resolve a load-time DLL dependency, and a DLL in a subdirectory beside the executable does not count as resolvable. The search order is the application directory, the system directories, the working directory, then PATH, and subdirectories of the application directory appear nowhere in it. Fix it by prepending the bundled directory to PATH in the environment you spawn the child process with, at every spawn site. macOS and Linux have the same trap under DYLD_FALLBACK_LIBRARY_PATH and LD_LIBRARY_PATH.
A Windows executable exits immediately. No stack trace, no log line, no dialog if you launched it from a script. Just an exit code:
0xC0000135 (STATUS_DLL_NOT_FOUND)The frustrating part is that the DLL is not missing. It shipped in the archive, it is on disk, and it is sitting in a subdirectory right next to the executable.
Windows just does not look there.
What actually happens
influxdb3.exe has a load-time dependency on python313.dll, because its query engine embeds Python through PYO3. The vendor's archive puts that DLL in a co-located python\ subdirectory, which looks perfectly reasonable when you unzip it.
Windows resolves load-time dependencies through a fixed search order: the application directory, the system directories, the current working directory, then each entry in PATH. Subdirectories of the application directory are not in that list. A DLL one folder down is as invisible as one that was never shipped.
And because this is a load-time dependency, resolution happens before any of your code runs. The process never starts, so there is nothing to catch and nothing to log. The exit status is the entire diagnostic.
That combination is what makes this expensive to debug. A missing-file error you can search for. An instant exit with a hex code, on a binary whose files are all present, sends people looking at antivirus, permissions, and corrupt downloads first.
The fix
Add the bundled directory to PATH in the environment you spawn the process with. Not the system PATH, and not permanently: just for the child process.
const bundledDir = join(binaryDir, 'python')
spawn(exePath, args, {
env: {
...process.env,
PATH: `${bundledDir};${process.env.PATH ?? ''}`,
},
})Two things worth getting right.
Prepend, do not append. If some other copy of that DLL is already reachable, you want yours to win. A version mismatch on an embedded runtime produces a second, weirder class of failure.
Do it at every spawn site. This is where we got it wrong initially. We fixed the start path, and the same executable is also invoked to verify the install and to create an auth token. Both of those still failed, in flows nobody thinks of as "running the database," so the bug looked intermittent and version-dependent rather than universal.
The more robust option, if you control the packaging, is to flatten the DLL next to the executable so no environment manipulation is needed at all. We could not here, because the vendor's layout is what it is, and rearranging a bundled runtime risks breaking whatever relative paths it uses internally.
The same trap on macOS and Linux
This is not a Windows quirk. Every platform has a search path for dynamic libraries, and every platform's default excludes "some subdirectory near the binary."
macOS fails with a dyld error naming an absolute path, which is a useful hint that the binary was linked against a library location that exists only on the build machine:
dyld: Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylibThat is the same class of bug wearing better error text. A binary built where Homebrew put OpenSSL at a specific absolute path will not start on a machine without it. The spawn-time lever is DYLD_FALLBACK_LIBRARY_PATH.
Linux uses LD_LIBRARY_PATH for the same purpose.
Because the shape repeats, it is worth centralizing rather than fixing three times. A single helper that returns the right environment variable for the current platform, spread into every spawn, means adding a new engine cannot reintroduce the bug on one platform only:
function getLibraryEnv(binPath: string): Record<string, string> | undefined {
const libDir = join(binPath, 'lib')
if (platform() === 'darwin') return { DYLD_FALLBACK_LIBRARY_PATH: libDir }
if (platform() === 'linux') return { LD_LIBRARY_PATH: libDir }
return undefined // Windows takes the PATH treatment above
}How to diagnose it quickly next time
The reason this eats an afternoon is that the symptom carries no information. Two things make it fast.
Read the imports directly. On Windows, dumpbin /dependents or Dependencies (the modern Dependency Walker replacement) will name the DLL that is not resolving. On macOS use otool -L, on Linux ldd. This turns "it exits instantly" into "it wants this exact file," which is the entire problem solved.
Suspect bundled runtimes first. Embedded Python, embedded Node, a JVM, an OpenSSL copy: anything that ships a language runtime inside the archive is where this lives. A self-contained static binary never has this problem, which is a real argument for preferring one when you get the choice.
If you take one habit away: when a process exits with no output at all, stop reading your own logs and go read the binary's imports. Nothing you wrote has run yet.
FAQ
What does 0xC0000135 actually mean?
STATUS_DLL_NOT_FOUND: the loader could not resolve a DLL the executable depends on at load time. Because that resolution happens before any of your code runs, the process never starts, so there is no stack trace, no log line, and no dialog. The exit status is the entire diagnostic.
The DLL is right there next to the exe. Why can Windows not find it?
It is probably one folder down. Windows searches the application directory itself, the system directories, the current working directory, and each entry in PATH. Subdirectories of the application directory are not on that list, so a DLL in python\ beside the binary is as invisible as one that never shipped.
Should I add the folder to the system PATH?
No. Add it to the environment of the child process you spawn, and prepend rather than append, so your copy wins over any other copy of that DLL already reachable. A version mismatch on an embedded runtime is a second, weirder class of failure.
Why does it seem intermittent?
Because you probably fixed one spawn site. We fixed the start path and the same executable was still being invoked to verify the install and to mint an auth token, and both of those kept failing in flows nobody thinks of as running the database. The bug reads as version-dependent when it is actually universal.
What is the equivalent on macOS and Linux?
Same class of bug, better error text. macOS fails with a dyld error naming an absolute path, usually one that only existed on the build machine, and the spawn-time lever is DYLD_FALLBACK_LIBRARY_PATH. Linux uses LD_LIBRARY_PATH. Because the shape repeats, one helper that returns the right variable per platform beats fixing it three times.
How do I diagnose this fast next time?
Read the binary's imports rather than your own logs. dumpbin /dependents or Dependencies on Windows, otool -L on macOS, ldd on Linux. That turns "it exits instantly" into "it wants this exact file." And suspect bundled runtimes first: embedded Python, embedded Node, a JVM, a vendored OpenSSL.
We hit this shipping database engines across five platforms at Layerbase, where "the vendor's archive extracts fine" and "the binary starts" turn out to be very different claims.
Keep reading
- Scripting your databases with the Layerbase CLIThe Layerbase CLI is usually introduced as a way to run databases without Docker. This is about the other half of the surface: every command speaks --json, every failure sets an exit code, and nothing hangs waiting for a keyboard. Test runners, CI jobs, and plain shell scripts can drive it like an API.
- One Command From Local Database to CloudGraduating a local database used to be five steps and a copy-pasted connection string. The promote command in the Layerbase CLI does the whole thing: create, import, connection string, and an optional .env rewrite.
- The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full': what this warning meansIf your Node app just started printing a SECURITY WARNING about SSL modes being treated as aliases for verify-full, nothing is broken and nothing has changed yet. Here is what the warning actually means, why it appeared out of nowhere, and the one-line connection string fix.
- From PGlite to Production PostgresPGlite is a real Postgres compiled to WASM, so graduating a prototype to a hosted database is a dump and a restore, not a rewrite. Here is the whole path, start to finish.