Your site returns “Resource Limit Is Reached”. Or it goes blank halfway through loading. Or it just gets slow every afternoon and the error log has nothing in it. You check your plan, it says unlimited CPU, and none of this adds up.
Almost none of that is a suspension. Nobody turned your site off. On a shared cPanel server running CloudLinux, you are looking at a kernel-level resource cap doing exactly what it was configured to do. Here is what those caps are, why each one fails differently, and how to read your own numbers before you open a ticket.
There are two layers, and only one is visible
Your cPanel package is the marketing layer. LVE is the enforcement layer.
The package controls what you see in cPanel: disk quota, bandwidth, addon domains, email, databases. CloudLinux’s LVE (Lightweight Virtual Environment) controls what you cannot see: CPU, memory, concurrent PHP workers, disk I/O. It enforces them with Linux kernel cgroups, one per hosting account. Hosting copy across this industry sells “unlimited” CPU while the kernel still holds each account to a fixed slice, which is why “unlimited, but my site 508s” is not a contradiction.
Limits resolve most-specific-first: a per-user override on one account beats the LVE package attached to the plan, which beats the server default in /etc/container/ve.cfg. The first match wins — a more generous server default does not raise you.
Per-account overrides are sticky. Headroom your host hand-set last year survives a package change and keeps overriding the new package, so the failure mode is the opposite of what people expect: you upgrade, pay more, and get nothing, because a stale override is still pinning the account to its old numbers. Overrides go away when an administrator removes them (lvectl delete <UID>), not when you switch plans. Worth asking about if an upgrade changed nothing.
LVE is scoped to the cPanel account, not the website. Six domains in one account share one CPU cap, one memory cap, one worker pool.
The three ways a limit fails
None of the three writes a plain “you hit a limit” line into your own site’s error log, which is why the most common resource ticket arrives as “my site is slow and there are no errors.”
| Limit | Enforcement | What visitors see | What you find in logs |
|---|---|---|---|
| SPEED (CPU) | Throttle | Slow page, HTTP 200 | Nothing |
| IO (KB/s) | Throttle | Slow page, worst on media | Nothing |
| IOPS (ops/sec) | Throttle | Slow page, worst on DB-heavy pages | Nothing |
| EP (entry processes) | Reject | HTTP 508 Resource Limit Is Reached | 508s in the access log; your PHP error log clean; the server’s Apache error log names it outright |
| NPROC (total processes) | Fork failure | 500, 503, blank or partial page | Usually nothing web-side; a shell or cron session shows fork: retry: Resource temporarily unavailable |
| PMEM (physical memory) | Kill | 500, 503, blank, or a page that stops mid-render | A kernel OOM line; in PHP either silence or Out of memory (allocated N) (tried to allocate M bytes) |
Throttle means nothing fails. The kernel hands the account less CPU or less disk bandwidth and the work still completes — later. A CPU-throttled site returns a valid HTTP 200 with a six-second time to first byte and no error anywhere. “Slow site, clean logs” is a signature, not a dead end.
Reject is EP. Entry processes are the number of dynamic requests executing at the same instant for that account. Not visitors, not requests per second — concurrency. A request that would push the account past the cap is refused at the web server door by CloudLinux’s mod_hostinglimits before PHP starts, and the visitor gets HTTP 508. Since concurrency equals arrival rate multiplied by execution time, a slow site with modest traffic can exhaust EP while a fast site with far more traffic never approaches it.
Your PHP error log stays clean, which is why people assume there is no evidence. There is: mod_hostinglimits logs every rejection to the server’s Apache error log.
[hostinglimits:warn] [pid NNNN:tid NNNN] mod_hostinglimits: Error on LVE enter:
LVE(NNNN) HANDLER(application/x-httpd-ea-php82) HOSTNAME(example.com)
URL(/index.php) TID(NNNN) errno (7) MHL-E2BIG - entry processes limit reached
That line names the limit, the account and the URL. If you are 508ing, ask your host for it.
Static files usually still serve, because images, CSS and JS typically do not enter the LVE, so the site looks half alive: the theme’s images load, the HTML page 508s. And it only bites at peaks, because it is a concurrency cap. You reload, it works, and you conclude your host is making it up.
Kill is PMEM. That cap covers the resident memory of every process the account runs, added together: all PHP workers, plus cron jobs, plus anything left open in a terminal. Exceed it and the kernel’s cgroup OOM killer terminates a process mid-execution. Sometimes PHP never gets to report anything and the page just dies. Sometimes — commonly under mod_fcgid, where each request is its own process — PHP notices the refused allocation and writes a fatal. That fatal is not the one you think it is.
NPROC deserves a warning because nobody gives one. It counts all processes, not just web ones, so an account at NPROC silently fails to start cron jobs and refuses to open a terminal in cPanel, with no obvious connection to the site being slow. Web requests just fail; the classic fork: retry: Resource temporarily unavailable is a shell message, so it shows up over SSH or in cron output, not in a site’s error log. Confirm it with the NprocF fault counter instead.
VMEM you can probably ignore. CloudLinux deprecated virtual memory limiting and recommends setting it to zero, and where it is zero no VMEM fault can occur. Older tutorials still discuss VMEM faults at length; check your own numbers instead. If lveinfo reports lVMem 0 for your account, the whole topic is dead on your server.
The mistake that makes it worse: raising memory_limit
A PHP memory_limit exhaustion and an LVE memory kill look identical in the browser — 500 or a blank page — and they are fixed in opposite directions.
| PHP memory_limit exhaustion | LVE PMEM kill | |
|---|---|---|
| What hit the wall | One PHP process exceeded PHP’s own per-process allowance | All of the account’s processes, summed, exceeded the kernel cap |
| Who enforces it | PHP, in userspace | The Linux kernel cgroup OOM killer |
| PHP error log | PHP Fatal error: Allowed memory size of N bytes exhausted |
Either silence, or PHP Fatal error: Out of memory (allocated N) (tried to allocate M bytes) |
| Reproducible | Yes, same page every time | No, intermittent and traffic-shaped |
| Blast radius | One request | Every site in that cPanel account, plus its cron jobs |
| Correct fix | Raise memory_limit, or fix the code that allocates too much | Cut total concurrent footprint, or move to more PMEM |
Read the string, not the silence. Allowed memory size of N bytes exhausted is PHP enforcing its own per-process ceiling, and raising memory_limit genuinely helps. Out of memory (allocated N) (tried to allocate M bytes) is different: PHP asked the kernel for memory and the kernel said no. That is the account cap, and raising memory_limit will not help. It will make it worse. A 500 or blank page with nothing in the log at all is the same cap, killing the process before it could complain.
Here is why it backfires. memory_limit is per process. PMEM is the sum across all of the account’s processes. Set memory_limit = 512M because a tutorial told you to, and four simultaneous requests, each legitimately growing to 300 MB, come to 1.2 GB. If the account cap is under that, the kernel starts killing workers — even though no individual process came near its 512 MB ceiling. So you follow the same advice again, raise it to 768M, and make things strictly worse.
Keep this model: memory_limit is a per-request ceiling, PMEM is the household budget. Raising the ceiling does not enlarge the budget. It lets each request spend more of it.
It runs the other way too: an account with generous PMEM will still hit a 32 MB PHP memory_limit fatal, and no LVE headroom fixes that. We chased one case where the customer had raised the limit everywhere the panel offers — MultiPHP INI Editor, .htaccess, a user php.ini — and it still failed, because the failing process was control-panel tooling running on the panel’s own bundled PHP binary, where none of those apply.
Proof, from the kernel
When we need to settle it, we read the kernel log, not the PHP log. On CloudLinux 7 and newer, an LVE memory kill looks like this (account ID and pid redacted).
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0-1,
oom_memcg=/lveNNNN,task_memcg=/lveNNNN,task=php-cgi,pid=NNNNN,uid=NNNN
Memory cgroup out of memory: Killed process NNNNN (php-cgi)
Two markers matter. constraint=CONSTRAINT_MEMCG means the kill was scoped to a memory cgroup — one account’s LVE, not the machine. oom_memcg=/lve<ID> names which account. A genuine server-wide out-of-memory event looks different: constraint=CONSTRAINT_NONE, with no cgroup scoping. Pull them with:
dmesg -T | grep 'oom-kill:'
Keep the colon; without it you also match the invoked oom-killer lines and roughly double your count. None of this applies on CloudLinux 6: dmesg -T is unsupported there and that kernel logs a beancounter format with no constraint= and no oom_memcg=. Use dmesg | grep -i 'oom-killer in ub' instead, where the ub number identifies the account.
On one of our shared cPanel nodes, the kernel logged 233 out-of-memory kills in a single 24-hour window while roughly three quarters of the machine’s RAM was still available (the available column in free -h — mostly reclaimable page cache). Every kill was cgroup-scoped, and two accounts produced all 233 of them. The server was not out of memory. Two accounts were. Adding RAM would have fixed nothing.
One correction, because it circulates widely: you cannot find these events by grepping /var/log/lve-stats.log. On current versions that file holds nothing but lve-stats daemon logging, and on CloudLinux 8 and 9 it is empty outright. It has never held fault or OOM records on any version. The statistics live in /var/lve/lvestats2.db and are read with lveinfo.
Match the symptom to the layer
| Symptom | Where the evidence is | What it means |
|---|---|---|
| 508 | Access log status field; server’s Apache error log for the MHL-E2BIG line | Entry processes exhausted. Concurrency. |
500, Allowed memory size fatal in the error log |
Error log, with file and line | Application bug or PHP’s own limit. Not LVE. |
500 or blank, error log silent or Out of memory (allocated ...) |
Kernel log: CONSTRAINT_MEMCG | LVE memory kill. |
| 503 | Web server or proxy layer, then NprocF and PMemF counters | Often PHP-FPM pool exhaustion or a service down — but NPROC and PMEM exhaustion also surface as 503, so check the counters before ruling LVE out. |
| 200 but slow, all logs clean | CPU and I/O fault counters | Throttling, which never writes an error. |
One more test, thirty seconds, often more decisive than any log line — it works because LVE is scoped to the account. All sites in one account degrade while other accounts on the server are fine: LVE limits on that account. Many unrelated accounts degrade together: server level. One site degrades while other sites in the same account are fine: that site’s own code, not a limit.
Read your own numbers: cPanel, Metrics, Resource Usage
That page gives you a live snapshot of CPU, entry processes, memory, I/O, IOPS and process count against your limits, plus a faults table covering the last day and longer. Three things about reading it.
- Read the faults column, not the usage percentage. A 4% average with 900 memory faults is a serious problem. A 60% average with zero faults is a healthy site using what it paid for. These caps bite in short bursts, so averages hide everything that matters.
- Identify which resource faulted, because the fix differs. Entry-process faults are a concurrency problem: cache. Memory faults are a per-worker footprint problem: lighter plugins, less concurrency. CPU faults mean execution time: slow queries and uncached pages. I/O faults are usually backups, logs, or uncached media.
- The live snapshot shows your current limits; the history keeps the old ones. lve-stats records the limit in force with each sample, so rows from before a plan change carry that plan’s numbers, which is what you want when reconstructing an old incident. The graphs are also per account, not per site: if several sites share one cPanel account, this page cannot say which of them is responsible.
What we run on the server side
lveinfo is the flight recorder — historical faults from collected statistics:
lveinfo --period=1d --by-fault=any --display-username
That is every account that hit any limit in the last 24 hours. --period takes values like 5m, 4h, 2d, today; --by-usage finds accounts approaching a limit, which is how you warn someone before it breaks.
The column convention is the key to the output. Every resource carries a prefix plus a fault counter: a for average over the period, m for maximum observed, l for the limit in force, and an f suffix for fault count — CPUf, EPf, PMemF, NprocF, IOf. Same rule as the cPanel page: ignore the averages, read the faults. An account averaging 6% CPU with a 96% peak and 86 CPU faults is fine most of the time and pinned against the wall at peaks — exactly what the customer is reporting, and exactly what the average hides.
Two traps. --show-all prints most of the fault counters but silently drops CPUf and EPf on lveinfo 3.x, and CPUf is missing from the default output as well. Name the columns you want or you will read a CPU-pinned server as clean:
lveinfo --period=1d --show-columns id,cpuf,epf,pmemf,nprocf,iof
And --by-fault=mep means max entry processes, not memory. To hunt memory kills, use --by-fault=pmem.
The limits themselves come from lvectl limits <UID>, which prints one account’s effective limits after override resolution in the order ID SPEED PMEM VMEM EP NPROC IO IOPS — SPEED being CPU percent, where 100 is one full core. During a live incident, lveps -t -s cpu shows who is burning the box this second.
What actually causes this on WordPress
Roughly in order of how often it turns out to be the answer.
- No page caching. The default state. Every visit runs the full WordPress bootstrap, every plugin, every query, so concurrency scales linearly with traffic — and EP is a concurrency cap. A modest traffic bump goes straight into 508s. Highest-leverage fix available on shared hosting.
- wp-cron firing on page loads. WordPress’s scheduler runs off a loopback HTTP request the site makes to itself, so on a busy site that is thousands of extra self-requests a day, each consuming an entry process. It shows in access logs as the server’s own IP with a
WordPress/x.xuser agent. SetDISABLE_WP_CRONand run WordPress cron from a real system cron every 15 minutes. Your site is DDoSing itself, politely. - Bot traffic and XML-RPC abuse. Crawlers and scrapers generate real, uncached, PHP-executing requests. Split your access log by user agent after excluding static files and look at the share that is not human. On accounts we investigate for entry-process faults it is routinely a large fraction, and often the majority. Pingback and brute-force amplification against
xmlrpc.phpis the same problem at higher concurrency; disable it if you do not use Jetpack or the mobile app. - 404 storms from vulnerability scanners. WordPress rewrite rules route requests for nonexistent
.phpfiles into WordPress, which serves a 404 page — but only after a complete WordPress bootstrap. (On sites with a 404-to-homepage redirect the scanner gets an HTTP 200 instead, which is why these scans read as hits in the logs.) A scanner probing a thousand plugin paths does not generate a thousand cheap static 404s; it generates a thousand full WordPress executions. We chased exactly this after a scanner report flagged plugin paths across a group of sites on one server: every flagged hit was a false positive, and every probe still cost a full bootstrap. A scan that finds nothing can still take your site down. - MyISAM tables and slow queries. MyISAM locks the whole table on write, so under concurrency requests queue behind each other, each holding an entry process while it waits — a database problem presenting as an EP problem. Convert to InnoDB. Same logic for heavy plugin JOINs and missing indexes: halving execution time halves concurrency at identical traffic.
- Leftovers. Stale
advanced-cache.phporobject-cache.phpdrop-ins still loaded on every request after the owning plugin was removed. Unbounded post revisions and orphaned tables.
All of it reduces to one thing: how many PHP processes are alive at the same moment. A bigger plan raises the ceiling; caching lowers the floor. The second is usually cheaper and always faster.
When more headroom is the right answer
Sometimes it is. But raising a cap on an uncached site fixes nothing on its own — it lets the account consume more before failing, and take a bigger bite out of its neighbours on the way. Fix the footprint first, then size the plan to what is left. If what is left still exceeds what a shared account allows, no amount of tuning changes that, and the workload needs resources of its own.
Limits are the reason your neighbour’s bad plugin is not your outage.
If you are troubleshooting right now: open cPanel, Metrics, Resource Usage, find the faults table, and screenshot it for the window when the problem happened. Note which resource faulted and when. That one screenshot turns “my site is broken” into a diagnosable problem.
Be First to Comment