Serving thousands of concurrent connections without choking the server
One thread per connection works well as long as the number of concurrent connections stays modest. Once PHP-FPM pools or similar setups generate thousands of parallel connections, that model tips over and the server loses throughput instead of gaining it. The thread pool plugin deliberately caps how many threads actually run at once.
Table of Contents
- 1. Why the classic thread-per-connection model hits its limits
- 2. The core idea behind the thread pool plugin
- 3. Configuring groups and worker threads
- 4. Stall limit: how the plugin prevents starvation from long requests
- 5. Priorities: short transactional requests ahead of long reports
- 6. Monitoring thread pool activity
- 7. Relationship to application-side connection pooling
- 8. Common pitfalls when introducing it
- 9. Practical recommendation for Magento environments with high connection load
- 10. Summary
- 11. FAQ
1. Why the classic thread-per-connection model hits its limits
In the default model, the server spawns a dedicated operating system thread for every new connection, which stays alive for the entire lifetime of that connection. With a few hundred concurrent connections this works smoothly with minimal management overhead. Once the number of concurrent connections grows into the low thousands, for example because several PHP-FPM pools each connect directly to the database with many workers, the picture changes considerably.
Every additional thread means additional memory for its thread stack plus additional context-switching overhead once more threads are active than there are CPU cores available. Past a certain point, actual throughput even drops, because threads start competing with each other for the same internal resources, such as buffer pool mutexes, instead of productively processing requests. More connections then no longer mean more throughput, but less.
2. The core idea behind the thread pool plugin
Instead of assigning every connection its own, permanent thread, the thread pool plugin bundles connections into a fixed number of thread groups. Each group manages a bounded pool of worker threads, typically sized around the number of available CPU cores, which pick up incoming requests from a queue rather than each connection permanently occupying its own thread.
The result is that the number of threads actually executing at once on the server stays capped, regardless of how many connections are open in total. A single connection that is currently waiting for a response from the client, or has no active request, does not block a worker thread, but frees it up for other connections that actually have work to do.
3. Configuring groups and worker threads
The central setting is thread_pool_size, which sets the number of thread groups and in practice is usually aligned with the number of CPU cores. thread_pool_max_threads additionally caps the absolute upper limit of worker threads across all groups, acting as a safety net against an uncontrolled thread explosion under unusual request behavior.
A group count that is set too low causes short, fast requests to get stuck behind long, blocking requests in the same queue. A count that is set too high, on the other hand, approaches the behavior of the classic thread-per-connection model again and loses the actual benefit of capping.
SHOW VARIABLES LIKE 'thread_pool%';
-- Typical starting point: number of CPU cores as group count
SET GLOBAL thread_pool_size = 8;
4. Stall limit: how the plugin prevents starvation from long requests
A pure queuing model would have an obvious problem: if a long-running request blocks the only active worker thread in a group, every subsequent, otherwise fast request in that group would have to wait until the long one finishes. thread_pool_stall_limit solves this by defining a time span after which an extra thread gets started in a group if a running request blocks longer than expected.
This mechanism prevents genuine starvation without requiring group size to be sized generously up front. In practice, a lower stall limit value means a faster reaction to blocking requests, but also more temporarily spawned extra threads, which is why the value needs to be deliberately weighed between reaction speed and thread growth.
5. Priorities: short transactional requests ahead of long reports
Within a group, the thread pool plugin distinguishes between high and low priority requests. Statements that are part of an already running transaction, or that get classified as short, receive preferred access to free worker threads, while new, potentially long-running requests such as large reporting queries can be placed into a lower priority tier.
This prioritization ensures a typical storefront transaction with several short, consecutive statements does not get stuck behind a single long-running analytics query, even if both request types hit the same server at the same time. Without this distinction, a single heavy report would be enough to noticeably degrade the perceived response time of the entire store for a short period.
6. Monitoring thread pool activity
For ongoing monitoring, server status exposes counters such as Threadpool_threads for the current total number of worker threads and Threadpool_idle_threads for the number of currently idle workers. A value that stays consistently low for idle threads while wait times keep rising is a sign that the configured group count is undersized for the current load.
Implementations such as the Percona Server plugin add more detailed tables on top of this, for example showing the current state of individual threads and groups, which make it possible to determine specifically whether requests are genuinely sitting in a queue or the bottleneck lies elsewhere in the system.
SHOW GLOBAL STATUS LIKE 'Threadpool%';
-- Total connections vs. threads actually executing
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
7. Relationship to application-side connection pooling
The thread pool plugin does not replace connection pooling on the application side or through an upstream proxy such as ProxySQL, it complements it. While an upstream pooler limits the number of connections that actually reach the server in the first place, the thread pool ensures that even if many thousands of connections still arrive, server resources are not exhausted by an uncontrolled number of simultaneously active threads.
For a Magento setup with several PHP-FPM pools each defining their own connection limits, a combination of both is usually the most robust approach: connection pooling reduces the number of connections as far as possible, and the thread pool catches the remaining peak load in a controlled way, instead of a single unexpected traffic spike bringing the server to its knees.
8. Common pitfalls when introducing it
A frequent pitfall is setting group size once based on the CPU core count measured at introduction time and never revisiting it afterward, even though hardware or request patterns change over time. Regularly reviewing the thread pool status values prevents a configuration that once fit from silently turning into a bottleneck years later.
A second pitfall involves applications with session-holding, blocking transactions, for example explicit locks held across several statements. In an undersized thread pool, such blocking patterns can cause an entire group to effectively stall for the duration of the block until the stall limit spins up an extra thread. Deliberately reducing long, lock-holding transactions therefore remains worthwhile even with a thread pool in place.
9. Practical recommendation for Magento environments with high connection load
For Magento installations where several application servers or containers access the same database concurrently, the thread pool pays off most when Threads_connected regularly climbs well past the low thousands while Threads_running stays comparatively low. This pattern shows that many connections are open but only a few are actually active, exactly the scenario where the thread pool has the biggest effect.
Before rolling it out in production, a test run under realistic, parallel load on a staging system is recommended, deliberately varying both group size and stall limit instead of relying on generic default values tuned for a different request pattern.
| Configuration variable | Purpose | Typical starting value | Effect if misconfigured |
|---|---|---|---|
thread_pool_size |
Number of thread groups | Number of CPU cores | Too low: queue backlog, too high: barely any capping effect |
thread_pool_max_threads |
Absolute cap on all worker threads | Server-dependent, safety net | Too low: requests get rejected instead of queued |
thread_pool_stall_limit |
Time until an extra thread spins up | A few hundred milliseconds | Too high: short requests can starve |
Threadpool_idle_threads (status) |
Number of idle worker threads | Observation value | Consistently low points to undersizing |
thread_pool_high_prio_tickets |
Number of priority executions per connection | Low, to preserve fairness | Too high: short requests get favored permanently, long ones starve |
Mironsoft
Database performance, index tuning, and Magento DB optimization
A Magento shop suffering from slow database queries?
We analyze MySQL databases for performance bottlenecks, optimize indexes and queries with purpose, and set up backup and replication strategies that actually work when it counts.
Performance Audit
Systematically investigate the slow query log and explain plans for bottlenecks.
Index Optimization
Build indexes with purpose for the shop's actual query load.
Backup Strategy
Set up reliable backup and restore processes for production Magento databases.
10. Summary
Thread Pool Plugin: The Essentials at a Glance
Problem
One thread per connection stops scaling at thousands of concurrent connections and lowers throughput instead of raising it.
Solution
Thread groups with a bounded worker count serve requests from a queue instead of assigning every connection a permanent thread.
Fairness
Stall limit and priority tiers prevent long requests from permanently blocking short ones.
Complement
The thread pool does not replace application-side connection pooling, it catches the remaining peak load in a controlled way.