Your WordPress site is blank. Here is what that actually tells you.
Most guides call a blank WordPress page a hidden PHP fatal error. In 2026 that is usually wrong. Since WordPress 5.2, core registers its own fatal error handler, catching E_ERROR, E_PARSE, E_USER_ERROR, E_COMPILE_ERROR and E_RECOVERABLE_ERROR and rendering a visible message: “There has been a critical error on this website.”, with an HTTP 500.
So a genuinely blank page – no error, no header, no partial layout – is itself a diagnostic signal. It means WordPress’s own handler either never ran or could not finish its own work. Here is how we route a blank-page ticket on our shared cPanel platform.
Step 1: get the HTTP status code and view source
Do not touch a plugin yet. Open DevTools (F12), Network tab, reload, click the top row (the document request) and read the Status column. Then press Ctrl+U to view source.
| What you see | What it means |
|---|---|
| Blank body, HTTP 200, source completely empty | Usually not a PHP fatal. PHP returned success and sent nothing, most often a cache file that was created but never written. The exception is a fatal that happened after headers were sent, or inside an output buffer that got discarded – that also yields 200 with an empty body. Check the error log before you commit to the cache theory. |
| Blank body, HTTP 500 | A fatal the handler could not report: a parse error in wp-config.php, a fatal in an early-loaded include, or core files damaged badly enough that the handler cannot complete. |
| “There has been a critical error on this website.” (500) | The handler ran. Most often the fatal is in a plugin or theme, and if WordPress can attribute it to a specific extension it emails a recovery link. Mu-plugins and cache drop-ins land here too, since they load after the handler is registered. No attribution means no email, and on multisite recovery mode is disabled entirely. |
| “Briefly unavailable for scheduled maintenance.” (503) | Leftover .maintenance file. Text, not a blank page, so not a white screen at all. |
| 508 “Resource Limit Is Reached” | An LVE limit was hit. Entry processes (concurrency) is the most common cause, but physical memory, CPU and IO faults can all surface as 508. cPanel Resource Usage is the only place that tells you which one, and when. |
| Truncated page, intermittent, nothing in any log | The kernel killed the process mid-request. Classic container memory kill. |
| Blank for visitors, fine when you are logged in | Strongly suggests a cached response is the problem – caching plugins and CDN edges both bypass the cache for logged-in users. Confirm it, because a theme template or a plugin that only runs for anonymous visitors produces the same split. |
That last row is the fastest discriminator in the list, but verify it rather than acting on it. Request the page as an anonymous client and read the headers:
curl -sI https://example.com/ | grep -i -E 'HTTP/|cache|age|x-litespeed|cf-cache'
A cache hit with an empty body points at the cache. No cache headers at all, and you are looking at code.
Zero-byte cached pages after a disk-full event
When a partition hits 100 percent, a full-page caching plugin still succeeds at creating the cache file. The inode allocation works. The write of the HTML does not. What is left is a 0-byte file the plugin treats as a perfectly valid cache entry, and every subsequent visitor gets HTTP 200, an empty body, and nothing in any error log.
Two properties make this nasty. It persists after the disk is freed, because clearing space does not purge the poisoned entry, so the story reads “the server had a problem hours ago, it was fixed, my site is still blank”. And the owner often cannot see it, because caching plugins bypass the cache for logged-in users.
WP-Optimize page cache lives at wp-content/cache/wpo-cache/<domain>/<url-path>/, holding index.html, index.html.gz and mobile.index.html. The diagnostic is zero versus non-zero: a healthy cached page runs from tens to hundreds of kilobytes, and you should read nothing into a file that is merely smaller than you expected. Same failure shape on WP Super Cache, W3 Total Cache, WP Rocket, LiteSpeed Cache and SpeedyCache; only the directory names differ.
The trap: do not delete all zero-byte files
Caching plugins deliberately place intentional 0-byte index.html guard files at cache-directory roots to block directory listing:
wp-content/cache/index.html
wp-content/cache/supercache/index.html
wp-content/cache/wp-rocket/index.html
wp-content/cache/speedycache/index.html
wp-content/cache/wpfc-minified/index.html
The mtimes on those run from 2016 through 2025 – years outside any current incident window. A blanket find -size 0 -delete takes them out along with the poisoned pages. Discriminate by modification time: the poisoned files were written inside the disk-full window, the guards were not.
Set the date to just before the partition filled; your host’s incident notification, or the timestamps in cPanel Resource Usage, will give you the window. Do not filter on -name '*.html' either – that misses index.html.gz, which is what gets served to any browser sending Accept-Encoding: gzip, so a sweep that looks clean leaves real visitors staring at a blank page.
# List first. Always list first.
find <cache-dir> -type f -size 0 -newermt "<YYYY-MM-DD of the disk-full event>" \
-printf "%TY-%Tm-%Td %TH:%TM %p\n"
# Only once the list looks right:
find <cache-dir> -type f -size 0 -newermt "<YYYY-MM-DD of the disk-full event>" -print -delete
Deleting a guard file by accident costs you only directory-listing protection, and a deleted poisoned page regenerates on the next request. The failure mode here is not data loss, it is not knowing what you changed – which is why you run the find with -printf before you run it with -delete.
Without SSH you do not need find at all. Hit Purge All in the caching plugin. If the plugin will not load, delete the whole wp-content/cache/ directory in cPanel File Manager; WordPress and the plugin recreate it, which sidesteps the guard-file problem entirely. Then check disk usage on the cPanel home screen. If you are at quota, that is the real cause and it will recur.
What this looked like for us
In July one of our shared cPanel nodes filled its root partition overnight. A customer opened a ticket the next morning: homepage completely blank. The disk had already been cleared by then, and the site was still blank, because the poisoned entry was still sitting there.
What matters is what happened next. Once we knew the signature we ran the same zero-byte sweep across every document root on that node, and found a second site silently hit by the same event: broken mobile page-cache files, no ticket, no complaint. The owner presumably thought their site was just flaky on phones. We cleared both sites and re-ran the sweep until it returned nothing.
Container memory kills versus PHP memory_limit
To a site owner these look identical. The fixes have nothing in common, and the standard advice makes one of them worse.
PHP memory_limit caps a single PHP process. PHP enforces it, notices, and writes a fatal to the error log before dying. CloudLinux LVE PMEM is a cgroup cap on the total resident memory of everything your account is running at once. The kernel enforces it by killing processes, and PHP never gets to report anything.
| Signal | PHP memory_limit | LVE container kill |
|---|---|---|
| Error log | Allowed memory size of 67108864 bytes exhausted, with an exact file and line |
Nothing. Apache may log “Premature end of script headers”, or nothing at all. |
| Pattern | Deterministic. The same URL fails every time. | Intermittent, and it tracks traffic. Fine at 3am, blank during a promo. |
| HTTP | 500 with the “critical error” page | 500 or 503, truncated or empty body, or a 508 – which on its own does not tell you which limit was hit. |
| Where you see it | cPanel Metrics then Errors, or the error_log beside the failing script |
cPanel Resource Usage, showing a Physical Memory fault with a timestamp. The only place a non-SSH customer can see it. |
| Fix | Raise memory_limit, then find the plugin that needed 500MB |
The cap is enforced outside your account, so nothing you set in WordPress or .htaccess moves it. WP_MEMORY_LIMIT does nothing here. |
If the real ceiling is the container, raising WP_MEMORY_LIMIT makes things worse. You are letting each worker grow larger, so concurrent workers reach the account-wide cap sooner and get killed harder. The intuitive fix points the wrong way.
Rule of thumb: if the log says “Allowed memory size … exhausted”, it is PHP. If the log says nothing and the failure tracks your traffic, it is the container.
67108864 bytes is 64MB, but “64MB is the shared hosting default” is folklore. The limit is set per EA-PHP version on each individual server, and it does not rise with version number. Measured on one of our shared nodes in March 2026:
| EA-PHP version | memory_limit on that node |
|---|---|
| PHP 7.2 | 32M |
| PHP 8.1 | 512M |
| PHP 8.2 | 256M |
Those are that server’s configured values, not vendor defaults – upstream php.ini-production ships 128M and every host edits it. The point is that you cannot assume: check the version you are actually moving to before you treat a PHP upgrade as a memory fix.
One more tell. If your error says 41943040 bytes (40M) and you have already raised the limit in MultiPHP INI Editor, .user.ini, .htaccess and wp-config.php with no effect, you are not looking at the PHP that serves your website. 40M is WordPress’s own WP_MEMORY_LIMIT default, and WordPress raises the running PHP limit to exactly 40M whenever the binary is set lower than that. So a 41943040 error means the PHP that ran the operation was capped below 40M and your per-domain settings never entered the picture. That is usually the EA-PHP system binary at /opt/cpanel/ea-phpXX/root/usr/bin/php, which is what WP Toolkit, CLI and cron contexts use. The fix is host-side and system-wide for that EA-PHP version: WHM, MultiPHP INI Editor, Editor Mode, select the ea-php version, raise memory_limit.
Zero-filled core files after a migration
We hit this after a server evacuation where the file-level copy did not preserve sparse extents: files arrived with the correct byte size and nothing but NUL bytes inside them. A crashed rsync, or one that ran into a full disk, produces the identical result.
The signature is deceptive. ls -l shows a plausible size. php -l file.php reports “No syntax errors detected”, because an all-NUL file parses as an empty file, so your syntax checker actively tells you it is fine. The reliable detector is strings file.php | wc -l, which returns 0 where a real core file returns hundreds.
Because the file is effectively empty, everything it defined never exists, and you get Class "wpdb" not found, Class "WP_HTML_Tag_Processor" not found, or Call to undefined function get_option(). Those fire after the fatal error handler is registered, but the handler itself depends on core files that are also corrupted – it needs a working wpdb to reach recovery mode and render its template – so it dies too, and you get a genuinely blank 500 instead of the “critical error” message.
Telling it apart from a plugin fatal: disabling every plugin and switching to a default theme changes nothing, the error path is always under wp-includes/ or wp-admin/ and never wp-content/, and it starts after a migration or restore rather than after an update. Detect it with wp core verify-checksums, which compares your files against WordPress.org’s published checksums for your installed version.
Fix it by reinstalling core at the exact version already installed, never at “latest”. You are mid-incident; do not stack a version upgrade with unknown plugin compatibility on top of a database still at the older schema.
Read the version out of the file first, before you overwrite anything. wp core version reads wp-includes/version.php, which is one of the files that may itself be zero-filled – if it comes back empty, get the version from your backup or your host, and do not fall back to latest.
grep "wp_version =" <docroot>/wp-includes/version.php
tar -czf ~/core-backup-$(date +%F).tar.gz -C <docroot> wp-includes wp-admin \
wp-config.php .htaccess wp-settings.php wp-load.php wp-blog-header.php index.php 2>/dev/null
tar -tzf ~/core-backup-$(date +%F).tar.gz | wc -l # expect several thousand entries
wp core download --force --skip-content --version=<the version you just read>
The 2>/dev/null matters: on a site with no .htaccess, tar still writes a valid archive but exits non-zero with a “Cannot stat” error, which is the last thing you want from the safety step that gates a destructive command. Count the entries instead of trusting the exit status.
wp core download --force --skip-content replaces core files only and never touches wp-content/, wp-config.php or .htaccess. Without --skip-content it also writes the bundled themes and Akismet into your wp-content/ – and the backup above does not cover wp-content/, so that overwrite is not recoverable from it. By hand, use rsync -a --exclude 'wp-content/' wordpress/ /path/to/site/. Do not use cp -r wordpress/* /path/to/site/, for the same reason. Configuration is safe either way, since the tarball ships wp-config-sample.php, not wp-config.php.
If it is still broken afterwards, clear wp-content/cache/, then move wp-content/object-cache.php and wp-content/advanced-cache.php aside. Those drop-ins load early and a fatal inside one presents as the same blank page. On accounts with addon domains, each domain is a separate install, so check per document root.
Plugin and theme fatals, and what recovery mode really does
If you got “There has been a critical error on this website.”, WordPress may have emailed a recovery link. That email fails more often than the documentation suggests:
- It is only sent when WordPress can attribute the fatal to a specific plugin or theme. If it cannot, you get the page and no email. On multisite, recovery mode is disabled outright.
- There is no magic URL. Recovery mode is only enterable through a signed, single-use link of the form
wp-login.php?action=enter_recovery_mode&rm_token=...&rm_key=.... Any guide telling you to visit?wp-recovery-mode=1is describing a parameter that does not exist. - It is rate limited to one email per day by default, so reloading the broken site will not produce another for 24 hours.
- It goes to Settings, General, Administration Email Address, which is often not your user account’s email and on handed-over sites is often a dead mailbox.
- On shared hosting it frequently never arrives, because the admin address is on the same broken domain or the site’s outbound PHP mail fails SPF or DKIM checks.
- It only pauses plugins and themes. It does nothing for corrupted core, a poisoned cache file, or a container kill.
So the file-level fix is the dependable path, not the fallback. Rename the plugin folder over SFTP or File Manager, or rename wp-content/plugins/ itself to disable everything. One thing nobody mentions: WordPress records the missing plugins as deactivated in the database on the next dashboard load, so renaming the folder back does not re-enable them. You reactivate from the Plugins screen. When you bisect, disable half at a time. Ten plugins is four reloads, not ten.
With shell access there is a faster path. WP-CLI bootstraps WordPress, so on a fatally broken site most wp commands fatal too. Skip past the broken extension:
wp --skip-plugins --skip-themes plugin list
wp --skip-plugins --skip-themes plugin deactivate <plugin-slug>
While the site still works, point recovery mail somewhere you control, on a domain that is not the site’s own: define( 'RECOVERY_MODE_EMAIL', 'you@example.com' );
Three smaller causes worth knowing
.htaccess under PHP-FPM. Forget “botched permalink change”. On cPanel the real cause is bare php_value or php_flag directives in .htaccess. Only mod_php and LSAPI understand those; under FPM, Apache returns an immediate 500. This bites everyone who copy-pastes a “raise your memory limit” snippet from a forum. Test by renaming the file, then move the setting into .user.ini or the MultiPHP INI Editor.
The wrong PHP version is running. On cPanel with CloudLinux, MultiPHP Manager (per domain) wins. The CloudLinux PHP Selector only takes effect when CageFS is enabled, the domain’s MultiPHP version is left at the system default, and the selector is set to something other than “native” – set MultiPHP to a specific version and the selector is bypassed for that domain. Command-line php -v answers a third question again: inside CageFS it reports the user’s PHP Selector build, outside it the EA4 system default, and neither is necessarily what serves your website. Ground truth is the runtime:
echo '<?php echo phpversion();' > public_html/phpver.php
# visit https://yoursite.com/phpver.php, then delete the file
Maintenance mode, which is not a white screen. During updates WordPress writes a .maintenance file in the WordPress root – the directory that holds wp-admin/ and wp-includes/ – not in wp-content/. Do not use wp-config.php as the landmark; it legitimately sits one level above that directory in plenty of installs. While the file is active you get visible text and an HTTP 503, not a blank page, and it expires on its own: the file carries a timestamp and WordPress stops honouring it after ten minutes. A maintenance page that genuinely persists usually means a cached copy of the 503 is still being served by your caching plugin or CDN, which loops straight back to the cache section above.
Where PHP errors actually land on cPanel
- Metrics, Errors. The last 300 entries of your web server error log. On an actively erroring site that can be ninety seconds of history, and it covers web server errors only.
- A file named
error_log, no extension. It is written into the directory of the script that failed, not a fixed location, so a plugin fatal usually produceswp-content/plugins/<plugin>/error_lograther than one at your document root. Search the whole account for it. Turn on Settings, Show Hidden Files, or you will never see.htaccess,.user.inior.maintenance. - Resource Usage. Which limit was hit and when. The answer to “my error log is empty but my site keeps dying”.
wp-content/debug.log, which needs all three constants below.WP_DEBUG_LOGandWP_DEBUG_DISPLAYare ignored unlessWP_DEBUGis true, so setting the log constant on its own produces no file at all and you wrongly conclude the site threw no errors.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
WP_DEBUG_LOG also accepts a path, so you can log outside the web root.
Prevention, and when to escalate
- Watch your disk. The nastiest failure here is a downstream symptom of a full partition, and it is not fixable from inside WordPress.
- Do not reflexively set
WP_MEMORY_LIMITto 512M on shared hosting. On a container-limited platform that can make you less stable. - Stay on a supported PHP version for the security fixes, but check the memory limit on the version you are moving to rather than assuming it goes up.
- Set
RECOVERY_MODE_EMAILand the three debug constants now, while the site works, so the escape hatch and the evidence both exist before you need them. - Stage updates, and keep backups you have restored from at least once. A backup you have never tested is a hypothesis.
- Escalate for container limits, which are enforced outside your account and cannot be moved by any setting you control – send a Resource Usage screenshot with the timestamp and skip twenty minutes of back and forth. Escalate too for a file that looks the right size but is empty, and for a PHP version that will not change no matter what you set.
Related reading
- Fix WordPress White Screen After Plugin Updates
- Set Up Redis Object Cache for WordPress
- Moving from Shared Hosting to a Cloud VPS
The two hardest causes here – a zero-byte cache file left behind by a full disk, and a container memory kill – are not WordPress problems at all. They are disk and memory problems that surface as a blank page, and neither is visible from inside wp-admin. That is also why, when the cause turns out to be server-side, the right move is to sweep every account the event touched rather than closing the one ticket in front of you. In July that turned one reported blank site into two fixed ones.
Be First to Comment