Troubleshooting

500 Internal Server Error on your server: read the log, then fix the cause

A 500 means your app or web server hit an error it could not handle. Read the log, then fix PHP fatals, memory_limit, .htaccess, permissions or a full disk.

Published ~9 min read

Short answer: a 500 Internal Server Error means your server or the application behind it hit an error it could not handle while building the response. The page itself tells you nothing; the error log names the exact cause. On a VPS it is almost always one of these: a PHP fatal error or uncaught exception, PHP’s memory limit, a broken .htaccess or web server config, files the app cannot write, a full disk, or an unhandled exception in a Node or Python app behind the proxy.

Debian · Ubuntu · read the log that matches your stack
# Reproduce once, then read the newest lines
sudo tail -n 30 /var/log/nginx/error.log           # nginx (+ PHP-FPM errors)
sudo tail -n 30 /var/log/apache2/error.log         # Apache
sudo tail -n 30 /var/log/php8.3-fpm.log            # PHP-FPM itself (8.2 on Debian 12, 8.4 on Debian 13)
sudo journalctl -u myapp -n 50 --no-pager          # Node / Python app run by systemd
docker logs --tail 50 mycontainer                  # app in Docker

# Or watch live while you reload the page
sudo tail -f /var/log/nginx/error.log

Not sure which PHP version runs? ls /run/php/ shows the socket, and the log file name follows it. Work through the causes below in the order the log points you to, not top to bottom.

Who generated the 500?

Behind a reverse proxy, the 500 may come from nginx itself or be passed through untouched from the app. The error page is a quick fingerprint:

What you seeGenerated byWhere to look
Plain page, title 500 Internal Server Error, footer nginxnginx itselfnginx error.log: config loops, temp-file permissions
The server encountered an internal error or misconfiguration…Apacheapache2/error.log: .htaccess, modules, PHP via AH01071
Blank white page, or There has been a critical error on this websitePHP (WordPress shows the second)PHP message lines in the web server or FPM log
Server Error (500) or Internal Server Error, no stack traceDjango, Flask, Express in productionjournalctl for the app's service, or docker logs
A Cloudflare-branded error pageCloudflare itself or a WorkerCloudflare status and Worker logs; your origin may be fine

The response headers tell you the same without reading the page. Replace the URL with the one that fails:

Linux · macOS
curl -s -o /dev/null -D - https://example.com/failing/page | grep -iE '^(HTTP|server|x-powered-by)'
Example output (httpbin.org/status/500)
HTTP/2 500
server: gunicorn/19.9.0

Here the Server header names gunicorn, so the Python app server produced the 500 itself. Behind Cloudflare that header always reads cloudflare, so test the origin directly with --resolve as in the error 522 guide.

The HTTP header checker shows the status code and headers from outside your network, and says when a CDN’s bot protection answered instead of your server.

Why does PHP hide the message? Production php.ini files on Debian and Ubuntu set display_errors = Off. When a fatal error happens and no output has been sent yet, PHP then switches the status to 500 and logs the message instead of printing it. If some HTML was already sent, you get a half page with status 200, which is why a blank or cut-off page and a 500 are often the same bug.

Cause 1: a PHP fatal error or uncaught exception

Under nginx, PHP-FPM sends errors back over FastCGI and nginx logs them; under Apache with PHP-FPM they appear as AH01071: Got error. The line you are looking for looks like this:

/var/log/nginx/error.log
[error] 1234#1234: *57 FastCGI sent in stderr: "PHP message: PHP Fatal error:
  Uncaught Error: Call to undefined function mysqli_connect() in /var/www/site/db.php:12"
  while reading response header from upstream, client: 198.51.100.7, ...

The message usually names one of three things:

  • Call to undefined function (mysqli_connect, imagecreatefromjpeg, mb_strlen): a PHP extension is missing, very often after a PHP version upgrade, because extensions are packaged per version. Install it for the version FPM runs, then restart FPM.
  • Parse error / syntax error: a file was edited or deployed half-way. Check it with php -l.
  • Uncaught exception in your code or a plugin: the file and line point at it. In WordPress, rename the plugin’s folder in wp-content/plugins to disable it.
Debian · Ubuntu
# Syntax check a file
php -l /var/www/site/db.php

# Which extensions does FPM have? Install the missing one for that version
sudo php-fpm8.3 -m | grep -i -E 'mysqli|gd|mbstring|intl|curl|xml|zip'
sudo apt install -y php8.3-mysql php8.3-gd php8.3-mbstring
sudo systemctl restart php8.3-fpm

Cause 2: PHP’s memory_limit

Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) is a fatal error like any other, so it also ends in a 500. 134217728 bytes is 128 MB, PHP’s default limit. Large imports, image processing and page builders hit it first. Raise it for the pool rather than globally:

/etc/php/8.3/fpm/pool.d/www.conf
php_admin_value[memory_limit] = 256M
Debian · Ubuntu
sudo php-fpm8.3 -t && sudo systemctl restart php8.3-fpm

On shared setups without root, put memory_limit = 256M in a .user.ini file in the site root instead; PHP-FPM caches those files for five minutes by default, so the change is not instant. Before you raise the limit, do the maths: every PHP-FPM worker may use up to that much, and pm.max_children workers run at once. A limit that lets the workers together outgrow your RAM trades 500s for OOM kills and 503 errors.

Cause 3: a broken .htaccess or Apache config

Apache reads .htaccess on every request, so one bad line returns 500 for the whole directory tree. The error log is explicit:

Log messageMeaningFix
Invalid command 'RewriteEngine', perhaps misspelled or defined by a module not included in the server configurationmod_rewrite is not loadedsudo a2enmod rewrite
Invalid command 'php_value'php_value only works with mod_php, not PHP-FPMMove the settings to .user.ini or the FPM pool
Options not allowed hereAllowOverride does not permit that directiveAllow it in the vhost or remove the line
Request exceeded the limit of 10 internal redirectsA rewrite rule loopsAdd a RewriteCond that stops the loop
Debian · Ubuntu · Apache
sudo apache2ctl configtest
sudo a2enmod rewrite && sudo systemctl restart apache2

# Quick test: does the 500 disappear without .htaccess?
sudo mv /var/www/site/.htaccess /var/www/site/.htaccess.off

Debian and Ubuntu ship Apache with AllowOverride None for /var/www, which means .htaccess is ignored entirely until you allow it in the vhost. If your file seems to have no effect at all, that is why.

The nginx equivalents

nginx has no per-directory files, but two config mistakes produce its own 500 page:

  • rewrite or internal redirection cycle while internally redirecting to "/index.php": the last argument of try_files sends the request back into the same location, where it never resolves to a file. Usually root points at a directory without index.php (a moved site or a new release path), or the location ~ \.php$ block that should hand it to PHP-FPM is missing. Fix the path or the location, then sudo nginx -t && sudo systemctl reload nginx.
  • open() "/var/lib/nginx/body/…" failed (13: Permission denied) on uploads: nginx buffers larger request bodies to disk and cannot write there, often after the user directive was changed. Give that user ownership of /var/lib/nginx.

Cause 4: the app cannot write where it needs to

Many frameworks crash outright when they cannot write a log, cache or session file. Laravel is the textbook case: it needs to write to storage and bootstrap/cache, and when it cannot, the log line reads The stream or file "…/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied. It typically appears after a deploy or git pull as root.

Debian · Ubuntu
# See permissions on every directory along the path
namei -l /var/www/app/storage/logs/laravel.log

# Give the web server user only the directories it must write
sudo chown -R www-data:www-data /var/www/app/storage /var/www/app/bootstrap/cache
sudo find /var/www/app/storage /var/www/app/bootstrap/cache -type d -exec chmod 775 {} +

Cause 5: the disk or its inodes are full

When the disk is full, PHP cannot write sessions, caches cannot be updated and the database refuses writes, and each of those can surface as a 500 on pages that worked an hour ago. Running out of inodes (millions of small session or cache files) has the same effect with free space still showing.

Debian · Ubuntu
df -h /          # space
df -i /          # inodes
sudo du -xh --max-depth=1 /var | sort -h | tail -n 8

The guide to No space left on device covers what is safe to delete. If the data has simply outgrown the disk, VMHeaven VPS plans can take extra storage blocks bought from the customer panel on the running server. The disk grows right after payment; grow the partition and the filesystem afterwards to use the new space.

Cause 6: the database is down or refuses the app

WordPress answers a failed database connection with “Error establishing a database connection” and status 500; other apps log a PDO or connection exception. Check the service and the credentials the app uses:

Debian · Ubuntu
sudo systemctl status mariadb --no-pager     # or: mysql, postgresql
sudo journalctl -u mariadb -n 30 --no-pager
mariadb -u appuser -p -h 127.0.0.1 appdb -e 'SELECT 1'   # MySQL server: mysql instead of mariadb

A database that the kernel killed for memory shows up in sudo journalctl -k | grep -i oom. What to do about it is in OOM-killed process.

Cause 7: a Node or Python app behind the proxy

When nginx proxies to an app, it passes the app’s 500 through as is: the access log records the 500, but the nginx error log stays empty. The traceback is in the app’s own output:

  • Express logs unhandled errors to stderr by default, so they land in journalctl -u yourapp or docker logs. With NODE_ENV=production the visitor only sees the status text.
  • Gunicorn and Flask also write tracebacks to stderr, which systemd collects in the journal.
  • Django is the trap: with DEBUG = False its default logging only e-mails errors to ADMINS and prints nothing. If you never set that up, 500s vanish without a trace. Add a console handler:
settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {"django.request": {"handlers": ["console"], "level": "ERROR"}},
}

Never switch DEBUG, APP_DEBUG or display_errors on in production to read an error: the debug pages show environment variables and credentials to whoever triggers them. Read the log instead.

Turning on WordPress’s own log

If the PHP message is not specific enough, WordPress can log its notices too. Add these to wp-config.php above the “stop editing” line:

wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

Errors go to wp-content/debug.log. That file is readable over the web by default, so switch the constants back and delete it once you have what you need.

How to prevent 500 errors

  • Test configs before reloading: sudo nginx -t, sudo apache2ctl configtest, sudo php-fpm8.3 -t.
  • Lint and stage deploys: run php -l or your test suite in the deploy script, and switch releases with a symlink instead of copying files over a live site.
  • Back up before upgrades: a PHP or plugin upgrade is the most common trigger. Take a backup first (on a VMHeaven VPS, from the panel) so a bad upgrade is a rollback, not a rebuild.
  • Watch disk and memory: alert at 85 % disk use and keep log rotation on for app logs, not only system logs.
  • Size the server for the stack: PHP sites with many plugins need RAM for FPM workers, and each request runs on a single core. A Standard KVM plan gives memory headroom, and Hi-CPU KVM is built for workloads that lean on one or two cores rather than many. If you would rather not run PHP-FPM yourself, our Plesk hosting includes daily backups with up to 10 restore points and the WordPress Toolkit.

If nginx shows a 502 instead of a 500, the app did not answer at all rather than answering with an error, and the fix is a different one.

Frequently asked

Where do I find the cause of a 500 Internal Server Error?

In the error log of whatever generated it. On Debian and Ubuntu that is /var/log/nginx/error.log or /var/log/apache2/error.log for the web server and PHP, the PHP-FPM log in /var/log, and journalctl -u yourapp or docker logs for Node and Python apps. Reload the failing page once, then read the newest lines.

Why does my PHP site show a blank page or a 500 instead of the error message?

Production PHP has display_errors turned off, so a fatal error is written to the log and PHP answers with status 500 instead of printing the message. If some output was already sent, the page is cut off with status 200 instead. Read the log rather than turning display_errors on, which can expose paths and credentials to visitors.

Can a full disk cause a 500 Internal Server Error?

Yes. With no free space or no free inodes, PHP cannot write session and cache files and the database refuses writes, and apps often turn those failures into a 500. Check with df -h and df -i, then free space or add storage.

Related articles

All troubleshooting