Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

The Best Database for Next.js Is Almost Always Postgres

Sean

Platform Writer

Sep 09, 2026
9 min read

For the large majority of Next.js applications the best database is Postgres, and the reason has nothing to do with benchmarks. Next.js runs queries in a lot of places at once: server components, route handlers, server actions, and possibly at build time. Whatever database you pick has to survive that access pattern, and the deciding property is how it handles connections, not how fast it returns a row.

The Best Database for Next.js Is Almost Always Postgres

Every list of the best databases for Next.js is the same list of eight products with the same three sentences about each. It is not useless, but it answers the wrong question. The question that matters is what your rendering strategy does to your connection count, and that answer narrows the field far faster than any feature comparison.

Table of contents

The real constraint is connections, not queries

A traditional server app opens a pool of maybe ten connections at startup and reuses them for the process lifetime. Postgres is happy. Everyone goes home.

Next.js can break that assumption in two ways. If you deploy to a serverless runtime, every concurrent invocation is potentially its own process with its own pool, and a hundred concurrent requests can mean a hundred pools rather than one. If you use React Server Components heavily, a single page render can issue queries from several components, and without care each one reaches for the pool independently.

Postgres allocates a backend process per connection. The default max_connections is often 100, and a small managed instance may allow fewer. Exhausting it does not degrade gracefully. It returns an error, and the error appears under load, which is the worst time to discover it.

This single constraint explains most of the database advice in the Next.js ecosystem:

  • Use a pooler in front of Postgres, so the application’s many short connections multiplex onto a few real ones.
  • Keep the client as a module-level singleton so a warm instance reuses it rather than opening a new one per request.
  • Prefer a long-running server over a serverless runtime when the query volume is high, because a persistent process makes the whole problem disappear.
  • Watch the pool size per instance, and multiply it by the instance count before deciding it is safe.

Server Components changed where the query runs

In the app router, an async server component can query the database directly. No API route, no fetch, no serialisation. It is a genuine simplification and it quietly changes the shape of your data access.

// app/orders/page.tsx
import { db } from '@/lib/db';

export default async function OrdersPage() {
  const orders = await db.query.orders.findMany({
    orderBy: (o, { desc }) => [desc(o.createdAt)],
    limit: 50,
  });

  return <OrderTable rows={orders} />;
}

The upside is obvious. The downside is that queries are now scattered across the component tree, and a page with six data-fetching components issues six queries. React deduplicates identical requests within a render pass, which helps, but it cannot merge two different queries that could have been one join.

Practical consequence: put the query in a function in a data-access module rather than inline in the component, even when inline works. It costs nothing, it makes the query greppable, and it gives you somewhere to add caching later without touching the component.

Postgres, MySQL, and the NoSQL detour

Postgres is the default recommendation because it is the least likely to become a constraint. JSONB columns cover the document use case without a second database. Full-text search covers the search use case until you genuinely outgrow it. Row-level locking, real transactions, generated columns, partial indexes and a mature extension ecosystem cover almost everything else.

MySQL is a completely reasonable second choice and is the right one if the team already knows it well. The operational maturity is comparable, the tooling is excellent, and the differences that matter to a typical web application are small. Familiarity beats theoretical advantage.

A document database is the right choice when the data genuinely is documents: variable shape, no meaningful relationships, read by whole object. That is a real category and it is smaller than its popularity suggests. Most application data has relationships, and modelling relationships without joins means writing the joins in application code, which is slower, buggier and harder to change.

The honest test: sketch the three queries your app will run most. If two of them touch more than one entity, you want a relational database.

Prisma, Drizzle, or plain SQL

The ORM question generates more heat than it deserves. All three options work.

  • Prisma. Excellent schema-first workflow, strong migrations, generated types that are genuinely good. The client is heavier, and the generated query is occasionally not the one you would have written.
  • Drizzle. SQL-shaped API, very thin runtime, types derived from a schema you write in TypeScript. Closer to the database, which means fewer surprises and slightly more typing.
  • Plain SQL with a typed query helper. Maximum control, zero abstraction to debug through, and you own the migration story yourself.

The decision that actually matters is not which one, but whether the migration path is version-controlled and runs automatically on deploy. A schema change applied by hand to production is the single most common cause of a deploy that works in staging and fails live.

# The release step that should exist on every deploy
npx prisma migrate deploy
# or
npx drizzle-kit migrate

Where the database should physically live

The most common performance problem in a Next.js app is not the database and not the framework. It is distance.

A server component that issues four sequential queries to a database in another region pays the round trip four times. At 80ms per round trip that is 320ms of pure latency before any work happens, and it will not show up in local development where the database is on localhost.

Three rules that remove most of it:

  1. Put the application and the database in the same region. This is worth more than any query optimisation you will do this year.
  2. Issue independent queries in parallel with Promise.all rather than awaiting them in sequence.
  3. Keep the connection inside a private network rather than going out over the public internet and back.

The parallel point is worth being concrete about, because the sequential version is what people naturally write.

// Sequential: three round trips end to end
const user = await getUser(id);
const orders = await getOrders(id);
const prefs = await getPrefs(id);

// Parallel: one round trip of wall time
const [user, orders, prefs] = await Promise.all([
  getUser(id),
  getOrders(id),
  getPrefs(id),
]);

The decision, condensed

Start with Postgres. Put it in the same region as the application, behind a pooler if the runtime is serverless. Pick whichever query layer the team will actually keep the migrations tidy in. Move the queries into a data-access module. Run migrations as a release step.

That configuration will carry a Next.js application from launch to a genuinely substantial amount of traffic, and every part of it is reversible except the data model, which is the one thing worth spending an afternoon on before you write the first migration.

Everything else on the list of eight products is a solution to a problem you probably do not have yet, and will recognise clearly if you ever do.

How this fits the rest of the stack

Once the choice is made, the cost is the application instance plus the database instance plus whatever storage sits beside them, and those are three separate numbers rather than one. The RunxBuild hosting calculator shows them together so the total is visible before the project commits to a shape. On RunxBuild a Next.js service deploys from a GitHub repository with build logs, environment variables and a live route, and a managed MySQL or Postgres instance runs on the private network next to it, with connection limits, backups and user management handled rather than assembled.

Useful related references:

FAQ

Is Postgres or MySQL better for Next.js?

Both work well and the framework does not care. Postgres has a slight edge on JSONB, full-text search and extensions, which reduces the chance of needing a second datastore later. If the team already knows MySQL, that familiarity is worth more than the feature difference.

Do I need connection pooling with Next.js?

If you deploy to a serverless runtime, yes, effectively always. Each concurrent invocation can open its own connections and a small Postgres instance will run out. On a long-running server process a normal application-level pool is sufficient.

Can server components query the database directly?

Yes, that is one of the main points of the app router. Keep the query in a data-access module rather than inline in the component so it stays greppable and cacheable, and remember that several components each fetching means several queries per render.

Is a NoSQL database a bad choice for Next.js?

Not bad, just narrower. It fits when the data genuinely is variable-shaped documents read as whole objects. If your main queries span more than one entity, you will end up writing joins in application code, which is worse than letting the database do them.

What is the biggest performance mistake with a Next.js database?

Putting the database in a different region from the application, followed closely by awaiting independent queries in sequence instead of running them with Promise.all. Both are invisible in local development and both cost hundreds of milliseconds in production.

#best database for nextjs#nextjs postgres#prisma#drizzle orm#connection pooling