Troubleshooting

How to fix 503 Service Unavailable: from hosting resource limits to the PHP-FPM pool

Fixing 503 Service Unavailable: the request path from the edge and web server to the PHP-FPM pool and the account's resource limits, showing which layer produced the 503

Fixing a 503 Service Unavailable error takes a few minutes when you know which layer generated the response before you touch a single setting, and a few days when you start raising numbers blindly. Unlike a 500 or a 502, nothing is broken in a 503: the service is alive and is deliberately not answering, because it has hit a resource limit, is rate-limiting requests, or is in maintenance mode. Here we use the response headers and the logs to find the culprit — shared hosting, the PHP-FPM pool, the web server, the edge or the application — and finish with a point most guides skip: 503 is the only correct status code for planned downtime, provided the downtime is short and comes with a Retry-After header.

What exactly does a 503 Service Unavailable error mean, and how is it different from 500 and 502?

A 503 Service Unavailable is the response a server sends to say that, right now, it cannot or will not handle the request, but that the condition is temporary. The standard, RFC 9110, says exactly that: a 503 means the server is currently unable to handle the request "due to a temporary overload or scheduled maintenance", and it may also send a Retry-After header to say how long the client should wait.

That definition also draws the line between a 503 and its two neighbors. In a 500, the application's code runs and fails halfway through, so something really is broken. In a 502, the proxy gets no valid response from the backend, or cannot find a backend at all. In a 503, no code has broken; it never even got its turn to run. So you treat a 500 by reading the log, a 502 by bringing the backend back to life, and a 503 by finding the limit you have hit. We cover the first two in separate guides: fixing a 500 error and fixing a 502 error.

One technical point that is almost always left out, and that rules out half the wrong guesses: on an Nginx plus PHP-FPM stack, if the backend dies or its socket stops answering, Nginx writes something like "connect() to unix:/run/php/php8.4-fpm.sock failed (2: No such file or directory) while connecting to upstream" to its error log and gives the user a 502, not a 503. (The "no live upstreams" message that many guides attribute to this case only appears with a multi-server upstream block, and that, too, is a 502.) So on Nginx the cause of a 503 is almost never "PHP is down": either Nginx is rate-limiting requests, someone has put return 503 in the config, or the response came from the application. And one honest caveat from the standard itself: RFC 9110 notes that a server is not required to use 503 when it is overloaded: "some servers might simply refuse the connection".

First step: which layer generated the 503?

Every request passes through several layers, and any of them can produce a 503: the edge or CDN, the origin web server, the shared-hosting resource limiter, the PHP process pool, and the application itself. Two simple commands draw the boundaries. First, the full response headers:

curl -sS -o /dev/null -D - https://example.com/

HTTP/2 503
server: LiteSpeed
retry-after: 600
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0

Two fields in this output are clues. The server header says which software wrote the response: if the name of an edge network sits there and no header from the origin comes with it, the 503 was generated at the edge and never reached your server. The presence of retry-after means something sent this response on purpose, rather than the server collapsing under load; and the exact value 600 is the signature of WordPress maintenance mode. The second step is to compare a static file with a dynamic page:

for u in / /wp-content/uploads/logo.png /wp-login.php; do
  printf '%-32s -> ' "$u"
  curl -s -o /dev/null -w '%{http_code}\n' "https://example.com$u"
done

/                                -> 503
/wp-content/uploads/logo.png     -> 200
/wp-login.php                    -> 503

Reading it is straightforward: a static file returning 200 next to a PHP page returning 503 means the web server is healthy and the problem lies in the PHP execution layer or in the account's resource limits. If everything returns a 503, even an image, the response is coming from a layer further out. And if only one specific path returns a 503, a rate limit or a security rule is almost always involved.

Which layer is producing the 503: the request path from the browser through the edge or CDN layer and the web server to the PHP-FPM process pool and the account's resource limits, with the signature each layer leaves; the first layer that refuses, from the outside in, owns the error
The five layers that can produce a 503, and the order in which to rule them out.
💡 The 503 diagnosis rule: before you change any number, write down three things: the server header, whether a static file also gets a 503, and whether the error is constant or only appears at peak hours. Those three answers usually settle which layer is at fault.

503 errors on shared hosting: account resource limits, the most common cause

The most common source of a 503 on shared hosting is the account hitting its resource limits. The most widespread isolation layer on cPanel and DirectAdmin servers is CloudLinux with its LVE technology; on a server without CloudLinux, the limits are usually set in each user's own PHP-FPM pool. Of the limits defined in the CloudLinux limits documentation, these are the ones that matter for a 503: SPEED for CPU, PMEM for physical memory, IO and IOPS for disk, NPROC for the total number of processes, and EP, or Entry Processes, for concurrent requests being executed. There is also a VMEM limit, and the same documentation lists hitting it as a cause of 500 and 503 errors, but CloudLinux explicitly recommends setting VMEM to zero, because it is deprecated.

And here a correction is needed that most write-ups get wrong: when the EP limit is full, the response is a 508, not a 503. What produces a 500 or a 503 is hitting the NPROC or PMEM limit. So "Resource Limit Is Reached" with a 508 code means EP; a 503 means memory and process count.

The right metric is not the number of visits but concurrency: ten thousand daily visits to pages that take 200 milliseconds rarely hit the limit, while a thousand visits to pages that take three seconds can easily hit it at peak time. On LiteSpeed there is a separate limit called PHP suEXEC Max Conn, which applies per worker, not per account: with two workers and a value of 10, you will have up to about 20 LSPHP processes (plus a burst of roughly 33 percent). Its log message points you to raising LSAPI_CHILDREN, but in suEXEC mode the setting to change is PHP suEXEC Max Conn itself. The LiteSpeed 503 troubleshooting documentation also traces running out of memory, fork() failures and a full disk to this same 503.

⚠️ On shared hosting these limits are not in your hands. Two things are: reducing consumption (page caching, removing heavy plugins, shortening each request's execution time) and opening a ticket that states the exact times of the errors, so support can check the account's fault counters for that window. Have the right expectations of the panel, too: the service view shows disk and bandwidth usage, but the memory, process and Entry Process counters are recorded on the server itself, and the way to reach them is a ticket.

The PHP-FPM pool is full: how do I calculate max_children?

On a VPS or a dedicated server, a saturated PHP-FPM pool is the most common root cause of 5xx errors, but the code the user sees depends on the web server in front, and this is where most guides go wrong. The PHP-FPM configuration documentation describes pm.max_children as the limit on "the number of simultaneous requests that will be served". When every worker is busy, Apache with mod_proxy_fcgi and LiteSpeed answer with a straight 503; on Nginx, though, you usually get a 502 or a 504, because the request either waits in the listen.backlog queue until fastcgi_read_timeout runs out (504), or connect() fails with (11: Resource temporarily unavailable) (502). So if you are on Nginx and seeing a 503, pool saturation is not the culprit. The FPM log, however, looks the same in all three cases, which makes it the most reliable clue:

grep -i "max_children\|listening queue" /var/log/php8.4-fpm.log

WARNING: [pool www] server reached pm.max_children setting (12), consider raising it
WARNING: [pool www] listening queue is not empty, #38 requests are waiting to be served, consider raising pm.max_children setting (12)

Swap in your own PHP version: the log path is /var/log/phpX.Y-fpm.log on Debian and Ubuntu, /var/log/php-fpm/error.log on AlmaLinux and CentOS, and under /opt/cpanel/ea-phpXY/root/usr/var/log/php-fpm/ on cPanel. Seeing either of these two lines is a definitive diagnosis.

For the live picture, enable the FPM status page with pm.status_path, which on its own is not enough: on Nginx you also have to define the matching location, or all you will get is a 404.

location = /fpm-status {
    allow 127.0.0.1; deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}

Three numbers on this page matter: listen queue is how many requests are waiting for a free worker, max active processes is the highest number of workers active at the same time, and max children reached, which, when it is above zero, means the limit has been hit at least once. Be sure to send the Host header, or the request lands on the default virtual host:

curl -s -H 'Host: example.com' http://127.0.0.1/fpm-status

pool:                 www
process manager:      dynamic
listen queue:         38
max listen queue:     129
idle processes:       0
active processes:     12
max active processes: 12
max children reached: 4

max_children is not an arbitrary number; it is the result of a division. Measure both numbers on the server yourself:

# average resident memory of live PHP-FPM *children* (master excluded), in MB
ps --no-headers -o rss,args -C php-fpm8.4 | awk '/pool /{s+=$1; n++} END {if (n) printf "%d workers, avg %.0f MB\n", n, s/n/1024; else print "no worker found"}'
12 workers, avg 78 MB

# RAM available right now — the running FPM workers are NOT counted as free
free -m | awk '/Mem:/ {print "available:", $7, "MB"}'
available: 1420 MB

The /pool / filter separates the workers from the master process; without it, the master is counted too and drags the average down. And this is where almost every popular formula goes wrong: the available column is measured at the very moment those 12 workers are running and using memory, so 1420 is not "the RAM left for PHP", it is the RAM left after them. You have to add the current workers' memory back: (1420 + 12×78) ÷ 78 ≈ 30. And because RSS counts shared memory again in every worker, that 30 is generous, not pessimistic; with headroom for MySQL peaks, 20 to 25 is more prudent. The most honest sentence in this section: raising max_children beyond the available memory does not cure a 503, it makes it worse. The server starts swapping, and the OOM Killer kills processes, sometimes MySQL itself. If the correct number is smaller than what the site really needs, the problem is not the configuration but a shortage of RAM; our Linux server monitoring guide covers the alert thresholds.

503 errors on Apache: a mod_proxy worker in error state, and a full disk

Unlike Nginx, which returns a 502 for a dead backend, Apache returns a 503 in one specific situation: when the proxy module cannot find a healthy worker for the origin, it returns HTTP_SERVICE_UNAVAILABLE, which is a 503, and puts that worker into an error state. The trace is in the httpd error log: "ap_proxy_connect_backend disabling worker for (127.0.0.1:9000)", followed by "proxy: HTTP: disabled connection for". The other half of the story is in the mod_proxy documentation: the retry parameter, 60 seconds by default, sets how long no requests are sent to that origin. In other words, even if you bring the backend back right now, you may keep getting a 503 for up to a minute, and that delay is not a bug.

systemctl status php8.4-fpm --no-pager
ss -lxp | grep -i php        # unix socket: the default on Debian/Ubuntu and cPanel
ss -lntp | grep ':9000'      # only if the pool listens on TCP
df -h /var /tmp

Run both ss commands as root, or the process name column stays empty. And keep in mind that ss -lntp only shows TCP sockets; empty output means "not on TCP", not "there is no socket". If the service will not start at all, check the disk space: a full disk, especially the log partition and temporary files, is the silent cause of many 503s, because no service can start without being able to write.

WordPress 503 error after an update: a leftover .maintenance file

The most common WordPress 503 comes from a failed update. During every core, plugin or theme update, WordPress creates a file named .maintenance in the site root and answers every visit with a 503 and the message "Briefly unavailable for scheduled maintenance" until the job is finished; if the update stops halfway, the file stays behind. Everyone covers that much; the detail that changes the troubleshooting path is in WordPress's own code.

The wp_is_maintenance_mode function only treats maintenance mode as active if the $upgrading value inside that file was recorded less than ten minutes ago. The direct consequence: if more than ten minutes have passed since a failed update and the site still returns a 503, the .maintenance file is not the culprit, and deleting it fixes nothing. The definitive sign of this state is in the headers: WordPress sends exactly Retry-After: 600.

cd /home/user/public_html
cat .maintenance
<?php $upgrading = 1788336000; ?>

# compare that timestamp with now: a gap over 600 seconds means it is already inactive
date +%s
1788339912

rm -f .maintenance

You can do the same without SSH access: open the site root in the panel's file manager or in an FTP client and delete .maintenance. Dot files are visible in the root listing, and before deleting it you can open it and read that single $upgrading line. In the Mehran Host panel, you do this from that hosting service's Files tab.

A lesser-known extra: if you put a file named maintenance.php in wp-content, WordPress shows it instead of the default page. It is the right place for a tidy page in your own language, provided you send the 503 code and the Retry-After header yourself, because on this path WordPress does not send its default headers and your page may get indexed with a 200 code.

503 errors on Nginx: request rate limiting, bots and the edge layer

If the 503 only appears for some visitors or on some paths, something is probably limiting the request rate on purpose. On Nginx this is the most common explanation of a genuine 503, because it is the default behavior: the documentation of the limit_req and limit_conn modules gives 503 as the default for both limit_req_status and limit_conn_status. That means any limiting rule someone once added to the server shows up today as a 503; we cover the syntax of these directives in setting up Nginx on Ubuntu. And because the default log level of limit_req is error, its trace is in the error log:

grep "limiting requests" /var/log/nginx/error.log | tail -3

2026/09/02 11:04:18 [error] 918#918: *20431 limiting requests, excess: 0.714 by zone "perip",
  client: 203.0.113.7, server: example.com, request: "GET /?s=test HTTP/2.0", host: "example.com"

Three scenarios sit behind lines like these. First, a flood of bots and scanners hitting heavy paths such as internal search and store filters; here the rate limit is doing its job, and the real problem is that those paths are unprotected. How to measure this traffic by source is covered in our server bandwidth guide. Second, a rule written too strictly, which also catches real users, especially several users behind one shared IP. Third, a limit at the edge or CDN, where the request never reaches your server and leaves no trace in the origin log; What is a CDN and how does it work walks through the request path from the edge to the origin.

The database can cause a 503 too

A database never sends a 503 directly (it doesn't speak HTTP), but it is one of the most reliable ways to end up with one. The chain is simple: a slow or locked query keeps each PHP worker busy for several seconds instead of two hundred milliseconds; the pool fills up; and depending on the web server, the layer in front returns a 503 (Apache and LiteSpeed) or a 502 and 504 (Nginx). So when the FPM log reports that max_children was reached, look at the slow queries before you raise that number.

On shared hosting, MySQL's own limits come into play as well: error 1040, "Too many connections", means the whole server has reached its connection limit; 1203 means this user has exceeded max_user_connections; and 1226 means one of this user's resource limits is used up. In WordPress, each of them ends in the "Error establishing a database connection" page.

And an important fact for SEO: the dead_db function sends that page with a default 500 code, not a 503, so a temporary database failure is presented to search engines as an internal error, with everything that implies. The fix is simple: create a file named db-error.php in wp-content. WordPress does not run it on the normal load path; it is only required when dead_db is called, before any other output, and execution ends right there. So don't use any function that depends on the database inside it (get_option, get_bloginfo, shortcodes), and write the HTML as fixed markup.

<?php // wp-content/db-error.php
http_response_code(503);
header('Retry-After: 300');
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html><html lang="en"><meta charset="utf-8">
<title>Temporary outage</title>
<p>The site is unavailable for a few minutes. Please try again shortly.</p>

Reducing the load on the database is a separate topic (page caching, the right indexes and trimming bloated tables), and we lay out the full path in our guide to speeding up WordPress.

503 and SEO: why this code is right for planned downtime

For a short, deliberate shutdown of a site, such as a hosting move, a database migration or a heavy update, the right code is 503, not 500 and not 404. The difference from a 200 is that Google does not index the "down for maintenance" page in place of the real content, and the difference from a 404 is that the URL is not treated as removed. Google's guide to pausing an online business is explicit, but don't read its order backwards: Google's first option is for the site to stay online with limited functionality. It describes taking the whole site offline as an extreme measure, appropriate only for a very short period of a few days at most, and if you have to do it, it says to return the information page with a 503 code.

The Retry-After header is the essential companion to this code. According to the Retry-After section of RFC 9110, its value is either a number of seconds to wait or an HTTP date, and sent with a 503 it indicates "how long the service is expected to be unavailable". Here is an Nginx config for a half-hour shutdown that exempts your own IP:

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com;

    if ($remote_addr != 203.0.113.7) {
        return 503;
    }

    location / { try_files $uri $uri/ /index.php?$args; }

    error_page 503 @maintenance;
    location @maintenance {
        root /var/www/maintenance;
        add_header Retry-After 1800 always;
        try_files /index.html =503;
    }
}

Two parts of this config cannot be left out: without ssl_certificate and its key, Nginx will not start at all, and without location / the exempted IP gets a 404 instead of the real site. Also, error_page keeps the original code, so the maintenance page is served with that same 503; with the =response syntax you could accidentally return a 200 and get that very page indexed.

The limits of this code have to be stated honestly too. Google's documentation on HTTP and network errors says that 500, 502 and 503 responses make Googlebot reduce the crawl rate, and that "Google's indexing pipeline removes from the index URLs that persistently return a server error". In other words, a 503 is only harmless as long as it is short. In the same guide to pausing an online business, Google is even more direct: closing the whole site, even for just a few weeks, can hurt how the site is indexed.

⚠️ Use a 503 for a short, announced window, not as long-term parking for a site. If a project is going to be paused for weeks, keep the site up with real content and limited functionality instead of a site-wide 503, which is Google's own preferred option. And treat an unintended, prolonged 503 as an SEO emergency, not an ordinary technical error.

Causes of a 503 error, how to confirm each one, and the fix

Every path that leads to a 503, summed up in one table:

CauseDefinitive testFix
A full PHP-FPM poolA server reached max_children line in the FPM log, or a listen queue above zeroCalculate max_children as available RAM plus the current workers' memory, divided by the memory of one worker
The account's memory or process limit on shared hostingErrors that coincide with peak hours, and the account's memory and process counters for the same window, from the panel's Resource Usage page or else through a ticketPage caching, removing heavy plugins, and a ticket with the exact times
The Entry Processes limit in CloudLinuxA 508 instead of a 503, with the message Resource Limit Is ReachedCut the execution time of each request; this limit cannot be changed from the customer's side
WordPress maintenance mode left behindA Retry-After header of 600, and less than ten minutes since the failed updateDelete .maintenance from the site root and finish the update by hand
Request rate limiting in the web serverA limiting requests line in the Nginx error log, with the zone name and the IPReview the limit_req rule and cache the expensive paths
A limit or an outage at the edge layerA 503 even for a static file, and no trace at all in the origin logReview the edge's rate limiting or traffic shield (on some services it is a multi-level setting, not a list of rules) and test the origin IP directly
A saturated or unreachable databaseErrors 1040, 1203 or 1226, and dynamic pages slowing down at the same timeFix slow queries, add indexes and raise the connection limit
A full disk, or a service that failed to startdf -h output near 100 percent, or a failed status for the PHP-FPM serviceClear out logs and temporary files, then start the service again

When 503 errors keep coming back, you have outgrown shared hosting

Take one pattern seriously: if the 503 doesn't come with an attack or an exceptional spike, but recurs under normal traffic, always at the same busy hours, it is not an error; it is a message. Your site's need for concurrency has outgrown the limits of the shared plan, and no setting on your side will move them. We compare the two models, and the break-even point for migrating, in shared hosting vs a VPS.

The trouble is that the decision to migrate keeps getting postponed, because nobody wants to buy a server for a month just to test a hypothesis. An hourly cloud server removes exactly that obstacle: create a cloud server (provisioning is automatic and usually takes under 60 seconds), bring up a copy of the site on it, tune the PHP-FPM pool with the formula from this article, and test it through those same busy hours.

If it works, you migrate; if it doesn't, you delete the server, and the cost is just those few hours. We explain the economics of this model in our guide to renting a server by the hour. And if you are not yet sure that migrating is the right path, the web hosting page lists the shared plans by hosting location; upgrading to a bigger plan is also done with a ticket, not from your panel settings.

A 10-step checklist for fixing a 503 error

The 503 is the best-behaved member of the 5xx family: the server is telling you it is alive and only lacks the capacity or the permission. All you need to do is peel the layers away from the outside in:

  • Read the response headers: the server value, and whether Retry-After is there or not.
  • Compare static and dynamic. An image returning 200 and a page returning 503 means the web server is healthy.
  • Read the code precisely. 508 means Entry Processes; 502 means the backend is unreachable.
  • Factor the web server into the diagnosis. On Nginx, a saturated PHP pool usually gives a 502 or 504, not a 503.
  • Grep the PHP-FPM log for max_children and listening queue.
  • Take the .maintenance file seriously only within ten minutes; after that, it has no effect.
  • Check the web server log for the rate-limiting line. A selective 503 means a rule is at work.
  • Check disk and memory with df -h and free -m.
  • For deliberate downtime, send a 503 with Retry-After: short and announced, not for weeks.
  • Treat repeated 503s under normal traffic as a signal to migrate, not as an invitation to raise the numbers.

Frequently asked questions

What does a 503 Service Unavailable error mean?

A 503 Service Unavailable error means the server is alive but cannot or will not answer right now, and the condition is temporary. The RFC 9110 standard gives the cause as a temporary overload or scheduled maintenance. In practice, it means the PHP process pool is full, the account has reached its resource limit, a rate-limiting rule has kicked in, or the site is deliberately in maintenance mode.

What is the difference between a 503 error and a 500 or 502 error?

Unlike a 500 or a 502, nothing is broken in a 503. In a 500, the application falls over while running its code, and the problem lies in the program or its configuration; in a 502, the web server in front gets no valid response from the backend service, or cannot reach it; in a 503, the service is up and deliberately not answering, because of a lack of capacity, rate limiting or maintenance mode. On Nginx, a stopped PHP-FPM usually gives a 502, not a 503.

How do I fix a WordPress 503 error after an update?

You fix a WordPress 503 after an update by deleting the .maintenance file, but only if less than ten minutes have passed since the failed update; the sign is a Retry-After header with a value of 600. Delete the file from the site root over SSH, the panel's file manager or FTP, then finish the update from the dashboard. After more than ten minutes, WordPress already ignores that file, so the cause is elsewhere: the hosting resource limits or a saturated PHP pool.

Does a 503 error hurt SEO?

A short 503 sent with a Retry-After header does no harm, and it is the most correct code for planned downtime, because Google does not index the temporary page in place of the real content. Letting it drag on is dangerous, though: Google's documentation says the crawl rate drops, and URLs that keep returning a server error are removed from the index.

How can I tell whether a 503 comes from my hosting or from a CDN edge?

To tell whether a 503 comes from your hosting or from a CDN, read the server value in the headers of a real GET response; if the edge service's name is there and no header from your own server comes with it, the response was generated at the edge. Avoid curl -I: it sends HEAD, and some edge services answer HEAD differently. A second test compares a static file with a dynamic page: if both get a 503 and the origin log is empty, the request never reached your host.

Related tutorials

Ready for some hands-on practice?

A Mehran Host hourly cloud server is ready in under 60 seconds. Practice as much as you like and pay only for the hours you use. Estimate the cost with the pricing calculator before you sign up.