Dynamic Code Evaluation Not Allowed in Edge Runtime: The Next.js Middleware Fix
It builds locally. It builds in CI. It fails on deploy, and the message does not tell you which of your 900 dependencies caused it:
Error: Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
You did not write eval. You imported Mongoose so you could look up the user's session, or jsonwebtoken so you could verify a token before letting the request through. That is the whole crime.
The framing that wastes the most time here is treating this as a bundler quirk to be silenced. It is not. It is the Edge Runtime telling you, correctly, that the code you are trying to run cannot run there. The fix is almost never to make the error go away. It is to move the work somewhere that has a real Node.js runtime, or to replace the library with one built for Web APIs.
Which fix applies depends entirely on your Next.js version:
- Next.js 16 and later:
middlewarehas been renamed toproxyand it defaults to the Node.js runtime, so the error disappears. Migrate withnpx @next/codemod@canary middleware-to-proxy .. Note that setting aruntimeconfig option in a proxy file now throws. - Next.js 15.5 to 15.x: add
export const config = { runtime: 'nodejs' }tomiddleware.ts. This became stable in 15.5 and was experimental from 15.2. - Next.js 15.1 and earlier: middleware is Edge only. Move the database or crypto work into a Route Handler or Server Component and leave middleware doing cheap routing decisions.
unstable_allowDynamic silences the build check for specific files. It does not make the code work. If the statement is ever reached at runtime, it throws.
The exact error
Error: Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation
The odd spacing in e. g. is in the real message, so if you are searching for it, that is why your quoted search returns nothing. You will also meet its close relatives, which have the same root cause and the same fixes:
Module not found: Can't resolve 'fs'
Module not found: Can't resolve 'net'
The edge runtime does not support Node.js 'crypto' module.
Why this happens
The Edge Runtime is not Node.js with some bits removed. It is a V8 isolate with a Web-standard API surface: fetch, Request, Response, URL, TextEncoder, crypto.subtle. There is no filesystem, no TCP sockets, no node:crypto, no process spawning. Middleware defaulted to it for years because middleware runs on every matched request, often at a CDN node far from your origin, and a full Node.js process per request would defeat the point.
The dynamic-evaluation ban is a consequence of how those isolates are deployed. Code is compiled once and the isolate is reused across requests, so allowing eval or new Function would mean compiling arbitrary strings inside a shared sandbox. It is disallowed outright, and Next.js checks for it at build time by walking the module graph.
That build-time walk is the part that surprises people. The check is static. If any module reachable from your middleware contains a literal new Function(...) anywhere in the file, the build fails, even if that branch could never execute in your application. Mongoose is the classic case: the Edge bundler resolves the browser export condition of the package, pulls in dist/browser.umd.js, and that bundle contains dynamic evaluation. You never call it. It still fails.
jsonwebtoken, bcrypt, most SQL drivers, Prisma's engine and anything wrapping a native addon fail for the other reason: they need Node core modules that simply do not exist in the isolate.
The fix
Step 1: Find out which dependency is actually doing it
The message frequently names no package. Comment out the imports in your middleware one at a time and rebuild. It is crude and it takes four minutes, and it beats guessing. When you have a suspect, confirm it by reading its exports map:
cat node_modules/mongoose/package.json | jq '.exports'
Look for browser, edge-light, worker or workerd conditions. A package that ships only node and browser conditions will hand the Edge bundler its browser build, which is where the new Function usually lives. A package that ships an edge-light condition has been built with this environment in mind and will normally be fine.
Step 2: Check your Next.js version, because the answer changed twice
npx next --version
| Version | What to do |
|---|---|
| 16.0 and later | middleware is deprecated and renamed to proxy, and proxy defaults to the Node.js runtime. Run the codemod, then import whatever you like. Do not set a runtime option in the proxy file, it throws. |
| 15.5 to 15.x | Node.js middleware is stable. Add export const config = { runtime: 'nodejs' } to middleware.ts. |
| 15.2 to 15.4 | Same thing, but experimental and gated behind a flag in next.config.js. Workable, not something I would put in front of paying customers. |
| 15.1 and earlier | No Node.js option exists. Step 3 is your only route. |
For Next.js 16, the migration is one command:
npx @next/codemod@canary middleware-to-proxy .
It renames middleware.ts to proxy.ts and renames the exported function. Your config.matcher carries over unchanged.
Step 3: Split the work if you are staying on Edge
This is the part worth internalising even if you can switch runtimes, because it produces a better application either way. Middleware runs on every matched request. Putting a database round trip in it means every page load pays for a connection to your database from wherever the edge node happens to be, which for a user in Nairobi hitting a database in Frankfurt is not a rounding error.
So do the cheap check at the edge and the real check where the data is:
// middleware.ts (or proxy.ts) - no database, no Node crypto
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose' // Web Crypto based, runs on Edge
const secret = new TextEncoder().encode(process.env.SESSION_SECRET!)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
await jwtVerify(token, secret) // signature and expiry only
} catch {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*'] }
Then the authorisation that needs the database happens in the Route Handler or Server Component that actually serves the page:
// app/dashboard/page.tsx - Node.js runtime, Mongoose is fine here
import { connectToDatabase } from '@/lib/db'
import { getSession } from '@/lib/session'
export default async function Dashboard() {
const session = await getSession() // reads and verifies the cookie
await connectToDatabase()
const user = await User.findById(session.sub).lean()
if (!user?.isActive) redirect('/login')
// ...
}
Swapping jsonwebtoken for jose is usually the single change that unblocks a stuck middleware, because jose is built on Web Crypto rather than node:crypto. Keep the signing secret out of the repository; my notes on a secrets workflow for small teams cover a workable setup.
Verification
A local next dev is not proof, because the dev server is more forgiving than the production bundler. Build the way your platform builds:
npx next build
A clean build prints the route table with no error, and middleware or proxy appears with a size in the listing. If it compiled but you want to be sure which runtime it landed on, run the production server and hit a matched route:
npx next start &
curl -sI http://localhost:3000/dashboard | head -1
Expect a redirect to /login for an unauthenticated request rather than a 500. A 500 with the dynamic-evaluation message in the server log means the build check passed only because something suppressed it, which is the failure mode unstable_allowDynamic creates.
What people get wrong
Using unstable_allowDynamic as the fix. This is the most-upvoted answer on most of these threads and it is the wrong tool for this job. It is a glob list that tells the build to stop checking specific files. The official error page is explicit that if those statements are executed on the Edge they will throw at runtime. It exists for the narrow case of a dependency containing unreachable dynamic code that tree-shaking cannot remove. Mongoose in middleware is not that case: it is going to try to open a TCP connection, and there is no TCP.
Adding export const runtime = 'nodejs' to the middleware file. The route-segment runtime export is for pages and Route Handlers. In middleware you need the config object, and in Next.js 16 proxy files the runtime option is rejected with an error because proxy is on Node.js already.
Webpack aliases and fallback: { fs: false }. This tells the bundler to substitute an empty module for something the library genuinely needs. The build goes green and the failure moves to runtime, where it is harder to diagnose. You have not removed the dependency on the filesystem, you have hidden it.
Trusting middleware as the security boundary. Separate from this error, but worth saying while you are in that file. A bypass disclosed as CVE-2025-29927 allowed a crafted request header to skip Next.js middleware entirely, and Next.js's own documentation now advises verifying authentication inside each Server Function rather than relying on the middleware layer alone. Treat middleware as a redirect optimisation. Do the authorisation where the data is.
When it is still broken
- The import is transitive. Your middleware imports
@/lib/auth, which imports your Mongoose models for a type, and the whole graph comes along. Import types withimport type { User } from '@/models/user', which is erased at compile time, instead of a value import. - A Route Handler is pinned to Edge. Search the codebase for
runtime = 'edge'. A file you forgot about a year ago will produce the identical message from a completely different part of the build. - The build passes and production still throws. Something is suppressing the check. Remove every
unstable_allowDynamicentry and rebuild to see the real list of offenders. - You are self-hosting behind a reverse proxy. If all you wanted from middleware was a redirect, a rewrite or rate limiting, do it in nginx instead and delete the middleware. It is faster, it cannot break your build, and the configuration is boring and stable.
The direction of travel is clear enough: Next.js renamed middleware to proxy specifically to discourage treating it as an application layer, and moved it to Node.js by default because almost nobody was getting value from the isolate. If you are still fighting this error on 15.x, the upgrade is likely less work than the workaround.
Frequently asked questions
- Can I use Mongoose or Prisma in Next.js middleware?
- Not on the Edge Runtime. Mongoose fails the build because the Edge bundler resolves its browser build, which contains dynamic code evaluation, and it would fail at runtime anyway because there are no TCP sockets in a V8 isolate. From Next.js 15.5 you can opt middleware into the Node.js runtime and use them, and in Next.js 16 the renamed proxy file runs on Node.js by default.
- Does unstable_allowDynamic fix the Edge Runtime error?
- It only suppresses the build-time check for the files you list. Next.js documentation states that if those statements are reached on the Edge they will throw a runtime error. It is meant for a dependency containing unreachable dynamic code that tree-shaking cannot remove, not for making a Node.js-only library work.
- How do I run Next.js middleware on the Node.js runtime?
- On Next.js 15.5 and later, add export const config = { runtime: 'nodejs' } to middleware.ts. On Next.js 16 middleware has been renamed to proxy and already defaults to the Node.js runtime, so no option is needed and setting a runtime option in a proxy file throws an error.
- What should I use instead of jsonwebtoken in Edge middleware?
- Use jose, which is built on the Web Crypto API and runs in a V8 isolate without Node core modules. Verify only the signature and expiry at the edge, then do any check that needs a database lookup in the Route Handler or Server Component that serves the page.