Node.js vs Edge runtime for AI routes in Next.js: why every model and billing route stays on Node
AI and billing routes in Next.js 16 belong on the Node.js runtime: database drivers need TCP, streaming works on both, and edge fails at request time.
"Run it at the edge" sounds like free speed, and AI routes feel like the place you most want it. For a route that calls a model and then writes usage to Postgres, it is usually the wrong move — and the way it goes wrong is the expensive way: the build passes, the deploy succeeds, and the first real request fails. Here is why every model and billing route in the kit pins the Node.js runtime, and how to tell when Edge actually fits.
The two runtimes
Next.js gives route handlers two server runtimes. Per the Next.js 16 docs, the Node.js runtime is the default and has access to all Node.js APIs; the Edge runtime supports a more limited set of web-standard APIs — fetch, Request and Response, streams, Web Crypto, and so on. You choose per route with one export:
export const runtime = "nodejs"; // or "edge"
The docs list the Edge caveats plainly: not all Node.js APIs are supported and some packages may not work, there is no Incremental Static Regeneration, Edge is not supported with Cache Components, and the option cannot be set in a proxy file. One thing that is not a reason to choose Edge: streaming. The docs note that both runtimes can stream, depending on your deployment adapter.
What breaks: the database driver
The kit's chat route records every request's token usage in Postgres, through the postgres.js driver. That driver talks to the database over a raw TCP connection using Node's net module — and raw TCP sockets are not in the Edge runtime's API list. To see exactly how that fails, I added a throwaway route with runtime = "edge" that imports the kit's database client and runs one query. The build succeeded, with only a warning that Edge disables static generation for that route. Then the first request returned a 500:
Error: Failed query: select 1
[cause]: Error: The edge runtime does not support Node.js 'net' module.
That is the trap: nothing in the build tells you the route is broken. The failure waits for production traffic, and on a metered route it can take your usage recording down with it. Billing has the same dependency — the Stripe webhook is the only writer of subscription state, and it writes that state to the same database.
Why the kit writes the default anyway
Node.js is already the default, so export const runtime = "nodejs" changes nothing at runtime. The kit writes it on every API route anyway, for two reasons. It documents a hard requirement where the next person will see it — this route needs Node, do not "optimize" it. And it pins the requirement against copy-paste: a handler pasted from an Edge example, or a teammate's well-meant tweak, cannot silently move a database-backed route onto a runtime that cannot reach the database.
When Edge fits
Edge is a reasonable choice for small handlers that only need web-standard APIs and no Node-only dependency: rewriting or redirecting based on request headers, a lightweight response computed from the request itself, or code that reaches its data store over plain HTTP fetch instead of a TCP driver. If the handler touches a TCP database driver, a Node-only SDK, or the filesystem, keep it on Node.
Things that bite
- Edge does not make the model faster. An AI route's latency is dominated by the model's own time to first token and generation, not the network hop between your user and your function. Moving the handler closer saves little of the time users actually wait.
- Long generations need a long-lived function. The kit's chat route sets
maxDurationfor long streaming responses. Whatever runtime you use, check what your platform allows for it before promising long answers. - Test the runtime, not just the build. A green build proves the code compiled, not that its dependencies run in the chosen runtime. Hit every non-default-runtime route once before you ship it.
- Async-only crypto is a portability tell. The Stripe SDK's webhook verifier has an async variant precisely because some runtimes only offer async Web Crypto; using
constructEventAsynckeeps that handler portable, as covered in the Stripe webhook verification post.
Shipwright — the Next.js and Claude starter kit this blog documents — pins every model and billing route to Node.js, so streaming, metering, quotas, and the webhook all run where the database driver works. See a metered request in the live demo.