How to Fix “500 Internal Server Error” on a WordPress Site
The web server hit something it could not handle and gave up. Read the error log, test .htaccess, then check permissions, the PHP handler and security rules, in that order.
Your homepage loads but every other page says 404 Not Found. The server is not handing pretty URLs to WordPress. Fix .htaccess, mod_rewrite, Nginx try_files or the rewrite rules.
Your homepage loads normally, but click any page, post or category and you get 404 Not Found. On Apache it often reads Not Found. The requested URL was not found on this server. On Nginx it is a bare white page that says 404 Not Found with nginx underneath. On LiteSpeed it is a plain error page with the LiteSpeed name at the bottom. Sometimes it is your theme’s own “Page not found” template instead, which points to a different cause.
Your content is almost certainly safe. Pretty addresses such as /about-us/ are not real folders on the server. They only work because the web server quietly hands every unknown address to WordPress’s index.php, and WordPress then works out which page you meant. When that handoff breaks, the homepage still works (it is index.php itself) and everything else fails. This usually appears right after a migration, a move to a new host or server type, a restore from backup, or a change to the Permalinks setting.
A request for https://example.com/about-us/ passes through two separate layers, and either one can produce the 404.
.htaccess. Nginx reads its own config files (it ignores .htaccess completely). The rule says: if the requested address is not a real file or folder, run /index.php instead. If this rule is missing or ignored, the server looks for a folder called about-us, finds nothing, and returns its own plain 404 without WordPress ever running.index.php runs, WordPress compares the path against a list of patterns stored in the database option rewrite_rules. A matching pattern turns about-us into a query like “the page with slug about-us”. If the list is stale or two patterns collide, WordPress runs fine but shows your theme’s 404 template.Telling these two apart first saves a lot of time:
| What the 404 looks like | Which layer failed | Go to |
|---|---|---|
| Plain server page (Apache, nginx or LiteSpeed branding, no theme design) | Web server never handed the request to WordPress | Steps 2 to 4 and Step 7 |
| Your theme’s styled “Page not found” on every page | WordPress ran but its rewrite rules are stale or empty | Step 5 |
| Theme 404 on one section only (a post type, a category, a single page) | A slug collision or a plugin’s rules | Step 6 |
Everything 404s inside a subfolder install such as /blog/ |
Wrong base path in the rules | Step 7 |
Redirect loops are a different problem, covered in the ERR_TOO_MANY_REDIRECTS guide (FG-03).
.htaccess (even an empty one) and any Nginx config file before you edit it, so you can put it back in seconds.wp, ls, apachectl, nginx or systemctl run in an SSH terminal on the server. Run wp commands from the site’s folder, the one that contains wp-config.php; on some hosts the command is wp-cli instead of wp. Commands starting with curl run on your own computer: macOS and Linux include curl, and Windows 10 and 11 include curl.exe in PowerShell (type curl.exe, not curl, which PowerShell treats as a different command), or you can use WSL. Blocks that start with #, <IfModule, <Directory or location are not commands: they are lines in a config file you edit.WordPress can always load a page through its plain query address, which needs no rewriting. Find a page ID (in the dashboard, hover a page title in Pages and read post= in the link), or over SSH:
wp post list --post_type=page --fields=ID,post_name,post_status
Type it as shown. --post_type=page limits the list to pages, and --fields chooses the columns. Good output is a table of IDs and slugs with status publish. An empty table means there are no pages, which is a database or migration problem rather than a rewriting one. Now open this address in a private window:
https://example.com/?page_id=POST_ID
Replace example.com with your domain and POST_ID with an ID from the list (use ?p=POST_ID for a blog post). If the page loads, WordPress and the database are fine and the problem is purely URL rewriting.
On your own computer:
curl -sI https://example.com/SAMPLE-PAGE/
Replace SAMPLE-PAGE with the slug of a real page. -s hides the progress meter and -I fetches headers only. Look at three things:
HTTP/2 404.server: header names the software answering (for example Apache, nginx, LiteSpeed or cloudflare).link: header containing rel="https://api.w.org/" means WordPress built the response. If that header is missing on a 404 but present on your homepage, the server answered alone, and the problem is in Steps 2 to 4.This is the most common cause after a migration. Files whose names start with a dot are hidden, and many FTP clients, zip tools and copy commands skip them. The site arrives without .htaccess, or with an empty one, and only the homepage works.
ls -la .htaccess && cat .htaccess
Run this over SSH in the site folder. -l shows details and -a includes hidden files. Bad output is No such file or directory, a size of zero, or contents with no # BEGIN WordPress section. In a file manager, switch on “show hidden files” (often in a settings menu) before you decide the file is missing.
Create or edit .htaccess in the site folder so it contains the standard WordPress block for a site installed at the domain root:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
There are no placeholders: paste it as shown. RewriteBase / says the site lives at the root. The two RewriteCond lines mean “only if the request is not a real file (!-f) and not a real folder (!-d)”, so images and CSS are served directly. The last rule sends everything else to /index.php, and [L] stops processing there. Keep any other blocks, such as a caching plugin’s, but leave them outside the WordPress markers, because WordPress rewrites everything between # BEGIN WordPress and # END WordPress when permalinks are saved.
Permissions of 644 are normal. If the file belongs to a different user than the site runs as (common after copying as root), WordPress cannot update it.
wp rewrite flush --hard
Type it as shown. --hard asks WP-CLI to regenerate .htaccess as well as the database rules. It prints Success: Rewrite rules flushed. even when it could not write the file, so check with cat .htaccess afterward. WP-CLI cannot detect Apache modules from the command line, so if the file is still not written, add these lines to a file named wp-cli.yml in the site folder and run the flush again:
apache_modules:
- mod_rewrite
Paste as shown. It tells WP-CLI to assume mod_rewrite is available. The hard flush works on single sites only, not multisite networks, and is pointless on Nginx.
A perfect .htaccess does nothing if Apache ignores it. This bites people moving to their own VPS, because the stock Apache 2.4 setting is AllowOverride None and mod_rewrite is often off.
Add a line of nonsense such as THIS_IS_A_TEST at the very top of .htaccess and load the homepage. If you get 500 Internal Server Error, Apache is reading the file: remove the line and move on to Step 5. If the homepage loads normally, Apache is ignoring .htaccess. Remove the line either way.
apachectl -M | grep rewrite
grep -Rn "AllowOverride" /etc/apache2/ /etc/httpd/ 2>/dev/null
Run over SSH as root. The first line lists loaded modules and keeps the rewrite line: good output is rewrite_module (shared), and no output means it is not loaded. The second searches the config folders (/etc/apache2/ on Debian and Ubuntu, /etc/httpd/ on RHEL family systems) and prints each AllowOverride line with its file and line number (-n); 2>/dev/null hides errors for the folder that does not exist. On Debian or Ubuntu, enable the module with a2enmod rewrite. Then find the <Directory> block for your document root and make it look like this:
<Directory /var/www/DOCUMENT_ROOT>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
DOCUMENT_ROOT is the path to the folder containing wp-config.php, as set by the DocumentRoot line in your virtual host file. AllowOverride All lets .htaccess use rewrite rules (the narrower AllowOverride FileInfo is enough for rewriting alone, but security and cache plugins often need more). mod_rewrite in .htaccess also needs FollowSymLinks or SymLinksIfOwnerMatch. Check the syntax with apachectl configtest (good output: Syntax OK), then apply it with systemctl reload apache2 (or httpd).
On shared hosting you cannot change these settings. If the typo test shows .htaccess being ignored there, open a ticket with the result.
If the server: header said nginx and there is no Apache behind it, .htaccess is irrelevant, however correct it is. Nginx needs its own instruction to fall back to WordPress.
nginx -T 2>/dev/null | grep -n "try_files"
Run over SSH as root. -T prints the full loaded configuration, and grep finds every try_files line. Good output includes a line ending in /index.php?$args; inside your site’s server block. Bad output is nothing, or a line such as try_files $uri $uri/ =404;, which is the stock default and causes exactly this symptom.
In your site’s server { } block, edit the location / block:
location / {
try_files $uri $uri/ /index.php?$args;
}
There are no placeholders; the $ words are filled in by Nginx. $uri tries the exact file, $uri/ tries a folder, and if neither exists the request goes to /index.php with the original query string ($args) attached, so search and tracking parameters still work. Your separate location ~ \.php$ block then passes it to PHP-FPM as usual. Test and apply:
nginx -t && systemctl reload nginx
nginx -t checks the syntax; good output ends with test is successful, and the reload only runs if the test passes. On managed Nginx hosting, ask your host to apply the standard WordPress try_files rule; they usually have a toggle or template for it.
If the 404 is your theme’s own template, the server is doing its job and WordPress’s list of patterns is the problem. It can be empty after an import, stale after a plugin added a post type, or cached in an object cache copied from another site.
Go to Settings, Permalinks and click Save Changes without changing anything. Visiting that screen and saving rebuilds the rules.
wp option get permalink_structure
wp rewrite list --format=count
wp rewrite flush
wp cache flush
Type these as shown. The first prints your structure, such as /%postname%/; an empty answer means plain permalinks are selected. The second counts the stored patterns: a normal site has well over a hundred, and 0 means the list is empty. wp rewrite flush rebuilds the list (good output: Success: Rewrite rules flushed.). wp cache flush empties the object cache, such as Redis or Memcached, which can otherwise keep serving the old, empty list; on a site with no persistent object cache it is harmless. To set the structure from the command line, use wp rewrite structure '/%postname%/' --hard, keeping the quotes.
If only one section 404s, look for two things claiming the same address. Typical examples: a page called portfolio and a custom post type whose archive is also /portfolio/, or a page called blog while the category base is blog.
wp rewrite list --match=/SECTION-SLUG/ --fields=match,query,source
Replace SECTION-SLUG with the path that fails, such as /portfolio/ or /portfolio/PROJECT-NAME/. --match shows only the patterns that would match that address, in the order WordPress tries them, and source shows where each came from (post, page, a taxonomy or a post type name). The first row wins. If the first row’s query points at a post type when you expected a page (or the reverse), you have found the collision. Then check what holds the slug:
wp post list --post_type=any --post_status=any --name=SECTION-SLUG --fields=ID,post_type,post_status
Replace SECTION-SLUG with the slug alone, without slashes. --post_type=any and --post_status=any include every type and status, including drafts. More than one row, or an unexpected type such as attachment, explains the clash.
Rename one side. Change the page slug in Quick Edit, or change the post type’s slug in the plugin that registers it (most post type plugins have a “rewrite slug” field), then save permalinks again.
If WordPress lives in a subfolder such as https://example.com/blog/, the rules must name that folder. Moving between a subfolder and the root often leaves the old path behind.
For a site served from /blog/, the .htaccess in the blog folder needs:
RewriteBase /SUBFOLDER/
RewriteRule . /SUBFOLDER/index.php [L]
Replace both SUBFOLDER tokens with the folder name, such as blog, keeping the slashes. These two lines replace the matching lines in the standard block from Step 2. If the site moved to the root, set them back to / and /index.php. On Nginx, the equivalent is a separate block:
location /SUBFOLDER/ {
try_files $uri $uri/ /SUBFOLDER/index.php?$args;
}
Again replace SUBFOLDER with the folder name. A special case is “giving WordPress its own directory”, where visitors use the root but the core files sit in a folder such as /wp/. There, the .htaccess and the small index.php live in the root, RewriteBase stays /, and the siteurl option points at the subfolder while home points at the root. Check both with wp option get siteurl and wp option get home.
LiteSpeed Web Server, which many hosts run in place of Apache, reads .htaccess and understands Apache rewrite syntax, so Steps 2, 5 and 7 apply unchanged. A few details differ:
.htaccess, and only if “Auto Load from .htaccess” is enabled in the WebAdmin console (under the server’s or virtual host’s Rewrite settings). It loads a folder’s .htaccess once and needs a graceful restart before later edits take effect, so a fixed file can look like it “did nothing”. On your own server, run /usr/local/lsws/bin/lswsctrl restart after editing. On hosting, open a ticket..htaccess, marked # BEGIN LSCACHE and # BEGIN NON_LSCACHE. These are normal. Make sure the # BEGIN WordPress block is still present after them and that nothing was pasted inside it.wp litespeed-purge all if the plugin’s WP-CLI commands are available, then test again.When the cause is not obvious, follow one request all the way through each layer instead of guessing.
On your own Apache 2.4 server you can make mod_rewrite log every decision. Add this inside your site’s virtual host, reload Apache, request one failing URL, then remove it:
LogLevel alert rewrite:trace3
No placeholders. It raises logging for mod_rewrite only, to trace level 3, while keeping everything else quiet. Read the site’s error log (often /var/log/apache2/error.log or /var/log/httpd/error_log) and look for lines containing [rewrite:trace. A healthy request shows RewriteCond: input='...' pattern='!-f' => matched followed by rewrite '...' -> '/index.php'. If you see no rewrite lines at all for the request, Apache never read the rules, which takes you back to Step 3. Remove it afterward: trace logging fills disks fast.
Install the free Query Monitor plugin, visit a failing URL while logged in, and open its Request panel. It shows the matched rewrite rule, the resulting query (such as pagename=about-us) and the query variables WordPress ended up with. A wrong match (such as attachment=about-us) points at Step 6; no match at all often means a disabled plugin used to register that post type.
Open a ticket if the deliberate typo test shows .htaccess is ignored, if the server is Nginx and you cannot edit its config, if you are on OpenLiteSpeed without restart access, or if the 404 persists with a correct .htaccess and freshly flushed rules. Include:
curl -sI for the failing URL, including the server: header.?page_id=POST_ID works or not)..htaccess, and the result of the typo test.Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Still stuck?
Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.
Get the free checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close