MySQL logo

There is a specific way MySQL monitoring goes wrong: the replication lag graph reads zero, everything looks healthy, and the replica has not received a byte in six hours. It is not a bug in your dashboard — it is what Seconds_Behind_Source reports when the wrong thread has died.

This guide covers the MySQL and MariaDB counters worth watching, the traps in reading them, and how to collect them with Bleemeo.

What MySQL is, briefly

MySQL is an open-source relational database first released in 1995, and still the most common database behind web applications — Twitter, Facebook and YouTube among them. Oracle acquired it in 2010, at which point co-founder Michael Widenius forked the project as MariaDB. Almost everything below applies to both; where the two diverge, the variable names changed but the concepts did not.

MySQL 8.0 also renamed the replication status fields from Master/Slave to Source/Replica. Both spellings appear here because both are still in the wild.

The metrics that matter

Connections, and the ceiling you hit at 3 a.m.

Threads_connected against max_connections is the headroom. Reach the limit and MySQL refuses new clients — including the connection you wanted for diagnosing it. MySQL reserves one slot for SUPER, which is the only reason those incidents are recoverable at all.

Three counters tell the story better than the gauge:

  • Max_used_connections — the high-water mark since startup. If it is close to max_connections, you have already come near the ceiling, whatever the current value says.
  • Connection_errors_max_connections — clients actively refused because the limit was reached. Should be zero. Any non-zero value is a past outage you did not notice.
  • Aborted_clients versus Aborted_connects — the first is clients that vanished without closing cleanly (often a pool with a shorter idle timeout than wait_timeout), the second is failed handshakes: wrong credentials, or a host that cannot resolve.

The InnoDB buffer pool

This is MySQL’s memory story. The buffer pool caches data and index pages; when it is too small for the working set, every query starts touching disk.

Read hit ratio as 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests): read_requests counts logical reads, reads counts the ones that had to go to disk. Above 99% is typical for a well-sized pool on an OLTP workload.

Also watch Innodb_buffer_pool_pages_free. A pool that has been at zero free pages since startup is not necessarily unhealthy — it filled up and is doing its job — but combined with a falling hit ratio it is a clear sizing signal.

Ignore the query cache entirely. It was deprecated in 5.7 and removed in 8.0, and dashboards still graphing Qcache_hits are measuring a feature that no longer exists.

Query quality, without a slow-query log

Two counter pairs tell you about bad queries without parsing any logs:

  • Created_tmp_disk_tables against Created_tmp_tables. MySQL builds temporary tables in memory until they exceed tmp_table_size, then writes them to disk. A high disk ratio means sorts and grouping are spilling — either tmp_table_size is too small or the queries are returning too much.
  • Select_scan and Select_full_join. Full table scans and joins with no usable index. A rising Select_full_join after a deploy is almost always a missing index on a new query path.

Slow_queries counts statements exceeding long_query_time. Useful, but only if long_query_time is set to something meaningful — the default of 10 seconds catches catastrophes and misses everything that matters. One second is a more informative setting.

Locks and contention

  • Innodb_row_lock_waits and Innodb_row_lock_time_avg. Row-level contention. A rising average wait means transactions are queuing behind each other, usually because something holds a lock longer than it should.
  • Table_locks_waited. On InnoDB this should be near zero; if it is not, something is still using MyISAM, or a LOCK TABLES crept into application code.

Replication, and the metric that lies

This is the one to get right. On a replica, three things must be true, and lag alone covers only one of them:

  1. Slave_IO_Running / Replica_IO_Running is Yes — the replica is receiving from the source.
  2. Slave_SQL_Running / Replica_SQL_Running is Yes — the replica is applying what it received.
  3. Seconds_Behind_Master / Seconds_Behind_Source is low.

The trap: that third value is computed from the applied relay log. If the I/O thread dies, the replica stops receiving new events, faithfully finishes applying the ones it already has, and then reports zero seconds behind — because it has caught up with everything it knows about. The graph flatlines at healthy while the data ages.

So alert on the two threads, not just the lag. And note the value is NULL when replication is stopped, which a naive threshold check reads as “not greater than 30” and passes.

Collecting it with Bleemeo

Once the agent is installed, it discovers MySQL and connects on its own in the common cases.

How the Bleemeo agent discovers and collects from MySQL

Where MySQL needs explicit credentials, the options are in the documentation. Grant the monitoring user only PROCESS, REPLICATION CLIENT and SELECT on the schemas it needs — enough for every counter above, and nothing more for a credential that sits in a config file.

The service dashboard you get for free

Once the agent can reach MySQL, a default dashboard appears:

The MySQL service dashboard created automatically by Bleemeo

Building the dashboard you actually want

A custom dashboard for what the default one does not show. The example below graphs:

  • Number of queries per second
  • Number of threads connected, running and cached
  • Status of MySQL
  • Number of selects per second
A custom Bleemeo dashboard with additional MySQL metrics

Threads connected against threads running is a more useful pair than either alone: connected tells you how many clients are attached, running tells you how many are actually doing work. A wide and widening gap means clients are queuing.

You can mix in anything else Bleemeo collects. For a containerised database:

  • Status of the MySQL container
  • Memory used by applications in percent
  • CPU used in percent
A Bleemeo dashboard combining MySQL and host metrics

Which is where a single screen starts earning its keep: buffer pool hit ratio next to host memory explains a slowdown that neither number explains alone.

Which of these deserve an alert

Alert on Why
Threads_connected above 80% of max_connections At the ceiling you cannot connect to diagnose
Connection_errors_max_connections above zero Clients were refused; it already happened
Slave_IO_Running or Slave_SQL_Running not Yes Lag reads healthy while replication is dead
Replication lag above your staleness budget Only meaningful alongside the two threads
Created_tmp_disk_tables ratio climbing Queries are spilling sorts to disk
Innodb_row_lock_time_avg rising Transactions queuing on each other

Queries per second is a dashboard line. It describes your traffic, not your database, and an idle application and a broken one draw the same graph.

MySQL monitoring FAQ

Why does my replica report zero seconds behind when replication is broken?

Because Seconds_Behind_Source measures how far behind the replica is in applying its relay log. If the I/O thread dies, no new events arrive, the SQL thread finishes what it already had, and the replica correctly reports that it has caught up — with a source it stopped listening to. Always check Replica_IO_Running and Replica_SQL_Running as well; note also that the lag value is NULL when replication is stopped, which a "greater than 30" threshold silently passes.

What is a good InnoDB buffer pool hit ratio?

Above 99% on an OLTP workload, computed as 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests). Read it together with free pages: a pool at zero free pages is normal, but a pool at zero free pages and a falling hit ratio means the working set has outgrown it.

Should I still monitor the MySQL query cache?

No. It was deprecated in 5.7 and removed entirely in 8.0. Dashboards still graphing Qcache_hits are charting a feature that no longer exists — the buffer pool is where MySQL's caching story is now.

Does any of this differ for MariaDB?

The concepts are identical and most counter names are too. MariaDB kept the Slave_* spelling where MySQL 8.0 moved to Replica_*, and the two have diverged on some InnoDB internals — but connections, buffer pool, temp tables, locks and the three-part replication check all read the same way.