DEV Community

Cover image for MySQL max_user_connections Error on Shared Hosting: Fix
Faizan
Faizan

Posted on Originally published at headsntail.com

MySQL max_user_connections Error on Shared Hosting: Fix

Most people who hit a MySQL connection limit ask the hosting company to raise the limit before they check why connections stay open. The max_user_connections error means one database user has reached the number of simultaneous connections it is allowed. The site can run fine for hours, then fail during busy traffic or right after a Laravel deployment.

Quick answer: MySQL error 1203 means a database user has reached the server's per-user connection limit. On shared hosting that limit is often 25 concurrent connections. Run SHOW PROCESSLIST as that user to see idle (Sleep) sessions, stop overlapping Laravel queue workers, turn off persistent connections you don't need, and fix slow queries. Ask for a higher limit only after that.

How the max_user_connections error works

MySQL counts open connections per database account. Every PHP request that touches the database opens a session for that account, and every background process (queue worker, cron script) holds its own. Take a Laravel payroll app on shared hosting whose database user payroll_user may hold 25 connections at once. On a normal morning only a few are open. At month-end, employees open their payslips together, each request needs a connection, and a few queue workers are already connected. The 26th connection attempt is refused:

SQLSTATE[42000] [1203] User 'payroll_user' already has more than 'max_user_connections' active connections
Enter fullscreen mode Exit fullscreen mode

MySQL documents error 1203 with SQLSTATE 42000, but some drivers and wrappers print HY000 in front of it. The number 1203 is what matters.

The error doesn't mean the database is corrupted, the server is down, or you suddenly have huge traffic. It means one account ran out of connection slots, and requests fail until a slot frees up.

MySQL error 1203 vs 1226 vs 1040

These errors are related but point to different limits:

Error Meaning What to investigate
1203 The user reached the global max_user_connections limit Connections opened by that database user
1226 The account reached its own resource limit (ER_USER_LIMIT_REACHED) Account-specific MAX_USER_CONNECTIONS set with ALTER USER/GRANT
1040 Too many connections: the whole server reached max_connections Server-wide usage, often other customers on shared hosting

On shared hosting the difference matters. With 1203 or 1226 the problem is your user's connections. With 1040 the server is full, other accounts on the same MySQL server may be the cause, and only the host can tell you.

One edge case from the MySQL manual: if an account is exactly at its limit, a disconnect followed very quickly by a new connect can still be rejected, because the server hasn't finished processing the disconnect. That is why the error sometimes shows up in bursts even when the average number of connections looks fine.

Why shared hosting providers use a limit of 25

A shared server runs databases for many customers. If one site opens hundreds of connections, every other site on that server slows down, so hosts cap each database user. HostGator, for example, documents 25 simultaneous MySQL connections on its shared accounts. cPanel's own documentation uses this example server configuration:

[mysqld]
max_connections = 150
max_user_connections = 25
Enter fullscreen mode Exit fullscreen mode

max_connections is the limit for the whole server, and max_user_connections is the limit per database user. These are example values, not universal defaults, so your host may use different numbers.

On MySQL 8.4 an account can have its own MAX_USER_CONNECTIONS that overrides the global value. An account value of 0 falls back to the global limit, and if the global value is also 0 there is no per-user limit from these settings. Some hosts add their own layer on top, for example CloudLinux MySQL Governor.

How to find who is holding the connections

Before you change PHP settings or upgrade hosting, look at the connections that already exist.

Step 1: check which user you are. Open cPanel > phpMyAdmin > SQL and run:

SELECT CURRENT_USER();
Enter fullscreen mode Exit fullscreen mode

Without the PROCESS privilege, MySQL only shows threads that belong to your own user, so check this first:

  • If it shows your app's database user (the one in .env, for example cpuser_payroll): continue in phpMyAdmin.
  • If it shows your cPanel username or a cpses_... user: phpMyAdmin won't show your app's connections. Run the checks as the app user instead, either from cPanel Terminal or from Laravel:
# cPanel > Terminal, asks for the app user's password
mysql -u cpuser_payroll -p -e "SHOW PROCESSLIST;"

# or from the project folder
php artisan tinker
>>> DB::select('SHOW PROCESSLIST');
Enter fullscreen mode Exit fullscreen mode

Both commands use a connection themselves, so if the user is already at its limit they can fail too. In that case, ask your host for a process list taken at the time of the error.

Step 2: list the sessions.

SHOW PROCESSLIST;
Enter fullscreen mode Exit fullscreen mode

You get columns Id | User | Host | db | Command | Time | State | Info. The useful ones:

Field What it tells you
User Database account that owns the session
Command Query (working) or Sleep (connected but idle)
Time Seconds spent in the current state
Info The running SQL statement, if any

Step 3: count them per user and state. This is quicker than counting rows by eye:

SELECT USER, COMMAND, COUNT(*) AS connections, MAX(TIME) AS longest_seconds
FROM information_schema.PROCESSLIST
GROUP BY USER, COMMAND;
Enter fullscreen mode Exit fullscreen mode

A few Sleep rows are normal. If many rows sit in Sleep with a large Time while the error is happening, look at persistent connections or background workers. Many rows in Query with a large Time point to slow queries.

My routine: during the busy hour I open phpMyAdmin, run SHOW PROCESSLIST, and count the Sleep rows under my database user. Then I fix the cause instead of asking for a higher limit. For my Laravel apps the fixes were short-lived queue workers, queue:restart after every deploy, and proper indexes on the attendance and payroll tables.

Emergency relief: you can kill your own threads without special privileges. Take the Id of a long-sleeping session from the list:

KILL 123456;
Enter fullscreen mode Exit fullscreen mode

This frees a slot immediately but doesn't fix anything. If the cause is still there, the connections come back. To find a stuck transaction rather than an idle one, see how to kill a MySQL transaction by ID.

Server-wide numbers: SHOW STATUS LIKE 'Threads_connected'; counts every open connection on the server, all customers included, so on shared hosting it says nothing about your own usage. SHOW GLOBAL VARIABLES LIKE 'max_user_connections'; and SHOW GLOBAL VARIABLES LIKE 'max_connections'; show the global settings, not an account-specific override. Your host can tell you that one.

Where the max_user_connections error comes from

1. Laravel queue workers keep piling up

I run Laravel HR and payroll applications on cPanel shared hosting. The first time I saw already has more than 'max_user_connections' active connections was during month-end payroll, when many employees were opening their payslips at the same time. It looked like a traffic problem, but the app used one database connection per request. The real problem was my cron: I had scheduled queue:work every minute without --stop-when-empty.

Laravel queue workers are long-lived processes, built to stay alive and wait for jobs. On a server with Supervisor that is what you want. On shared hosting, cron starts a new worker every minute while the old ones are still running, and each one holds a database connection. Against a limit of 25, a dozen leftover workers leave very little for normal web requests. If the error shows up mostly while jobs run, or SHOW PROCESSLIST lists many sleeping connections from the same host, start here.

Fix the Laravel queue cron

This is the line that causes the pile-up:

* * * * * /usr/local/bin/php /home/USERNAME/app/artisan queue:work
Enter fullscreen mode Exit fullscreen mode

Use a short-lived worker that exits on its own:

* * * * * /usr/local/bin/php /home/USERNAME/app/artisan queue:work --stop-when-empty --max-time=50 --sleep=1 --tries=3 >> /home/USERNAME/queue.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Replace USERNAME, the app folder and the PHP path with the values from your account. Laravel 12 needs PHP 8.2 or newer, so if /usr/local/bin/php is older, use the version-specific path. The cron job generator builds the line for you.

  • --stop-when-empty exits the worker when the queue is empty.
  • --max-time=50 makes the worker exit after about 50 seconds, before cron starts the next one.
  • --sleep=1 is how long the worker waits before polling an empty queue again.
  • --tries=3 limits retry attempts for failed jobs.

--max-time is not a hard kill: a job that is running when the time is up finishes first. If your jobs regularly run longer than a minute, workers can still overlap. Wrap the command in flock so a new worker starts only when the previous one has exited:

* * * * * flock -n /home/USERNAME/queue.lock /usr/local/bin/php /home/USERNAME/app/artisan queue:work --stop-when-empty --max-time=50 --tries=3 >> /dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode

-n makes the new run quit immediately when the lock is held, so you never have more than one worker. As an alternative, let the Laravel scheduler start the worker with withoutOverlapping() in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('queue:work --stop-when-empty --max-time=50')
    ->everyMinute()
    ->withoutOverlapping();
Enter fullscreen mode Exit fullscreen mode

Use only one of these setups, not both, or you start two workers every minute. The scheduler itself needs its own cron line; see Laravel schedule:run not working on cPanel.

The queue.log in the first line grows forever. Once the worker runs cleanly, send the output to /dev/null or clear the log now and then.

After every deployment, run:

php artisan queue:restart
Enter fullscreen mode Exit fullscreen mode

This tells running workers to exit gracefully after their current job, so no worker keeps running old code with old connections. The restart signal is stored in Laravel's cache, so the web app and the workers must use the same cache store. For the full deploy order, follow the Laravel deployment checklist for cPanel.

2. Persistent connections stay open

The second common cause is persistent database connections. A normal PDO connection closes when its object is destroyed, usually at the end of the request. A persistent connection is not closed at the end of the script. PHP caches it and reuses it, and PDO doesn't clean it up. On shared hosting with many PHP processes and a small limit, each process can hold a slot while doing nothing. That shows up as rows of Sleep with large Time values.

In Laravel, open config/database.php and look at the options array of the mysql connection. Unless you have a measured reason for persistence, it should not contain PDO::ATTR_PERSISTENT => true. Remove that line or set it to false:

'options' => extension_loaded('pdo_mysql') ? array_filter([
    // keep existing entries such as the SSL CA line
    PDO::ATTR_PERSISTENT => false,
]) : [],
Enter fullscreen mode Exit fullscreen mode

Change only that entry and keep any SSL or other options that are already there. Then clear and rebuild the config cache:

php artisan config:clear
php artisan config:cache
Enter fullscreen mode Exit fullscreen mode

Connections that are already persistent go away only when the PHP processes holding them exit or the server's idle timeout closes them, so expect the Sleep rows to drop over the next while, not instantly.

For a plain PHP or WordPress-era app, search the code for persistent connections in other forms: PDO::ATTR_PERSISTENT in a new PDO(...) call, or a mysqli host that starts with p: (for example mysqli_connect('p:localhost', ...)), which is mysqli's persistent mode. Remove the p: prefix.

For long-running scripts that are finished with the database, Laravel can close the connection explicitly:

use Illuminate\Support\Facades\DB;

DB::disconnect();
Enter fullscreen mode Exit fullscreen mode

Call it only after the last query. If code after it still needs the database, Laravel reconnects, and you gain nothing.

3. Slow queries keep connections busy

Not every connection error comes from idle sessions. A connection stays occupied for as long as its query runs. A payroll dashboard that scans a large attendance table without an index might take several seconds, and if many employees open that report together, those slow queries alone can fill the limit. In SHOW PROCESSLIST this shows up as many Query rows with a large Time.

Check the plan of a slow query (use your own table and column names):

EXPLAIN
SELECT *
FROM attendances
WHERE employee_id = 15
  AND attendance_date BETWEEN '2026-10-01' AND '2026-10-31';
Enter fullscreen mode Exit fullscreen mode

Look at type, key and rows. type: ALL with no key and a large rows value means a full table scan. Check the existing indexes first:

SHOW INDEX FROM attendances;
Enter fullscreen mode Exit fullscreen mode

If no index starts with these columns, a composite index usually helps this kind of query:

ALTER TABLE attendances
ADD INDEX idx_employee_attendance (employee_id, attendance_date);
Enter fullscreen mode Exit fullscreen mode

Run EXPLAIN again afterwards. key should now show idx_employee_attendance and rows should be much smaller. Don't add indexes blindly: a redundant index wastes space and slows down writes. Back up the table and add the index at a quiet hour.

For dashboard totals that don't need to be live on every page load, cache them for a few minutes. Caching cuts the number of repeated queries, but it doesn't replace a missing index.

4. Bots and repeated requests

Search forms, login pages and public filter endpoints get hammered by automated clients. If every request runs an expensive query, a small crawler can use up your connection slots. Laravel's throttle middleware caps this per client:

Route::middleware('throttle:10,1')
    ->get('/search', [SearchController::class, 'index']);
Enter fullscreen mode Exit fullscreen mode

throttle:10,1 allows 10 requests per minute per client key. Adjust the numbers to how people really use the page.

If your access logs show one specific crawler causing the load, you can block its User-Agent in .htaccess, so the request never reaches PHP or MySQL:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTP_USER_AGENT} "^BadBotToBlock/" [NC]
    RewriteRule ^ - [F,L]
</IfModule>
Enter fullscreen mode Exit fullscreen mode

BadBotToBlock is a placeholder, not a real crawler name. Put in only a pattern you found in your logs. User-Agents can be faked, so a host firewall or CDN rule is the stronger option. Don't block crawlers in general, or you block Google and Bing too. For the other hardening steps, see the website security checklist for cPanel.

Verify that the fix worked

After the change, wait for the next busy period and run the count query from the diagnosis section again. You should see a handful of connections for your user, mostly short Query rows, and no growing list of long Sleep sessions. For queue workers, ps aux | grep queue:work in cPanel Terminal should show at most one or two worker processes, never a long list.

Common misunderstandings about MySQL connection limits

"It works on XAMPP, so it will work on shared hosting"

Not necessarily. I test schema changes on Windows with XAMPP before deploying. My local MySQL has no per-user connection cap configured by default, so a page or job that is fine locally can hit error 1203 once it runs on shared hosting. Local tests prove the code is correct, not that it fits within production limits.

"Upgrading to Laravel 11 or 12 caused the error"

A Laravel upgrade doesn't change how many connections a request opens. Usually something else changed at the same time: workers weren't restarted, the cron line was edited, the cache or queue driver changed, or new code runs more queries. Check what the deployment changed before blaming the framework version.

"Raising max_connections fixes error 1203"

No. max_connections is the server-wide limit. Error 1203 comes from the per-user limit (max_user_connections or the account's own MAX_USER_CONNECTIONS), and a bigger server limit doesn't lift a smaller per-user one.

"Redis will remove MySQL connection errors"

Redis takes load off MySQL only if your cache, sessions or queues currently live in the database. It doesn't close persistent PDO connections, stop overlapping workers, or speed up an unindexed report query.

When to contact the host or move to a VPS

If you have fixed the workers, persistent connections and slow queries, and the error still appears during normal traffic, contact your host. Ask these exact questions:

  1. What is the simultaneous connection limit for my database user?
  2. Is it enforced by the global max_user_connections, an account-level MAX_USER_CONNECTIONS, or something like MySQL Governor per cPanel account?
  3. Can you send the process list from the time of the error?
  4. Is the server itself hitting max_connections (error 1040)?
  5. Can my user's limit be raised on my current plan?

Only an administrator can change these limits. On MySQL 8.4, an account-specific limit is set like this (user, host and value must match the real account):

ALTER USER 'database_user'@'localhost'
WITH MAX_USER_CONNECTIONS 50;
Enter fullscreen mode Exit fullscreen mode

The global value is set in the server configuration (my.cnf). Regular shared hosting customers can't change either one, so don't try to edit server files you don't control.

If your app legitimately needs more connections (payroll runs, many background jobs, heavy reports), a VPS gives you control over MySQL limits, Supervisor-managed workers and monitoring. A VPS won't fix leaking connections by itself, though. I'd fix the application first, then decide whether the host's limit is really too small.

If you need help reviewing your Laravel and MySQL hosting setup, you can contact HeadsNTail.

FAQ

Is max_user_connections applied to the cPanel account or the database user?

MySQL enforces it per database account (user@host), so two database users under the same cPanel account each have their own count. Some hosts add a limit per cPanel account on top, through CloudLinux MySQL Governor or their own policy, and HostGator documents its 25-connection limit per cPanel account. Ask your host which one applies.

Can I increase max_user_connections from cPanel?

No. cPanel's MySQL Databases page lets you create users and databases but not change connection limits. The host's server administrator has to change the global value in my.cnf or the account limit with ALTER USER.

How many MySQL connections does Laravel open per request?

Normally one per database connection that the request actually uses, opened on the first query. You get more if the app uses several connections (for example a separate reporting database or read/write split). Queue workers, scheduled commands and other PHP processes each hold their own connection, which is why they matter so much against a limit of 25.

Will switching Laravel cache to Redis fix error 1203?

Only partly, and only if your cache, sessions or queue currently use the database driver. Redis doesn't stop overlapping workers, persistent connections or slow queries, which are the usual causes of 1203. Check how many connections your user holds and why before you add Redis.

Next step: the next time the error appears, run the per-user count query during the busy period, note how many Sleep and Query rows your database user has, and match them to the cause above before you contact the host.


Originally published at headsntail.com. Free tools for PHP, Laravel and cPanel work: headsntail.com/tools.

Top comments (0)