Diagnosing Drupal Fatal Errors After Deploy or Cache Rebuild
A step-by-step diagnostic guide for Drupal's generic fatal error page after deploy or cache rebuild. Covers OPcache staleness, DB connectivity, config sync mismatches, and file permissions — with ordered checks, targeted fixes, and clear escalation criteria.
16 Apr 2026, 04:30 UTC

Recognizable Condition
After a code deploy, module enable, or drush cache:rebuild, users hit Drupal's generic error page — "The website encountered an unexpected error" — or a blank white screen. No stack trace appears unless error verbosity is temporarily raised. The issue may affect all users or only a subset, and it often coincides with a recent change to code, configuration, or infrastructure.
Assumptions
- Composer-managed Drupal 10/11 on Linux with PHP-FPM and MySQL or PostgreSQL
- Drush available on the server or via a deployment pipeline
- Configuration sync directory lives outside the webroot (e.g.,
../config/sync) - A staging environment mirrors production for safe reproduction
If any assumption is false, adapt the checks accordingly — for example, on Pantheon or Platform.sh, PHP-FPM restarts and file permissions are managed by the platform.
Cause and Diagnostic Quick-Reference
| Symptom | Likely Cause | First Diagnostic Step |
|---|---|---|
| Blank page for some users only | Stale OPcache or multiple PHP-FPM workers serving old code after deploy | Check opcache.validate_timestamps in php.ini; restart PHP-FPM |
| PDOException or "SQLSTATE[HY000]" in logs | Wrong $databases credentials, DB down, or network block | Run drush status and drush sql:connect |
| Missing config entities after sync | core.extension mismatch between exported and active config | Compare exported config with drush config:status and run import with --diff |
| "The directory ... is not writable" or file upload fails | Incorrect ownership, SELinux context, or missing file_private_path | Check ownership of sites/default/files, private, and tmp directories |
Ordered Diagnostic Checks
- Reproduce and capture context. Note the exact timestamp, URL, HTTP method, and user role (anonymous, authenticated, admin). Open the browser dev tools Network tab to confirm a 500 or 200-with-empty-body response.
- Read recent logs. On the server, check:
Run these as the deployment user or root. Look for# Drupal watchdog (dblog) via Drush drush watchdog:show --tail=50 --severity=error # PHP-FPM error log (path varies by distro) tail -n 100 /var/log/php-fpm/error.log # Syslog if Drupal logs there grep -i drupal /var/log/syslog | tail -20PDOException,ContainerBuildererrors, orfile_permissionswarnings. - Check status report if admin UI reaches. Visit
/admin/reports/statusand/admin/reports/dblog. If the site returns a 500 on those paths too, skip to Drush. - Run Drush baseline.
drush status # Verify Drupal root, DB connection, config sync path, and PHP version drush cache:rebuild # If this fails, the error output often pinpoints the cause - Validate configuration sync.
drush config:status # Shows counts of new, changed, removed config objects drush config:import --diff --dry-run # Review the diff before committing - Inspect recent changes and permissions. Compare the deployed commit with the previous release. Verify file ownership:
# Typical ownership for PHP-FPM user www-data ls -la sites/default/files/ ls -la ../config/sync/ stat -c '%U:%G' sites/default/files/private
Targeted Fixes Tied to Findings
Stale OPcache or Partial Container
If the error appears intermittently across requests, OPcache is serving stale bytecode. Fix:
- Deploy atomically (symlink swap or rsync
--delay-updates). - Restart PHP-FPM:
systemctl reload php8.3-fpm(adjust version). - If
opcache.validate_timestamps=0(common in production), also clear OPcache viaopcache_reset()in a post-deploy script or restart. - Run
drush cache:rebuild. - Verify with repeated anonymous requests:
for i in {1..10}; do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/; done— all should return 200.
Risk: Restarting PHP-FPM drops active sessions. Schedule during low traffic or use a rolling restart if multiple workers exist.
Database Credentials or Connectivity
If logs show SQLSTATE[HY000] [2002] Connection refused or Access denied:
- Confirm
settings.local.phpor environment variables inject correct$databasesvalues. - Test connectivity:
drush sql:connectshould drop you into a MySQL/psql prompt. - Check grants:
SHOW GRANTS FOR 'drupal_user'@'%';. - Clear caches after credential fix:
drush cache:rebuild.
Risk: Editing settings.php directly on production is error-prone; use a secrets manager or deploy-time injection.
Configuration Mismatch
When drush config:status shows unexpected changes or core.extension differs:
- Enable maintenance mode:
drush state:set system.maintenance_mode 1 --input-format=integer. - Export current active config as a safety snapshot:
drush config:export --destination=/tmp/pre-fix-config. - Review the diff from step 5 above. Resolve missing modules, UUID mismatches, or deleted config entities.
- Run the import for real:
drush config:import --diff. - Disable maintenance mode:
drush state:set system.maintenance_mode 0 --input-format=integer. - Re-run
drush statusanddrush cache:rebuild.
Risk: Config import can delete content types or fields if the exported config no longer references them. Always review --diff output and have a database snapshot.
File Permissions or SELinux
If logs show FileNotFoundException or Permission denied on file_private_path:
- Ensure the PHP-FPM user owns writable directories:
chown -R www-data:www-data sites/default/files chown -R www-data:www-data ../private chown -R www-data:www-data /tmp/drupal-private # if custom tmp - Set directory permissions to
775and files to664(avoid world-writable777). - On RHEL/CentOS/Fedora, check SELinux contexts:
ls -Z sites/default/files/ # Should show httpd_sys_rw_content_t restorecon -Rv sites/default/files/ - Verify
$settings['file_private_path']insettings.phppoints to an absolute path outside the webroot. - Retry
drush cache:rebuild.
Risk: Over-permissive settings expose uploaded files to execution. Never set code directories writable by the web user.
Escalation Criteria
- Database/Infrastructure: Connection errors persist after credential and network validation, or monitoring shows saturated CPU, disk I/O, or storage full. Escalate to DBAs or hosting provider.
- Security: Logs reveal unexpected file writes in code directories, unknown admin accounts, modified
hook_requirements()orhook_install(), or exploitation indicators (e.g.,eval()in watchdog). Escalate to security team immediately; do not reboot or destroy evidence. - Release Management: Config import cannot reconcile (e.g., UUID conflict on
system.site), or the site remains down after all targeted fixes. Escalate to the release manager with a rollback plan — revert the deploy artifact, restore the database snapshot, and re-enable maintenance mode.
Verification After Fix
- On staging with the same deploy artifact, clear caches and load anonymous pages from more than one PHP-FPM worker (use the
curlloop above or a load-test tool). - Check
/admin/reports/status— all requirements should show green. - Check
/admin/reports/dblog— no new fatal entries during a normal smoke test (visit homepage, login, create a node, upload a file). - Run
drush config:statusand a diff import to confirm configuration is synchronized. Record the exact commands in your runbook.
Limitations and Notes
- This guide assumes Drush 12+ and Drupal 10/11. Drush command names differ in older versions (e.g.,
drush crvsdrush cache:rebuild). - Do not enable verbose error display (
$config['system.logging']['error_level'] = 'verbose') on production. Raise logging temporarily insettings.local.phponly for the debugging window, then revert. - Never run
drush config:importordrush deploy:hook(which runs database updates) during live traffic without maintenance mode and a tested rollback path. - If your hosting platform manages PHP-FPM (Pantheon, Platform.sh, Acquia), use their CLI tools (
terminus,platform,acli) for restarts and log access instead ofsystemctl.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.