The WordPress white screen of death is the one failure that gives you nothing to work with: no error text, no status code you can see in the browser, no clue which of your 30 plugins did it. Most guides hand you the same five steps — recovery mode, disable plugins, switch theme, raise memory, enable debug. Those steps are correct. They are also why people spend an afternoon disabling plugins on a site whose PHP was never broken in the first place.
A blank page has at least three distinct mechanisms behind it, and two of them do not respond to the standard checklist at all. Ten minutes spent identifying which one you have will save you the afternoon. (A fourth section below covers migrations — that is a situation that can trigger any of the three, not a mechanism of its own.)
Start here: which kind of blank page is it?
Open a terminal and run two commands against the broken URL. If you don’t have a terminal handy, the browser’s View Source and Network tab give you the same two facts.
curl -sI https://example.com/ | head -1
curl -s https://example.com/ | wc -c
The first tells you the HTTP status. The second tells you how many bytes of HTML the server actually sent. Between them you can sort the problem into one of four buckets:
- 500 status, near-zero bytes — a genuine PHP fatal error. The classic WSOD. Go to Type 1.
- 200 status, near-zero bytes — PHP died but errors are suppressed, or something exited early. Also Type 1, but recovery mode may not have fired.
- 200 status, thousands of bytes — your HTML is intact and something is hiding it. That is not a PHP problem at all. Go to Type 3.
- 200 status, but the HTML is a version of the page you already fixed — you are looking at cache. Go to Type 2.
That last one catches more people than anyone admits. You do the right fix, reload, still blank, conclude the fix didn’t work, and go break something else.
Type 1 — A real PHP fatal error
This is the case every tutorial is written for, so we’ll keep it tight.
Check the admin email first
Since WordPress 5.2, a fatal error triggers an email to the site admin address titled “Your Site Is Experiencing a Technical Issue.” It contains a recovery-mode link that logs you in with the offending component paused, and it names the exact file and line. This is the single fastest path to an answer and it is the step people skip most often, usually because the admin address is an old mailbox nobody reads.
If that email never arrives, that itself is a finding: your site probably cannot send mail at all. Worth fixing separately — a WordPress install with broken wp_mail() also means no password resets, no order notifications, and no form submissions reaching you. That has four possible causes of its own, and only one of them is fixed by an SMTP plugin.
Read the actual error instead of guessing
Edit wp-config.php and put this above the “That’s all, stop editing” line:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Reload the page, then read wp-content/debug.log. You get a filename and a line number. Everything below this point is guesswork by comparison — do this before you start disabling things.
Keep WP_DEBUG_DISPLAY off. Printing PHP errors to a live page leaks absolute server paths to anyone who visits.
Rule out plugins without the dashboard
If you can’t reach /wp-admin, rename the plugins directory over SFTP or SSH so WordPress cannot find it, then reload. If the site returns, create the directory again and move plugins back one at a time. Slow, but conclusive.
Fall back to a default theme
Same trick: rename the active theme’s folder. WordPress falls back to a bundled default. If the white screen clears, the fault is in the theme — very often in a recently edited functions.php.
Raise the memory limit
define( 'WP_MEMORY_LIMIT', '256M' );
Worth trying, but treat a memory fix as a diagnosis, not a cure. A stock WordPress site does not need 256 MB to render a page. If raising the limit fixes it, something is loading far more than it should, and you have just bought time rather than solved anything.
Type 2 — You already fixed it. Your cache is still serving the blank page.
Every caching layer between PHP and the visitor can capture a broken response and keep handing it out after the underlying fault is gone. There are usually more layers than people realise:
- Page cache plugins — WP Rocket, LiteSpeed Cache, W3 Total Cache. Purge all, don’t just purge one URL.
- Server-level cache — LiteSpeed’s built-in cache, Varnish, Nginx FastCGI cache. These live outside WordPress and a plugin purge may not touch them.
- OPcache — PHP keeps compiled bytecode in memory. Edit a PHP file over SFTP with a preserved timestamp and PHP can keep running the old broken version. Reload PHP-FPM, or call
opcache_reset(). - CDN — Cloudflare and friends will happily cache an HTML response. Purge everything, and check whether Development Mode is available to you while you work.
- The browser — test in a private window before you conclude anything.
A throwaway query string is a useful first probe, because most HTTP-layer caches treat it as a distinct URL:
curl -s "https://example.com/?cachebust=12345" | wc -c
If that returns a working page while the clean URL returns a blank one, an HTTP cache holding the broken response is the leading hypothesis — but it is a hypothesis, not a conclusion. An unexpected query parameter can also change routing or which template is chosen. Confirm it by reading the cache headers on the clean URL and by requesting the origin directly, before you spend time purging things.
Cache layers cause the mirror-image problem too: a page that is never cached at all because something sets a cookie on every response. That one shows up as a slow site whose checklist keeps coming back green.
But do not treat it as proof of anything when it fails to help. It cannot rule out OPcache, which caches compiled PHP in memory and does not care what the URL looks like. Some CDNs strip unknown query parameters from the cache key, so it may not bypass the edge either. And on some setups an unexpected parameter changes routing rather than bypassing cache. Confirm by comparing the clean URL against the origin directly, and read the cache headers:
curl -sI https://example.com/ | grep -iE "x-litespeed-cache|cf-cache-status|x-cache|age"
One more trap, and it is specific to OpenLiteSpeed rather than to LiteSpeed generally. OpenLiteSpeed does not re-read .htaccess per request the way Apache does — you can write a perfectly correct rule, verify the file content on disk, and see no change at all until the server is restarted. LiteSpeed Enterprise (LSWS) is built for Apache compatibility and reads .htaccess much more like Apache does, so advice written for one does not transfer to the other. Check which one you are actually on before you conclude your rule is wrong.
Type 3 — The page is there. CSS is hiding it.
This is the case that makes people lose entire days, because the standard checklist cannot find it. PHP is fine. The HTML is fine. The page returns 200 with 80 KB of markup. It just renders as an empty white rectangle.
An overly broad CSS selector is enough to do it. An attribute selector written to hide one decorative element — something like [data-footer] { display: none } — will match every ancestor carrying that attribute, and if one of them is a wrapper around the entire body, the whole page disappears while remaining perfectly present in the DOM.
Two checks separate this from a real WSOD in seconds:
- View source. Not Inspect Element — actual View Source. Look for a distinctive sentence from your own content, not for “is there a lot of HTML”. A fatal can occur after the header, navigation and a pile of inline scripts have already been printed, so a large response tells you nothing on its own.
- Check whether the response was cut off. A truncated page usually ends mid-tag, with no
</body>or</html>. That is a fatal mid-render, not a CSS problem.
Deliberately not offering a character-count threshold here. Byte size does not separate these cases in either direction: a short legitimate page can be hidden by CSS, and a broken page can be large. What separates them is whether your main content is present in the source — check for the content, not for the size.
A warning about a check you will see recommended elsewhere: document.body.scrollHeight does not distinguish these two cases. We tested both — a page whose wrapper is display: none and a page whose body is completely empty — and both returned exactly the viewport height, because body is stretched to fill the viewport either way. The number looks meaningful and tells you nothing.
Once you know the HTML is present, find which element is hiding it:
const el = document.querySelector('main, article, .site-content, #content, #primary');
if (!el) {
console.log('UNDETERMINED: no main-content element matched. ' +
'Find the real container in the DOM and re-run with its selector.');
} else {
let n = el, hidden = [];
while (n) { // include itself
const cs = getComputedStyle(n);
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0')
hidden.push([n.tagName + (n.id ? '#' + n.id : ''), cs.display, cs.visibility, cs.opacity]);
n = n.parentElement;
}
console.log(hidden.length
? hidden
: 'Ancestors are not hiding it. Check colour, z-index, or an overlay.');
}
Three outcomes, and they are genuinely different: a list of offending ancestors, “not hidden by ancestors”, or undetermined because the selector matched nothing. The third is not a pass — it means you have not run the test yet. Themes vary enough that a generic selector often misses, and treating a miss as “everything is fine” is how this check gets used to confirm the wrong conclusion.
When it is CSS, look at whatever you changed most recently: a customizer edit, a child theme stylesheet, a “hide this element” snippet copied from a support thread. Debug mode will never show you this, because nothing is broken as far as PHP is concerned.
The migration case — a situation, not a fourth mechanism
One clarification before the list: a site that went blank right after a move is not a fourth kind of failure. It is a situation that can produce any of the three above — usually a PHP fatal, sometimes a stale cache drop-in. It earns its own section because the likely causes are narrow and specific, not because it needs different diagnosis. Run the same status-and-bytes check first, then work this list.
The code was working an hour ago, so start with what changed around it rather than inside it. A blank page is only one of the things that breaks quietly after a migration.
- PHP version jump. Moving from PHP 7.4 to 8.x turns a pile of long-tolerated warnings into fatal errors. Older commercial themes and abandoned plugins are the usual casualties. Set the new host back to the old version, confirm the site returns, then upgrade deliberately.
- File ownership and permissions. Files unpacked as
rootthat the web server user cannot read produce a blank page with nothing useful in the WordPress log. Check the web server error log, not justdebug.log. - Absolute paths in wp-config.php. Constants such as
WP_CONTENT_DIR, or adefinepointing at a cache directory that only existed on the old server. - An object cache drop-in left behind. A
wp-content/object-cache.phppointing at a Redis instance the new server doesn’t run will take the site down before anything else loads. Rename that file first. - Missing PHP extensions. The old host had
imagickorintl; the new one doesn’t; a plugin calls it at load time.
If the site is being moved into or out of mainland China, add one more: hosting there requires an ICP filing tied to the domain, and a site that resolves but returns nothing may be blocked rather than broken. We cover that in hosting a website in China.
The plugins you cannot disable from the dashboard
Renaming wp-content/plugins is the standard advice, and it misses an entire category of code: must-use plugins. Anything in wp-content/mu-plugins/ loads on every request, cannot be deactivated from the admin screen, and is not affected by renaming the regular plugins folder. Recovery mode will not pause it either.
Hosts install them. Security plugins install them. So do migration tools, and so do developers who wanted a hook to load early. If you have exhausted the normal checklist, rename that directory too.
The same blind spot applies to drop-ins: object-cache.php, advanced-cache.php and db.php sit directly in wp-content/, load before almost everything, and never appear on the plugins screen.
Before you change anything
Take a copy of the database and the wp-content directory before the first edit. Not because these fixes are dangerous individually, but because a white screen usually means several people have already tried several things, and you want a point you can return to.
Edit PHP files with an editor, not a stream editor. Running a find-and-replace one-liner against a live wp-config.php or functions.php is how a recoverable problem becomes an unrecoverable one — a single unbalanced quote and the site is down for real. Whatever you change, run php -l yourfile.php before you reload.
When to stop and hand it over
Work through the four types above and most white screens resolve within the hour. Get outside help when:
- The debug log points into core files rather than a plugin or theme — that usually means a corrupted upload or a compromised install, not a bug.
- The site returns after you disable plugins but breaks again with each one re-enabled in isolation, which points at the theme or the database rather than any single plugin.
- You find code you did not write in
mu-pluginsor in a drop-in. Treat that as a security incident and stop making changes that overwrite evidence.
We repair WordPress sites for a living, mostly for export-facing businesses whose sites are visited from both sides of the Great Firewall. If you would rather not spend the afternoon on it, see WordPress support and bug fixes, or send us the URL and we’ll tell you which of the four types you’re dealing with before you commit to anything.
