log in
consulting hosting industries the daily tools about contact

ProxySQL vs. Laravel's Connection Pool: When the Proxy Earns Its Keep

Running MariaDB on a single VM? Here's when adding ProxySQL between Laravel and your database actually pays off — and when it just adds another thing to break.

ProxySQL is one of those tools that looks like overkill until the night your MariaDB server starts rejecting connections at 2am because you've got 400 open sockets and max_connections is 151. I've been there. But I've also added ProxySQL to setups where it genuinely made things worse — slower deploys, one more daemon to babysit, and confusion for the next developer who opens config/database.php and wonders why the port is 6033.

Here's how I actually think about this decision.

What Laravel Does By Default

Laravel's database layer sits on top of PDO, which means every time a request comes in and touches the database, a TCP connection gets opened to MariaDB, used, and released when the request ends. PHP-FPM processes persist across requests, so PDO can reuse connections via persistent PDO — but Laravel disables PDO::ATTR_PERSISTENT by default, and for good reason. Persistent connections with PHP-FPM are a footgun. A connection that dies mid-transaction doesn't get cleaned up properly, and you end up serving the next request on a broken socket.

So in practice, with the default Laravel config on a typical PHP-FPM setup, each worker opens a fresh connection per request cycle. If you've got 20 FPM workers and your app is busy, you've got up to 20 simultaneous connections. That's nothing. MariaDB handles it fine.

The math changes when you start scaling FPM workers to handle traffic, or you add a queue worker fleet, or you run multiple processes per Horizon supervisor. Now you're looking at 100, 200, 300 connections — and most of them are idle, waiting for the next job or request.

What ProxySQL Actually Does

ProxySQL sits between your application and MariaDB and multiplexes connections. Your 200 FPM workers all connect to ProxySQL (local Unix socket or 127.0.0.1:6033). ProxySQL maintains its own pool of, say, 20 real connections to MariaDB and routes queries through them. Your app thinks it has 200 connections. MariaDB sees 20.

That's connection multiplexing, and it's the core value proposition on a single VM. Everything else ProxySQL does — query routing, read/write splitting, query mirroring, stats — is useful but secondary.

// config/database.php — talking to ProxySQL instead of MariaDB directly
'mysql' => [
    'driver'    => 'mysql',
    'host'      => '127.0.0.1',
    'port'      => '6033',          // ProxySQL's port, not MariaDB's 3306
    'database'  => env('DB_DATABASE', 'forge'),
    'username'  => env('DB_USERNAME', 'forge'),
    'password'  => env('DB_PASSWORD', ''),
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
    'strict'    => true,
    'engine'    => null,
    'options'   => [
        PDO::ATTR_TIMEOUT => 5,
    ],
],

That's it on the Laravel side. ProxySQL handles the rest.

On the ProxySQL side, the key config that actually matters for a single-VM setup:

-- Inside ProxySQL admin (mysql -u admin -padmin -h 127.0.0.1 -P 6032)

-- Tell ProxySQL where MariaDB actually lives
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections)
VALUES (0, '127.0.0.1', 3306, 25);

-- The ProxySQL user that talks to MariaDB
INSERT INTO mysql_users (username, password, default_hostgroup, max_connections)
VALUES ('forge', 'yourpassword', 0, 500);

-- Connection pool behavior
UPDATE global_variables
SET variable_value = '5000'
WHERE variable_name = 'mysql-connection_max_age_ms';

UPDATE global_variables
SET variable_value = '10'
WHERE variable_name = 'mysql-free_connections_pct';

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

The max_connections = 25 on the server definition is the ceiling ProxySQL enforces against MariaDB. Your 200 app workers can still all "connect" to ProxySQL; they just queue if all 25 backend connections are busy. That queuing behavior is something you tune with mysql-connect_timeout_server — I usually set it around 3000ms before ProxySQL errors back to the app.

The Gotchas That Will Bite You

Session-level state doesn't survive multiplexing. This is the big one. If you SET a session variable, use LAST_INSERT_ID() outside of a transaction, or rely on user-defined variables (@myvar), ProxySQL can route your next query to a different backend connection. Your session state is gone. In Laravel apps this usually isn't a problem because the ORM doesn't do much session-level trickery — but if you're using DB::statement('SET @rank := 0') as part of a ranking query, that pattern breaks under multiplexing.

The fix is to wrap those in explicit transactions. ProxySQL pins a connection for the duration of a transaction.

// This is safe — ProxySQL pins the connection for the transaction
DB::transaction(function () {
    DB::statement('SET @row_number := 0');
    $results = DB::select('
        SELECT @row_number := @row_number + 1 AS row_num, id, name
        FROM products
        ORDER BY created_at DESC
    ');
    return $results;
});

mysql_use_result streaming queries. If you're streaming large result sets with cursors, multiplexing breaks that too. ProxySQL can't share the connection mid-stream. You'll see Commands out of sync errors. For any streaming query, either use a dedicated hostgroup in ProxySQL that bypasses multiplexing, or route those queries directly to port 3306.

Deployment and migrations. Your php artisan migrate hits ProxySQL on 6033. This is usually fine, but I've had a client's migration fail in a confusing way because ProxySQL's connection validation was stricter than expected on a freshly created database. My rule now: migrations run against 3306 directly in the deploy script, not through the proxy.

# deploy.sh — migrations bypass ProxySQL
DB_PORT=3306 php artisan migrate --force

The Unix socket temptation. MariaDB is fastest over a Unix socket on the same VM. ProxySQL runs over TCP even locally. You're adding a tiny bit of latency per query — usually sub-millisecond, but measurable if you're doing 5,000 queries per request (which, if you are, that's a different problem to fix first).

When the Overhead Pays Off

I added ProxySQL to a healthcare client's setup last year. They were running a Laravel app with a Horizon queue on a single VM — 30 FPM workers plus 3 Horizon supervisors with 10 workers each. That's a potential 60 connections at peak, which was fine until we added two more queue worker types and hit 120. MariaDB's default max_connections (151) started becoming a real constraint because their EHR integration pulled bursts of sync jobs.

With ProxySQL capping the backend at 30 connections, MariaDB was happy. Response times dropped slightly because MariaDB wasn't juggling as many idle connections. The queue throughput actually improved because ProxySQL's queuing at the proxy level was more graceful than MariaDB outright refusing connections.

I'd reach for ProxySQL when:

  • You're running more than 80-100 total processes that touch the database (FPM + queue workers + cron + whatever else).
  • You're approaching MariaDB's max_connections and raising it is causing memory pressure.
  • You're planning to add a read replica and want a clean way to route SELECT queries to it without changing application code. ProxySQL's query rules handle this elegantly.
  • You need per-query stats and the slow query log isn't granular enough. ProxySQL's stats tables are genuinely excellent for this.

I'd skip ProxySQL and just tune MariaDB directly when:

  • You have fewer than 50 total connections. Just raise max_connections to 200 and move on.
  • Your app is a simple CRUD Laravel site with a single queue worker. ProxySQL is a daemon that needs monitoring, log rotation, and the occasional restart. That's real operational overhead on a project that doesn't need it.
  • You're on managed hosting (Forge, Ploi) where you don't fully own the server config. Running ProxySQL alongside those setups gets messy fast.
  • Your dev environment doesn't mirror production closely. Debugging a multiplexing issue locally when dev connects to 3306 directly is a special kind of painful.

The Middle Ground Nobody Talks About

Before going full ProxySQL, check whether you've actually tuned MariaDB's thread cache and connection overhead first. thread_cache_size = 32 and bumping max_connections to 300 with appropriate innodb_buffer_pool_size tuning solves the problem for probably 70% of the single-VM setups I've looked at. ProxySQL doesn't make bad MariaDB config good — it just limits the blast radius.

Also: if your real problem is slow queries holding connections open too long, ProxySQL doesn't fix that. It just means 25 connections are stuck instead of 200. Fix the queries.


ProxySQL is a legitimate tool and I run it in production without apologies. But on a single VM, it earns its keep at a pretty specific threshold — you need real connection pressure, not theoretical connection pressure. Add it when the connections are actually hurting you, not because a blog post said you should.

The operational cost is low but it's not zero. Know what you're signing up for before you put something between your app and its database.

Related

Need help shipping something like this? Get in touch.