MySQL Too Many Queries: Symptoms, Diagnosis & Fixes

The database isn't slow — it's drowning. Thousands of tiny, instant queries, all arriving at once.

What It Is

There are two completely different ways a database can make your site feel slow, and confusing them costs people whole afternoons. The first is one query that takes too long — a missing index, a JOIN across a million rows. That's a slow query, with its own page. This page is the sneakier one: every query is individually fast, a fraction of a millisecond, and the server is still on its knees, because there are simply too many of them, arriving too fast, all at once.

Picture a checkout counter. A slow query is one customer with a cart of four hundred items, holding up the line. A high query rate is four hundred customers each buying a single pack of gum, all in the same thirty seconds. Each transaction is trivial. The cashier is fine. The line is the disaster. MySQL is that cashier — astonishingly fast at any single query, and helpless against a flood of them.

The most important sentence here: a high query rate is almost never a database problem. The database is the victim. Something above it — the application, a loop, a missing cache, a bot — is generating far more queries than the work requires. So while everything below is framed around MySQL and MariaDB (the same engine for our purposes — MariaDB is the community fork, every command works on both), the fix will almost always live in your application code, not a my.cnf knob. The most tempting fix on this page — raise max_connections — is the one we'll spend the end warning you away from.

How You Notice

A query flood shows up in a handful of places:

  • "Too many connections." The signature error, landing in your application log and your inbox at the same time:

    ERROR 1040 (HY000): Too many connections
    

    Every new request that needs the database gets turned away at the door. The site doesn't slow down gracefully — it falls off a cliff, because connection number max_connections + 1 simply can't get in.

  • Threads_connected and Threads_running climbing. These two numbers are the database's pulse — the first counts open connections, the second the ones actually doing work this instant:

    mysqladmin extended-status | grep -E "Threads_connected|Threads_running"
    

    On a healthy box, Threads_running sits in the low single digits — most connections are idle, waiting for the app to send them something. When it climbs into the dozens and stays there, the database has more simultaneous work than CPU cores to do it.

  • Latency only under load. The page that's instant when you test it alone crawls the moment real traffic arrives, yet nothing in the slow-query log looks wrong. That's the tell that separates a rate problem from a slow query: a slow query is slow always; a rate problem is fine until the crowd shows up.

  • CPU pinned on lots of tiny things. In top the mysqld process eats a core or two, yet no single query is the offender — the cost is in the sheer count. Sustained, this is a classic feeder of high load.

Any one of these means: stop hunting for the slow query, because there isn't one. Count the queries instead.

How I Diagnose It

The whole diagnosis rests on one number, and MySQL has been keeping it for you all along. Ask how many statements it has run since startup:

mysql -e "SHOW GLOBAL STATUS LIKE 'Questions'"
+---------------+------------+
| Variable_name | Value      |
+---------------+------------+
| Questions     | 1843920117 |
+---------------+------------+

That number alone is meaningless — a lifetime total, billions deep on any long-running box. The magic is the rate of change. A counter is just a number; a counter sampled twice is a velocity. Read it, wait ten seconds, read it again, divide the difference by ten — that's your queries per second, the metric this whole page is about:

mysql -e "SHOW GLOBAL STATUS LIKE 'Questions'"; sleep 10; mysql -e "SHOW GLOBAL STATUS LIKE 'Questions'"

(Questions counts statements the client sent; Queries also folds in those fired inside stored procedures. For most apps, use Questions — it maps to "things my application asked for.") A small site might do 20–100 QPS. If two readings show several thousand and your user traffic hasn't multiplied to match, you've found it.

Note

There's no universal "too many" number — a beefy box serves tens of thousands of QPS all day, a tiny one chokes at a few hundred. The signal isn't the absolute rate; it's the mismatch between the query rate and the real work being done. Ten thousand QPS to render a page showing fifty rows is the smell, whatever the hardware.

Once you know the rate is high, the next question is which queries — and the answer is almost always "the same one, over and over." Catch them in the act:

mysql -e "SHOW FULL PROCESSLIST"
Id      User  Host             db    Command  Time  State  Info
501012  app   10.0.0.5:51112   shop  Query    0     init   SELECT * FROM products WHERE id = 8842
501013  app   10.0.0.5:51114   shop  Query    0     init   SELECT * FROM products WHERE id = 8843
501014  app   10.0.0.5:51116   shop  Query    0     init   SELECT * FROM products WHERE id = 8844

Stare at that, because it's the whole story in three lines. Same table, same shape, id ticking up one at a time, every Time reading 0. That is a textbook N+1 query pattern: the application fetched a list, then walked it row by row, firing one query per item instead of asking for them all at once. The processlist is the crime scene, and the repeated statement is the smoking gun. Run it a few times — if you keep seeing the same template with a marching parameter, you've found the exact spot in your code.

For which templates dominate over a window of time, the real tool is pt-query-digest from Percona Toolkit, run against the general or slow log with the threshold dropped to zero. It groups thousands of statements back into templates and ranks them by count, so "this harmless little SELECT ran 1.2 million times in five minutes" jumps off the page. (Turn the general log on only briefly with SET GLOBAL general_log = 'ON' and off the moment you've sampled — it logs every statement and fills a disk fast.)

Last, compare MySQL's query rate against your web traffic. If nginx or Apache shows 50 requests a second while MySQL shows 8,000, that's 160 queries per page — and that ratio is what you're trying to shrink. A healthy app keeps it in the low single or double digits.

How to Fix It

The fixes line up against four classic causes, and they share one theme: you reduce the number of queries, you almost never tune the database. In rough order of how often they're the real answer:

  • Fix the N+1 in the application. The big one. Your code loops over a result and queries inside the loop: one SELECT to fetch fifty orders, then fifty more to fetch each order's customer. Collapse all fifty-one into one — a JOIN, or a single WHERE id IN (…) with the whole list. With an ORM (Hibernate, Eloquent, Active Record, Django), that means turning on eager loading for the relationship you're walking. One good JOIN can turn 8,000 QPS back into 80.

  • Add a cache layer. The best effort-to-payoff ratio on the page. A startling share of those queries re-fetch data that hasn't changed since the last request — the same category list, the same config row, read fresh from disk every page view. Put a cache in front: an in-process application cache, or a shared store like Redis or Memcached — purpose-built key-value stores that answer in microseconds and take the read load off MySQL entirely. If a query's answer is the same for thousands of requests in a row, it should hit a cache, not the database.

  • Use connection pooling. If your app opens a fresh connection per request and closes it after, you pay MySQL's handshake (auth, TLS, thread setup) thousands of times a second, on top of the real queries — and under a spike that becomes a reconnect storm piling straight into "Too many connections." A connection pool keeps a small set of warm connections open and lends them out. Most frameworks ship one or have one a library away; in front of MySQL itself, a proxy like ProxySQL pools server-side.

  • Rate-limit the abusive endpoint. Sometimes the flood isn't your logic — it's a crawler hammering one expensive URL (your search box, a faceted filter) hundreds of times a second, each hit fanning out into dozens of queries. Cap it at the front door: a limit_req zone in nginx or a rate limit in HAProxy throttles a single IP before it reaches the database. It's also your first line against a clumsy denial-of-service attempt.

Warning

When "Too many connections" is staring at you, the obvious move is to crank max_connections higher and restart. Resist it as anything but a stopgap. Raising the ceiling without fixing the cause just moves the wall back — and a worse wall, because every extra connection costs real memory, and 2,000 connections all running their tiny queries at once can starve the box of RAM and CPU and take it down harder than the original error did. A higher max_connections lets more of the flood through; it does not make the flood smaller.

So the order is: cut the queries (N+1, then cache), stop the reconnect churn (pool), throttle the abusers (rate-limit) — and only then, if the numbers still justify it, give MySQL more room, sized to your RAM rather than to make an alert stop. Be honest about which step you're on. Reaching for max_connections first is the move that feels like progress and isn't.

How to Avoid It

You don't prevent a flood by tuning MySQL; you build the app so the flood can't form. Three habits, and a fourth that's the point of this whole site:

  1. Cache by default. Ask "does this need to hit the database every time?" while writing the code, not after the incident. Data that changes rarely and reads often is cache-shaped — reach for Redis or Memcached before the traffic teaches you to.

  2. Batch, don't loop. When you're about to query inside a loop — stop, that's the N+1 forming. Fetch the whole set in one JOIN or IN (…). Most ORMs will, the moment you ask; the trap is that they default to lazy, and lazy is N+1 with a friendly face.

  3. Pool your connections. Set it up once, at the start of the project, and you never meet the reconnect storm at all.

  4. Watch QPS as a first-class metric. A single SHOW GLOBAL STATUS LIKE 'Questions' tells you nothing; the same reading plotted over time turns a slow creep into a line you can see bending upward weeks before it becomes an outage. An N+1 a new feature quietly introduced doesn't announce itself — it just nudges the QPS graph up, and keeps nudging, until one busy afternoon it tips you over max_connections.

Pro Tip

The fastest sanity check there is: read Questions and Threads_connected ten seconds apart, divide the first delta by ten for your QPS, and glance at the second against max_connections. Two numbers, ten seconds, and you know instantly whether you're looking at a rate problem or chasing the wrong ghost.

Why It Floods: The Cost of a Question

Here's the part you don't need mid-incident but that makes every number above click: why should fast queries, in bulk, hurt at all?

Because a query is never just its execution. Every SELECT, however trivial, drags a procession behind it. The connection is authenticated. The SQL text is parsed into a tree. The optimizer decides how to run it — which index, which order — even for a one-row lookup it has done a million times. A thread is scheduled onto a core; locks are taken and released; the result is serialized back over the network. Each step is cheap. Multiply by ten thousand a second and the overhead of merely handling the question swamps the cost of answering it.

And there's a hard ceiling underneath. MySQL runs each connection's work on a thread, and your server has a fixed, smallish number of CPU cores. When Threads_running exceeds the core count, threads stop running in parallel and take turns, the OS rapidly switching the CPU between them. Past a point the switching itself costs more than the work, and total throughput actually drops as you add concurrency. The line gets longer and the cashier gets slower. That's the cliff behind "fine until the crowd shows up": comfortably under the core count, until one spike or one N+1 loop pushes you over and the system tips into thrashing in seconds.

This is also why a cache is so disproportionately powerful. A cache hit skips the entire procession and answers from memory in microseconds — it doesn't make those 10,000 queries faster, it makes them never happen. The deepest fix for "too many queries" is, almost poetically, to ask fewer of them. The database was never the problem — the number of times you reached for it was.

See Also

  • MariaDB — the engine itself: config, my.cnf, and where max_connections lives
  • Redis — the cache layer that makes most repeated queries disappear
  • Memcached — the other classic in-memory cache for read-heavy workloads
  • database — what a relational database does under the hood, from zero
  • key-value store — the data model behind the caches above
  • high load — what a query flood does to the rest of the box
  • database issue — the broader family of database problems
  • poor performance — when the app is slow and you're not sure which layer to blame
  • denial-of-service — when the flood is hostile rather than accidental
  • nginx — where to rate-limit the abusive endpoint before it reaches MySQL
  • HAProxy — the other front door for throttling and connection control

Is your database actually slow — or just being asked the same question ten thousand times a second?

CleverUptime watches the load and CPU a query flood drives up, flags when mysqld is eating your cores, and tells you in plain language whether you're looking at a runaway query rate or one genuinely slow statement — so you fix the loop in your app instead of cranking a database knob that only moves the wall.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →