I mentioned this briefly in an earlier post on middleware patterns, that direct database queries don't run reliably in middleware, and a few people asked what "not reliably" actually means in practice, since it sounds vague enough to ignore. It's worth a real, specific answer, because the actual failure mode here is genuinely easy to miss until it costs you something.
Why This Happens
Next.js middleware runs on the Edge Runtime by default, a deliberately restricted environment built for speed, running geographically close to the visitor rather than in one central server location. That speed comes from real constraints, the Edge Runtime doesn't support the full Node.js API surface, no fs module, limited or no support for certain networking and crypto APIs that Node.js provides natively.
Mongoose, and the underlying MongoDB Node.js driver it depends on, was built assuming a full Node.js environment. It uses APIs and networking behavior that the Edge Runtime doesn't fully support.
What This Actually Looks Like
// middleware.ts
import { connectDB } from '@/lib/db';
import User from '@/models/User';
export async function middleware(request: NextRequest) {
await connectDB();
const user = await User.findOne({ /* some lookup */ });
// ...
}
Depending on your exact Next.js version, deployment platform, and what specifically the query does, this can fail in a few different ways, and none of them are a clean, obvious "Mongoose doesn't work here" error message. Sometimes it's a build-time error about an unsupported Node.js API being used. Sometimes it's a runtime error only surfacing in production, not locally. Sometimes, depending on caching and how the request happens to be handled, it appears to work in casual testing and then behaves inconsistently under real load or on a different deployment region. The inconsistency itself is what makes this dangerous, a bug that fails the same way every time is far easier to catch than one that only sometimes fails.
The Actual Fix: Opt Into the Node.js Runtime, or Move the Query Out
Next.js lets middleware explicitly opt into the full Node.js runtime instead of the Edge Runtime default, though this comes with real tradeoffs worth understanding, not just applying blindly.
// middleware.ts
export const config = {
runtime: 'nodejs', // opts out of Edge Runtime, restores full Node.js API support
};
export async function middleware(request: NextRequest) {
await connectDB();
const user = await User.findOne({ /* ... */ });
// ...
}
This genuinely fixes the compatibility problem, Mongoose now runs in an environment it actually supports. The tradeoff is losing the specific performance benefit the Edge Runtime provides, middleware no longer runs at the network edge closest to each visitor, it runs in a more traditional serverless environment, which can mean measurably slower middleware execution for a globally distributed audience.
The Better Fix, Usually: Don't Query the Database in Middleware at All
For the specific, extremely common case of an auth check, the better pattern avoids this tradeoff entirely by never needing a database query in middleware in the first place.
// middleware.ts (Edge Runtime, no database dependency at all)
import { verifyToken } from '@/lib/auth'; // verifies a signed JWT, no DB call needed
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
const session = token ? verifyToken(token) : null;
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
A signed JWT can be verified using just its signature, a cryptographic check that doesn't require a database round trip at all, and that verification, done correctly, runs fine in the Edge Runtime. The actual database-backed check, confirming a user still exists, checking permissions that could have changed since the token was issued, belongs in the layout or page itself, which runs in the normal Node.js environment without any Edge Runtime restrictions.
// app/(dashboard)/layout.tsx (normal Node.js runtime, database queries fully supported)
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await getSession(); // JWT already verified by middleware
await connectDB();
const user = await User.findById(session.userId); // safe here, full Node.js runtime
// ...
}
This gets you the actual benefit middleware is good for, fast, edge-based routing decisions, without ever asking it to do something it was never built to support reliably.
The Actual Rule
Middleware should make routing decisions based on data it can access without a database, cookies, headers, a verified token's payload. Anything that genuinely needs a database lookup belongs in a layout, page, Server Action, or route handler, which all run in the full Node.js environment by default, not in middleware, which runs on the Edge Runtime unless explicitly configured otherwise, and even then, carries a real performance tradeoff for opting out.
Check Your Own Middleware
If your middleware.ts imports anything that eventually calls Mongoose, or any other database driver built for a full Node.js environment, that's worth checking today, especially if it's not throwing an obvious, consistent error, since the inconsistent version of this failure is the one that actually causes real production incidents nobody can easily reproduce.
If you've hit this, or something like it, in production, genuinely curious what the actual failure looked like on your end, a clean error, or something stranger and harder to pin down. Drop it in the comments.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)