Skip to main content

On 29 September, Netlify published the engineering write-up behind a rebuild of its Edge Functions platform: V8 isolates out, Firecracker MicroVMs in, roughly 5x faster at the median. Buried in the architecture section is a sentence most vendors would never put in print. “V8 isolates, no matter their name, do not provide this level of isolation.” That is a platform telling you the thing it used to run your code in was not a security boundary.

The post also offers a reassurance: “This doesn’t change how Edge Functions are written or used.” We wanted to know whether that holds. So we measured what the JavaScript ecosystem actually does when a platform changes runtimes underneath it.

TL;DR

  • Netlify replaced V8 isolates with Firecracker MicroVMs, cutting warm p50 invocations from 25-40ms to 5-6ms and p99 by 47.4%, and said plainly that V8 isolates do not provide tenant isolation.
  • We checked 106 npm packages that commonly ship inside edge functions. 37 of them serve a different implementation depending on a runtime condition string you never set.
  • Exactly one of the 106, @libsql/client, ships a condition named netlify. We bundled it with the configuration @netlify/edge-bundler 16.1.1 actually uses, and that condition is never selected.
  • Same package, same version, different conditions: 414,578 bytes of bundle versus 245,432, and one build throws on configurations the other serves.
  • Your runtime and your module resolution are chosen by different parties, and neither one reports what you got. That is the gap worth auditing, not the runtime swap.

Conditional exports are a contract nobody reads

Since Node 12, a package can route a single import specifier to several different files using conditions in its exports field. node gets one build, browser another, and the edge ecosystem has since bolted on workerd, edge-light, deno, bun, worker and more. The library author writes the map. The bundler decides which key wins.

That means the code you ship to production is selected by a string set in your platform’s build configuration, not in your repository. You do not import it. You do not see it in your lockfile. It does not appear in a diff.

We took 106 runtime libraries and SDKs of the kind that genuinely end up inside edge functions and middleware: database clients, auth, payments, observability, analytics, HTTP clients, crypto. For each we pulled the published manifest from the npm registry and walked the full exports tree.

68 ship an exports map at all. 28 of those branch on a runtime condition inside it, and a further nine switch builds through the older top-level browser field: 37 of the 106 in total. Within the exports maps, the distribution is browser in 18 packages, node in 11, workerd in 8, bun, worker and react-native in 5 each, edge-light in 4, deno and react-server in 3, edge in 2. Just over a third of the libraries in a typical edge bundle hand you different code based on a word.

One package names Netlify. Netlify never asks

Exactly one package in the 106 declares a netlify condition: @libsql/client 0.18.0. It points at lib-esm/web.js. Its deno condition points somewhere else entirely, at lib-esm/node.js.

Those are not two formats of the same thing. node.js routes file: URLs into a native SQLite driver. web.js handles only ws:, wss:, http: and https:, and for anything else throws URL_SCHEME_NOT_SUPPORTED. A configuration that works under one condition is a runtime exception under the other.

So we checked which condition Netlify sets. @netlify/edge-bundler 16.1.1, published 28 September, bundles npm dependencies in dist/node/npm_dependencies.js by calling esbuild with platform: 'node' and mainFields: ['module', 'browser', 'main']. There is no conditions array.

We reproduced it. Same esbuild version the bundler pins, 0.28.1, same options, resolving a real entry point:

  • Netlify’s configuration resolves @libsql/client to lib-esm/node.js and pulls the native SQLite code path into the output. Bundle: 414,578 bytes.
  • conditions: ['netlify'] resolves to lib-esm/web.js, no native path. Bundle: 367,015 bytes.
  • conditions: ['workerd'] resolves to web.js as well, and swaps the Postgres driver too. Bundle: 245,432 bytes.

A 169,146-byte spread across the same source, the same lockfile and the same package versions. A library author wrote a build specifically for Netlify, and Netlify’s own bundler does not ask for it.

This is not a bug report. Netlify’s new MicroVMs provide real Node built-ins, which is precisely why platform: 'node' is a defensible choice now in a way it was not on a V8 isolate. The point is narrower and worse: the resolution contract and the execution environment are set independently, by two different parties, and neither of them tells you which combination you ended up with.

The build that only runs on Cloudflare

The postgres package makes the stakes concrete. Version 3.4.9 ships an entire parallel source tree under cf/ for the workerd condition, with a polyfill module that shims process, os, fs, crypto and tls, and opens sockets by importing cloudflare:sockets. That build cannot run anywhere except Cloudflare. The default build imports bare net and tls instead.

postgres declares workerd and bun. It declares no edge-light, no deno, no netlify. Everywhere else you get the Node build, and the bundle we produced under Netlify’s configuration carries unprefixed os, fs, net, tls, crypto, stream and perf_hooks imports out the other side, to be satisfied by an import map the platform generates.

Four layers decide whether your database connection works: the library’s condition map, the bundler’s condition set, the platform’s import map and the runtime’s actual capabilities. Three of those four live outside your repository.

What to do about it

Audit the output, not the input. The only honest answer to “what is in my edge function” comes from the bundle, so make your platform emit a metafile or a build manifest and keep it. Grep it for the entry points you did not expect.

Write an assertion, not a comment. If your edge function must use the HTTP driver rather than the native one, add a startup check that fails loudly on the wrong code path. A condition flip is otherwise silent until a specific request shape hits it in production.

Treat a runtime migration as a resolution review. When a platform changes its execution model, the interesting question is not whether your syntax still parses. It is whether anything in the chain that picks your dependency builds has moved, and whether anyone measured it.

And reach for the boring architecture where you can. A database client that speaks HTTP has one build. A client that needs raw TCP has as many builds as there are socket APIs, and you inherit every one of those decisions.

REPTILEHAUS builds and operates production systems on exactly this kind of infrastructure, and this is the sort of thing we check before it reaches a customer rather than after. If you are running edge functions, serverless workloads or a build pipeline nobody has audited in a while, get in touch.

Methodology: package manifests pulled from the npm registry on 1 October 2026; condition maps walked in full, including nested keys. Bundle comparisons produced with esbuild 0.28.1, the version pinned by @netlify/edge-bundler 16.1.1, bundling a single ESM entry point importing @libsql/client 0.18.0 and postgres 3.4.9.

📷 Photo by Timelab (@timelabpro) on Unsplash