Database Performance

MySQL Connection Limits and Max Connections: Tuning for Scale

Understand MySQL max_connections, thread caching, and connection pool sizing to handle high-concurrency workloads

JusDB Team
September 20, 2022
11 min read
141 views

At 2 AM, your on-call engineer receives a flood of alerts: applications are throwing ERROR 1040: Too many connections, users are getting errors, and the MySQL server is refusing new connections entirely. This is one of the most common and most preventable production incidents in MySQL operations. Connection limit misconfiguration quietly accumulates technical debt until it collapses under load — and when it goes, it goes fast. Understanding how MySQL manages connections, how to size max_connections correctly, and how to instrument your system before crisis strikes is foundational knowledge for any DBA running MySQL at scale.

TL;DR
  • MySQL's max_connections default (151) is almost always too low for production workloads — calculate the right value using your available RAM and per-connection memory overhead.
  • Thread caching via thread_cache_size dramatically reduces the cost of connection churn; watch Threads_created to know if your cache is working.
  • Connection poolers like ProxySQL multiplex thousands of app connections onto a smaller set of real MySQL threads, solving the impedance mismatch between stateless apps and stateful DB connections.
  • Per-user limits (MAX_USER_CONNECTIONS) and timeout variables (wait_timeout, interactive_timeout) are your first line of defence against connection leaks.
  • Monitor SHOW STATUS LIKE 'Threads_%' and Max_used_connections continuously — not just when things break.

The Connection Problem

Every application that talks to MySQL opens a TCP connection. Simple enough at small scale. The problem emerges when you have dozens of application servers, each running a pool of threads, each thread holding a persistent connection open "just in case." A modest deployment — 10 app servers, 20 threads each, 5 connections per thread pool — can exhaust MySQL's default max_connections = 151 before you've even deployed your first feature flag.

The failure mode is brutal. Once MySQL hits the connection ceiling, new connection attempts are rejected immediately. Your application doesn't get a slow response — it gets an error. Connection pools on the application side start stacking up waiters, timeouts cascade, and what started as a connection limit breach rapidly looks like a full application outage.

Compounding the problem: MySQL connections are not free. Each connection consumes RAM for its thread stack, sort buffers, read buffers, and join buffers. Blindly cranking max_connections to 10,000 doesn't solve the problem — it trades connection errors for memory exhaustion and OOM kills. The solution requires understanding the real economics of MySQL connections.

Warning

The Too many connections error (ER_CON_COUNT_ERROR) is thrown even to users with the SUPER privilege — except one reserved connection is kept for administrative access. Never set max_connections so high that you cannot connect to diagnose the problem itself.

How MySQL Handles Connections

When a client connects to MySQL, the server spawns a dedicated thread to service that connection. This one-thread-per-connection model (the default in MySQL 5.7+, and still the norm in MySQL 8.x for most deployments) means that every active connection carries thread overhead regardless of whether that connection is actually executing a query.

MySQL maintains several internal structures per connection:

  • Thread stack: controlled by thread_stack, defaulting to 1MB on 64-bit systems.
  • Sort buffer: sort_buffer_size (default 256KB, allocated per-sort operation, not per connection at startup).
  • Read buffer: read_buffer_size (default 128KB, allocated on demand).
  • Join buffer: join_buffer_size (default 256KB, allocated per join without index).
  • Net buffer: net_buffer_length for network I/O.

The per-connection memory footprint at rest is roughly 1–2MB per connection for the thread stack and fixed allocations, but can spike to 10–20MB+ per connection during heavy query execution when session buffers are actively allocated. This is why the connection memory calculation matters so much for capacity planning.

You can observe all live connections at any time:

text
SHOW PROCESSLIST;

Or for a less truncated view of the query column:

text
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
ORDER BY time DESC;

Sizing max_connections

The canonical formula for calculating a safe max_connections ceiling is:

text
max_connections = (available_RAM_MB - innodb_buffer_pool_size_MB - key_buffer_size_MB) / connection_memory_MB

In practice, assume roughly 4–8MB per connection when session buffers are in active use. For a server with 32GB RAM, 20GB InnoDB buffer pool, and ~1GB for other global structures:

text
available = 32768 MB - 20480 MB - 1024 MB = 11264 MB
max_connections = 11264 / 6 MB ≈ 1877

# Conservative estimate — leave headroom for the OS and other overhead
max_connections = 1500  # safe target for this configuration

To check your current setting and the historical high-water mark:

text
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Max_used_connections';

If Max_used_connections is within 80% of max_connections, you are approaching saturation and need to act before load increases further.

You can change max_connections dynamically without a restart:

text
SET GLOBAL max_connections = 500;

But always persist the change to your configuration file:

text
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
max_connections = 500
Tip

MySQL 8.0 introduced connection_memory_limit (per-session) and global_connection_memory_limit to cap memory consumption from connections at the server level. These are valuable guardrails on 8.0+ deployments alongside max_connections.

Thread Cache: Reducing Connection Overhead

Creating and destroying OS threads is expensive. MySQL's thread cache (thread_cache_size) keeps recently-closed threads in a pool so they can be reused by incoming connections, avoiding the overhead of thread creation on every new connection.

The key monitoring variable is Threads_created. If this value grows rapidly over time, threads are not being served from cache and you are paying thread-creation cost on every new connection:

text
SHOW STATUS LIKE 'Threads_%';

-- Example output:
-- Threads_cached    12
-- Threads_connected 48
-- Threads_created   9823   <-- high and growing = cache misses
-- Threads_running   7

To calculate the thread cache hit ratio:

text
-- Cache miss rate (lower is better)
SELECT (Threads_created / Connections) * 100 AS thread_cache_miss_pct
FROM (
  SELECT
    (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Threads_created') AS Threads_created,
    (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Connections') AS Connections
) t;

A cache miss rate above 1–2% indicates thread_cache_size is too low. Tune it to match your typical peak concurrency:

text
[mysqld]
thread_cache_size = 100   # for workloads with up to ~100 concurrent connections

Threads_running — the count of threads actually executing queries right now — is the most actionable real-time metric. A spike in Threads_running is a leading indicator of query pile-up and often precedes a Too many connections event.

Per-User Connection Limits

When a single runaway application or a misconfigured service is monopolising connections, MySQL's per-user resource limits are your surgical instrument. You can cap connections for any user account without touching max_connections:

text
-- Set at account creation
CREATE USER 'app_readonly'@'%'
  IDENTIFIED BY 'password'
  WITH MAX_USER_CONNECTIONS 50;

-- Modify an existing account
ALTER USER 'app_service'@'%'
  WITH MAX_USER_CONNECTIONS 200;

-- Verify current limits
SELECT user, host, max_user_connections
FROM mysql.user
WHERE max_user_connections > 0;

Connection leaks from applications that fail to close connections on error paths are a common culprit for hitting limits unexpectedly. Two timeout variables help reclaim these idle connections automatically:

text
[mysqld]
# Disconnect non-interactive connections idle for more than 300 seconds
wait_timeout = 300

# Disconnect interactive sessions (mysql CLI) idle for more than 3600 seconds
interactive_timeout = 3600
Warning

Setting wait_timeout too aggressively (under 30 seconds) can cause application-side connection pool errors when pooled connections are closed by the server before the pool detects them as stale. Ensure your connection pool's validationQuery or equivalent keep-alive interval is shorter than wait_timeout.

Connection Pooling: Solving the Impedance Mismatch

Modern stateless application architectures create a structural problem: hundreds of application processes or goroutines each want a connection "available immediately," but MySQL connections are expensive stateful resources. Application-side connection pooling (HikariCP, c3p0, SQLAlchemy pool) helps, but it only solves the problem within a single process. Across dozens of application servers, you still accumulate thousands of real MySQL connections.

A connection multiplexer/proxy sits between your application tier and MySQL and handles this at the infrastructure level. The two dominant options are:

  • ProxySQL: A high-performance MySQL proxy with connection multiplexing, query routing, read/write splitting, and query caching. It maintains a small pool of real MySQL connections and fans out thousands of application connections onto them using connection multiplexing.
  • MySQL Router: Lighter-weight, designed for InnoDB Cluster topologies, handles failover and read/write routing but offers less connection multiplexing than ProxySQL.

A ProxySQL multiplexing configuration example:

text
-- In ProxySQL admin interface
-- Set the maximum real MySQL connections ProxySQL will open
UPDATE mysql_variables SET variable_value = '200'
  WHERE variable_name = 'max_connections';

-- Configure a backend MySQL server
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections)
  VALUES (1, '10.0.0.10', 3306, 200);

LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;

With ProxySQL in place, your MySQL server might see 200 real connections while 5,000 application connections are handled transparently by the proxy. This is the architecture that makes high-concurrency MySQL workloads tractable.

Tip

ProxySQL's connection multiplexing works best with short-lived, stateless queries. Connections using session state (temporary tables, session variables, locking) are not multiplexable and will maintain dedicated backend connections. Design your application accordingly if you intend to rely heavily on connection multiplexing.

Monitoring Connection Health

The following queries form the foundation of a connection monitoring routine. Run them from a monitoring tool (Prometheus MySQL exporter, Datadog, etc.) on short intervals — 15 to 30 seconds — rather than waiting for an incident to check them manually.

text
-- Current connection utilisation percentage
SELECT
  @@max_connections AS max_connections,
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Threads_connected') AS current_connections,
  ROUND(
    (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Threads_connected')
    / @@max_connections * 100, 2
  ) AS utilisation_pct;

-- Historical peak connections
SHOW STATUS LIKE 'Max_used_connections';

-- Current active query threads (leading indicator of saturation)
SHOW STATUS LIKE 'Threads_running';

-- Connection aborts (failed handshakes / authentication failures)
SHOW STATUS LIKE 'Aborted_connects';

-- Full connection status snapshot
SHOW STATUS LIKE 'Threads_%';
SHOW STATUS LIKE 'Connection%';

Key alert thresholds to configure:

  • Warning: Threads_connected > 70% of max_connections
  • Critical: Threads_connected > 90% of max_connections
  • Warning: Threads_running > 50 sustained for more than 60 seconds
  • Investigate: Aborted_connects growing rapidly (authentication failures, network issues, or connection limit rejections)
Tip

The Prometheus mysqld_exporter exposes all of these as Prometheus metrics out of the box. Pair it with the community MySQL Grafana dashboard (ID 7362) for a complete connection monitoring view without building queries from scratch.

Key Takeaways
  • Calculate max_connections from available RAM, not guesswork — use the formula (available_RAM - buffer_pool) / connection_memory and leave 20% headroom.
  • Watch Max_used_connections and alert before you hit the ceiling, not after the Too many connections error appears in production logs.
  • Tune thread_cache_size to match your peak concurrency level; monitor Threads_created growth rate to confirm the cache is effective.
  • Use MAX_USER_CONNECTIONS to isolate runaway services and prevent a single bad actor from consuming the entire connection pool.
  • Set wait_timeout and interactive_timeout to reclaim leaked idle connections automatically, but calibrate them against your connection pool's keep-alive interval.
  • For workloads with many application processes, a connection proxy like ProxySQL is not optional at scale — it fundamentally changes the economics of connection management.
  • Threads_running is the most important real-time signal: a spike here is a leading indicator of saturation, typically visible before user-facing errors begin.

Run MySQL Without the Connection Firefighting

Tuning connection limits is one of dozens of operational concerns that MySQL DBAs have to stay on top of — alongside replication lag, InnoDB buffer pool hit rates, slow query analysis, backup verification, and failover testing. At JusDB, our managed MySQL service handles connection architecture, continuous monitoring, and performance tuning as part of the platform. You get production-grade MySQL with expert configuration out of the box, without dedicating engineering cycles to operational plumbing.

Explore JusDB's managed MySQL offering and see how teams are shipping faster by letting us own the database operations layer.

Share this article