Skip to content
DERKONLINE

Stop Exhausting MySQL Connections in Next.js Dev Reloads

Hot reload re-creates module-scoped pools until MySQL caps out; stash the pool on globalThis so reloads reuse one set.

Derrick S. K. Siawor6 min read

You are deep in a feature, saving a file every few seconds, when the dev server starts throwing ER_CON_COUNT_ERROR: Too many connections. You restart MySQL, it works for a few minutes, then it happens again. Nothing in your code opened a thousand connections. And yet the database is out of them. This is one of the most confusing bugs in Next.js development because the cause is invisible: it is not your code, it is the way hot reload re-runs your code.

The good news is the fix is three lines and it has been the standard answer for years. Once you understand why the connections stack up, the pattern that stops them is obvious, and it costs nothing in production.

Why hot reload exhausts the pool

In development, Next.js uses Fast Refresh. When you save a file, it re-evaluates the affected modules so your change shows up without a full restart. That is the feature you want. The problem is what happens to anything created at module scope.

If you create a database pool at the top level of a module, like this, every hot reload re-runs that module and creates a brand new pool:

// db.ts: the version that breaks
import mysql from "mysql2/promise";

export const pool = mysql.createPool({
  host: process.env.DB_HOST,
  connectionLimit: 10,
});

The old pool does not get cleaned up. Its connections are still open, still counted by MySQL, just orphaned because nothing references them anymore. Save the file ten times in a session and you have ten pools, each holding up to its connection limit, and MySQL's max_connections ceiling arrives fast. This is the same mechanism behind every "database client stacking in dev mode" report: the module-scoped instance is re-created on each reload and the previous one leaks, the dev-time sibling of the memory leaks that make a long-running SPA slower by the hour.

In production this never happens, because there is no Fast Refresh re-running modules. The module evaluates once, the pool is created once, and it lives for the life of the process. So the bug only bites during development, which is exactly when it is most disorienting, because the code that "works in production" is silently broken on your own machine.

The fix: stash the pool on globalThis

The reason a fresh pool is created on every reload is that module scope gets re-evaluated. The trick is to store the pool somewhere that survives re-evaluation. In Node, that place is globalThis. It is not reset by Fast Refresh, so a value parked on it persists across reloads, and the next reload reuses the existing pool instead of building a new one.

// db.ts: the version that holds
import mysql, { type Pool } from "mysql2/promise";

const globalForDb = globalThis as unknown as { pool?: Pool };

export const pool =
  globalForDb.pool ??
  mysql.createPool({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    connectionLimit: 10,
  });

if (process.env.NODE_ENV !== "production") {
  globalForDb.pool = pool;
}

Read it top to bottom. On the first evaluation, globalForDb.pool is undefined, so a pool is created. On every subsequent hot reload, globalForDb.pool is already set, so the ?? short-circuits and the same pool comes back. One pool, reused forever, no matter how many times you save.

Hot reload flow: reuse existing globalThis pool or create one, attach only outside production

The NODE_ENV !== "production" guard is the detail people skip. You only need to attach to globalThis in development, because production never re-evaluates the module. Guarding it keeps the production path clean and signals intent: this is a dev-reload workaround, not architecture. The same exact pattern works for any client that holds connections, an ORM client, a Redis client, a queue connection. Anything you instantiate once at module scope and never want re-created should live on globalThis in dev.

Make it a reusable helper

Once you have written this twice, factor it into one helper so every connected client gets the same protection without copy-paste. The shape is "give me a name and a factory; reuse the global if it exists, otherwise create and store it."

function singleton<T>(name: string, factory: () => T): T {
  const g = globalThis as Record<string, unknown>;
  if (process.env.NODE_ENV === "production") return factory();
  if (!g[name]) g[name] = factory();
  return g[name] as T;
}

export const pool = singleton("dbPool", () =>
  mysql.createPool({ /* ... */ })
);

In production it just calls the factory and returns, no global involved. In development it caches by name on globalThis, so reloads reuse it. We keep one of these in every project's database layer because it makes the right behavior the default, and it stops a whole category of confusing local crashes before anyone hits them.

What this does not fix, and what to check next

The globalThis pattern stops the leak from hot reload. It does not fix a genuinely undersized pool or a query path that holds connections open longer than it should. If you still see connection pressure after applying it, look at three things.

First, your connectionLimit against your database's max_connections. The pool will never exceed its limit, but if you run several services against one database in dev, the combined limits can still add up beyond what MySQL allows. This is the same database-latency lens worth applying broadly, since a slow query path can quietly drag down your whole API the same way an exhausted pool does. Second, queries that acquire a connection and forget to release it, usually a missing release in an error path, which slowly drains the pool until it deadlocks. Often the real culprit is a query pattern doing far more work than it should, which is why hunting down N plus one queries is worth the time. Third, long-running transactions that hold a connection for the duration. None of those are the hot-reload bug, but they wear the same Too many connections mask, so confirm the pool is genuinely a singleton first, then look at usage.

Small fix, real difference

This is a three-line change, but it is the kind of detail that separates a development setup that fights you from one that gets out of your way. Nobody should have to restart MySQL because they saved a file too many times. Pin the pool to globalThis in dev, guard it out of production, factor it into a helper, and the problem is gone for the life of the project.

We bake this into the database layer of every web app we build, the same way we wire up one-command stack bootstrap, healthchecks that kill startup races, and parameterized queries. It is not a clever trick to remember under pressure. It is a default that makes the local environment behave, so the only bugs you chase are real ones in your code, not artifacts of the reloader. The same care we put into a connection pool on a laptop is the care we put into how we run the database in production, where the cost of a leak is measured in downtime instead of an annoyed afternoon.