Drupal: Logging, the Status Report and Troubleshooting

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

When a Drupal site misbehaves, the cause is almost always already written down somewhere: in the Recent log messages report, in the Status report, in the web server’s or PHP’s error log, or in the output of a Drush command. This page explains where each of those lives, how Drupal’s logging works internally, how to read the Status report, what to do about a white screen or an HTTP 500, how and when to rebuild caches, how to recover a site that broke after an upgrade or a configuration import, and which contributed developer tools (Devel and Webprofiler) help to find a problem. The running example is the Meridian Museum site, whose meridian_collection module synchronises collection objects from a fictitious collections-management API.

It was checked on 2026-10-30 against Drupal 11.4.x, Drush 13 and 14 and PHP 8.3 or later. Drupal 10 reaches end of life on 2026-12-09 (see Versions, Release Cycle and Support), and Drupal 12.0.0 is planned for the week of 2026-12-07, so the sections on removed modules apply to both upgrades. The audience is the site administrator and the developer who is called when the site is down; routine scheduled work is in Cron, Queues and Maintenance, and the command-line tool is described in Drush.

A diagnosis path

Troubleshooting is faster when it follows a fixed order, from the cheapest and least risky check to the most invasive one. The order used on this page is shown below.

flowchart TD S[Site misbehaves] --> Q{Is a page rendered at all?} Q -- "blank page or HTTP 500" --> L[Read the web server and PHP error log] Q -- "page with a message" --> R[Open Recent log messages and the Status report] L --> F{Fatal error found?} F -- yes --> X[Fix code, configuration or missing file] F -- no --> E[Raise error_level to verbose temporarily] E --> R R --> X X --> C[Rebuild caches with drush cr] C --> V{Fixed?} V -- no --> D[Reproduce with Devel or Webprofiler locally] D --> X V -- yes --> B[Restore error_level to hide and note the cause]

Two rules hold throughout. First, work on a copy (a DDEV project or a staging environment) whenever the fix involves editing configuration or the database, and take a backup before touching production (see Backups and Disaster Recovery). Second, remember that the cause is frequently an incomplete deployment: code updated but drush updatedb or drush config:import not run, or the reverse. The drush deploy command runs the correct sequence (updatedb, config:import, cache:rebuild and deploy hooks).

Where Drupal writes its logs

Drupal sends every log message to the PSR-3 logger channel service (logger.factory). Each message has a channel (called a type in the report, for example php, cron, user or meridian_collection), a severity (the eight RFC 5424 levels from Emergency down to Debug), a text with placeholders, a timestamp, the user, the request URL and the client address. The factory forwards each message to every logger that is registered with the service tag logger. Core registers two of them as modules, and the web server and PHP keep two further logs that Drupal does not control.

Log Provided by Typical use

Recent log messages (admin/reports/dblog)

Database Logging module (dblog), a core module

Day-to-day review by administrators and developers on small and medium sites

Syslog

Syslog module (syslog), a core module that writes to the operating system’s syslog facility

Forensic and long-term logging, shipped to a central log service

PHP error log

PHP (error_log directive), independent of Drupal

Fatal errors, parse errors and out-of-memory conditions that happen before Drupal can log anything

Web server error log

Apache, nginx or PHP-FPM

HTTP 500 responses with no Drupal page, timeouts, permission and rewrite problems

Recent log messages (Database Logging)

The Database Logging module stores messages in the watchdog database table and shows them under Reports > Recent log messages at admin/reports/dblog. The report lists the type, date, message, user and operations, can be filtered by type and severity, and links each row to a detail page with the full message, location, referrer, hostname and the variables. The User Guide chapter that describes it is Concept: Logging (the "Recent log messages" section of the Prevent-and-fix chapter).

The same chapter states the limit that matters for design decisions: administrators and automated cron tasks can clear these messages, so the report should not be used for forensic logging, and the Syslog module is the recommended alternative for that purpose. The table is pruned on every cron run. The number of rows to keep is a setting on the Logging and errors page (admin/config/development/logging), stored as row_limit in the dblog.settings configuration object; the choices run from 100 up to 1,000,000 rows, or unlimited. On a busy site an unlimited table slows the database and makes backups large. If cron does not run, the table is never pruned, which is one more reason to monitor cron (see Cron, Queues and Maintenance).

The permission that gates the report is View site reports. It is a restricted permission because messages can contain user names, e-mail addresses and request details; grant it only to administrators (see Users, Roles and Permissions).

Writing messages from the Meridian module

A module logs through the logger.channel.<name> services. A channel is declared once in *.services.yml, then injected like any other service (see Services and Dependency Injection).

# meridian_collection.services.yml
services:
  logger.channel.meridian_collection:
    parent: logger.channel_base
    arguments: ['meridian_collection']

  meridian_collection.object_sync:
    class: Drupal\meridian_collection\ObjectSync
    autowire: true
<?php

declare(strict_types=1);

namespace Drupal\meridian_collection;

use Drupal\Core\Cache\CacheBackendInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Synchronises collection objects from the collections-management API.
 */
final class ObjectSync {

  public function __construct(
    #[Autowire(service: 'logger.channel.meridian_collection')]
    private readonly LoggerInterface $logger,
  ) {}

  public function importOne(string $accession_number, array $payload): void {
    if (empty($payload['title'])) {
      // Placeholders keep the message translatable and safe to display.
      $this->logger->warning('Object @number has no title and was skipped.', [
        '@number' => $accession_number,
      ]);
      return;
    }
    try {
      $this->save($accession_number, $payload);
      $this->logger->info('Imported object @number.', ['@number' => $accession_number]);
    }
    catch (\Throwable $e) {
      // Pass the exception under the "exception" key; Drupal's logger
      // processors then add the file and line to the entry.
      $this->logger->error('Import of @number failed: @message', [
        '@number' => $accession_number,
        '@message' => $e->getMessage(),
        'exception' => $e,
      ]);
    }
  }

  private function save(string $accession_number, array $payload): void {
    // Create or update the collection_object entity here.
  }

}

Pitfalls when writing messages:

  • Use placeholders (@name escaped, %name escaped and emphasised, :name for URLs) and never concatenate user input into the message. The dblog report renders the stored message with its variables, so a concatenated value is a stored cross-site scripting risk for the administrator reading it.

  • Do not log secrets, tokens, full request bodies or personal data. The watchdog table is readable by anyone with the report permission and is copied into every database backup.

  • Choose the severity deliberately. error and above are the messages that monitoring should alert on; an info message for every imported object produces thousands of rows per sync.

  • In procedural code a \Drupal::logger('meridian_collection') call is acceptable, but classes should receive the logger by injection so that it can be replaced in tests.

Syslog

The Syslog module (syslog) writes each message to the operating system’s syslog facility instead of, or in addition to, the database. Its settings are on the same Logging and errors page: the identity string that prefixes each line, the facility (a LOG_LOCAL0 to LOG_LOCAL7 value or LOG_USER) and the message format with tokens such as !base_url, !timestamp, !type, !ip, !request_uri, !referer, !severity, !uid, !link and !message. On a host the lines end up in /var/log/syslog or the journal, where an agent can ship them to a central service; in a container the usual practice is to write to standard error and let the platform collect it. The two modules can run together, and a common arrangement is Syslog for retention and alerting with Database Logging kept on a small row limit for convenience, or switched off entirely on production sites that have central logging. The Drupal User Guide and the community documentation describe Syslog as the better choice for forensic needs; this page does not claim a particular throughput, which depends on the host.

Uninstalling Database Logging removes the report but also removes the only in-site record; before doing so on a site with no other logger, confirm that Syslog or an external logger is configured.

Choosing what visitors see: the error level

The same Logging and errors page controls how PHP errors and exceptions are shown to the visitor. The setting is error_level in the system.logging configuration object:

Value Effect

hide

No error messages are shown. The right value for production.

some

Shows errors and warnings ("Errors and warnings").

all

Shows errors, warnings and notices.

verbose

Shows everything in all plus a backtrace for exceptions. For development only.

On a production site, set hide so that stack traces, file paths and database details are not shown to visitors, who could use them to attack the site. In a development environment, override the value in settings.php or in an environment-specific file instead of exporting it to the shared configuration (see settings.php and Environments):

<?php

// web/sites/default/settings.local.php (DDEV and local development only).
$config['system.logging']['error_level'] = 'verbose';

// Keep the full log of what happens locally.
$config['dblog.settings']['row_limit'] = 0;

A verbose setting left on a production site is a recurring information-disclosure finding in security reviews, so make the settings.php override conditional on the environment, and never commit a settings.local.php that is loaded on production.

Reading logs from the command line

Drush reads the watchdog table without a browser. The command is watchdog:show with the aliases wd-show, ws and watchdog-show; its documentation is at drush watchdog:show.

# The ten most recent messages (the default count)
ddev drush watchdog:show

# Search the message text for a substring
ddev drush watchdog:show "Import of"

# Show 50 messages, only warnings and worse, from one channel
ddev drush watchdog:show --count=50 --severity-min=Warning --type=meridian_collection

# Only PHP errors, with the location and hostname columns
ddev drush watchdog:show --type=php --extended

# Follow new messages as they arrive (a separate command)
ddev drush watchdog:tail

# Machine-readable output for a script
ddev drush watchdog:show --count=100 --format=json

The options are --count (default 10), --severity for one level (a number or a name), --severity-min for that level or worse, --type for one channel, --extended for extra columns, --fields and --field to choose columns, --filter for an expression such as type!=locale, and --format for the output format. drush watchdog:tail prints new entries as they arrive, and drush watchdog:delete empties or prunes the table. Because the table can be cleared by an administrator or by cron, take the entries you need as evidence before running any command that deletes them.

Drush reads the database that the site’s settings.php configures, so a site with no working database connection also has no Drush log. In that case the web server and PHP logs are the only evidence, which is why they are the first place to look for a white screen.

The Status report

The Status report at admin/reports/status (Reports > Status report) lists every requirement that core and installed modules check. The Drupal User Guide describes it as a short overview of the site’s settings that flags problems it detects, and recommends copying it when asking for help on the Drupal.org forums or issue queues. Its entries are grouped by severity, and the number of errors and warnings is shown on the page and, for errors, in the administrative toolbar’s report item.

Where the entries come from

Each entry is produced by a requirements check: a module returns an array of requirements, each with a title, a value, an optional description and a severity. The checks run in three phases: during installation, during updates and at runtime, and the Status report shows the runtime ones. The severity levels are Informational, OK, Warning and Error. In Drupal 11.2 and later the severities are the cases of the RequirementSeverity enum, and the old integer constants REQUIREMENT_ERROR and the like are deprecated in 11.2.0 for removal in 12.0.0; modules that compare severities with integers need to be updated (see the API reference for REQUIREMENT_ERROR). In recent 11.x releases the runtime and update phases are also being split into separate hooks, so a module that implements requirements should be checked against the API reference for the version it targets. hook_install, hook_requirements and the other install-time hooks have to stay procedural even in a module that uses object-oriented hooks everywhere else (see Hooks and Object-Oriented Hook Implementations).

The same data is available without a browser:

# Every line of the Status report
ddev drush core:requirements

# Only the errors (severity 2)
ddev drush core:requirements --severity=2

# Machine-readable output, handy for a monitoring check
ddev drush core:requirements --severity=1 --format=json

The command has the aliases status-report and rq, an --ignore option that removes named requirements (the key names appear in --format=yaml output), and exits with a non-zero status when an error is present at the chosen severity, which makes it usable as a deployment gate. In a CI job the pattern is to run drush core:requirements --severity=2 after the deployment and fail the job if it reports anything.

Common requirement errors and what they mean

Entry Meaning and usual remedy

Database updates: Out of date

A module or core was updated but its update functions have not run. Run drush updatedb (or update.php), see Update API and Post-Updates and the database updates screen.

Configuration synchronisation: Out of sync

The active configuration differs from the exported files. Review with drush config:status and import or export, see Configuration Management.

Cron maintenance tasks: Never run, or not run for a long time

Nothing is triggering cron. Add a system cron job, see Cron, Queues and Maintenance.

Trusted Host Settings: Not enabled

trusted_host_patterns is missing from settings.php. Without it a request with a forged Host header can poison generated links and caches. Add the patterns for the Meridian domains, see settings.php and Environments. The User Guide links this entry to a community page.

Database system version, PHP version or PHP extensions (for example gd, pdo_mysql, opcache)

The host is below the minimum or lacks an extension, see Requirements and Hosting. A missing PHP extension in the CLI and in the web SAPI is a classic cause of "works with Drush, fails in the browser".

Update notifications: security update available, or update information cannot be fetched

Run the update procedure in Updates, Composer and Security Advisories, or check outbound connectivity from the server.

File system: Not writable, or temporary directory missing

The web server user cannot write to sites/default/files, the private path or the temporary path configured in settings.php, see File System and Private Files.

Settings file protection: not protected

settings.php or the sites/default directory is writable after installation. Make them read-only; Drupal normally does this itself (the skip_permissions_hardening setting disables that, which is useful on some CI hosts only).

Missing modules or themes

The core.extension configuration lists an extension that is not in the code base. See Missing modules after an upgrade.

When the Status report itself cannot load (a fatal error or a memory-limit failure), use the Drush command and the logs first. The report is read-only, so a warning in it never needs urgent action, but an error that persists after a deployment normally means the deployment is incomplete.

White screens and 500 errors

A blank page or the generic message "The website encountered an unexpected error. Try again later." is the same family of problems seen from the visitor’s side. The three visible forms are:

  • A completely blank page (the "white screen of death"): PHP stopped with a fatal error or ran out of memory, and display_errors is off, so nothing was printed. The evidence is in the PHP or web server error log.

  • The Drupal message "The website encountered an unexpected error": an exception reached Drupal’s exception handler, which logged it (look in Recent log messages, type php) and showed the generic page because error_level is hide.

  • A web server HTTP 500 or 502/504 page: the failure happened before or outside Drupal: a syntax error in .htaccess, a PHP-FPM crash or timeout, a missing vendor directory, or a permission problem.

A checklist, in order

  1. Read the error log first. On DDEV: ddev logs for the web container and ddev logs -s db for the database; on a host, the PHP-FPM log and the virtual host’s error log. A fatal error line names the file, the line and the message. This is faster than any experiment.

  2. Show the error. Temporarily set $config['system.logging']['error_level'] = 'verbose'; in settings.local.php (or the production-safe equivalent: reproduce on staging). If even that shows nothing, the failure is before Drupal boots, for example in settings.php; check it for syntax errors with php -l web/sites/default/settings.php.

  3. Check the most likely causes in this order:

    1. a code change that was deployed without composer install (the vendor directory is out of step with composer.lock), or with a different PHP version than the one that installed it;

    2. a PHP memory or execution time limit (memory_limit, max_execution_time) hit by an expensive page, an import or a cache rebuild;

    3. a missing or renamed class after a module update or removal, usually shown as Class not found or Service not found, which a cache rebuild fixes if the service container is stale;

    4. a database that is unreachable, full or read-only (SQLSTATE[HY000]);

    5. a configuration import that enabled a module whose code is not present;

    6. file permissions or ownership changed after a deployment.

  4. Rebuild caches (next section) once the cause is fixed or if the error mentions a missing service or plugin.

  5. Compare with a working copy. On a site with configuration management, git diff for the configuration files and the lock file often points at the change that broke it.

The community pages that cover this are Troubleshooting Drupal (its section list includes Broken site problems) and Broken site problems. The latter’s recovery sequence after installing or updating a module or theme is: reload the page, run the database updates, clear caches from the Performance page, run cron from the Status report, run rebuild.php, check the log for details, and, for a blank page with no clear error, run drush cache:clear and read the terminal output. Truncating the cache tables is mentioned there as a last resort.

HTTP 500 and 503 responses specific to Drupal

  • A 503 with "Site under maintenance" is maintenance mode, not a fault. Turn it off with drush maint:set 0 (see Cron, Queues and Maintenance). A deployment that failed halfway can leave it on.

  • "Unable to connect to the database" or SQLSTATE[HY000] [2002] points to the database server, to wrong values in settings.php, or to the credentials in the environment variables that settings.php reads. The database values belong in the environment, never in a committed file.

  • "The provided host name is not valid for this server" is the trusted host check rejecting the request; add the domain to trusted_host_patterns.

  • A 500 only for a proxy, CDN or Varnish layer can be a missing HTMX header pass-through. From Drupal 11.3, proxies and CDNs in front of the site must forward the HX-Request and HX-Redirect headers, otherwise HTMX-based forms and the admin UI behave strangely (see AJAX and HTMX).

  • Windows and IIS: IIS support was removed in Drupal 11.0, and Windows production hosting has been deprecated since 11.3; the troubleshooting steps on this page assume Apache or nginx.

Rebuilding caches

Many Drupal faults are stale caches: a new service, plugin, route or template that the container, discovery caches or render caches do not yet know about. A cache rebuild discards the compiled container, the plugin and route caches, the Twig compiled templates and the render and page caches, and rebuilds the critical ones. It is safe but expensive, because the first requests afterwards are slow (a large site can take a minute), so avoid doing it during peak traffic if you can.

Method When to use

Configuration > Development > Performance > Clear all caches (admin/config/development/performance)

The site works and you have an administrator account. The User Guide’s Clearing the Cache page lists it first.

drush cache:rebuild (alias drush cr)

The normal method from a terminal, including during drush deploy. It works even when the site returns a 500, because it bootstraps Drupal in a way that rebuilds the container.

drush cache:clear (alias drush cc)

Lists individual cache bins so a single one, such as render, can be cleared without a full rebuild.

core/rebuild.php

No Drush and no working administrator login: for example a shared host. The script is enabled temporarily in settings.php (see below).

Truncating the cache_* tables

A last resort when a corrupted cache entry keeps the site from booting. drush sql:query can truncate each cache_* table; do it on a copy first.

Using core/rebuild.php

The rebuild script is protected: it only runs when settings.php allows it, so that a stranger cannot trigger an expensive rebuild. The sequence given in the User Guide page is:

<?php

// Last lines of web/sites/default/settings.php. Remove them after the rebuild.
$settings['rebuild_access'] = TRUE;

Open https://meridian.example.org/core/rebuild.php in a browser (replace the host with your own), wait for the home page redirect, and then delete the line again. As an alternative to editing the file, an authenticated user with the Administer site configuration permission can run the script without the setting, but then the site is not usually broken enough to need it.

Leaving rebuild_access on in production lets any visitor trigger a rebuild repeatedly, which is a cheap denial-of-service vector, so always remove it afterwards.

# Full rebuild (alias: cr)
ddev drush cache:rebuild

# After a deployment, the supported order in one command
ddev drush deploy

# Clear only the render cache bin
ddev drush cache:clear render

# Equivalent check: is the container healthy after the rebuild?
ddev drush core:requirements --severity=2

Rebuilding caches is the answer to many problems and the cause of none, so treat a cache rebuild that "fixes" a fault as a clue: if a rebuild is needed after every deployment, something (a missing drush deploy step) is not being done; if it is needed repeatedly during normal use, a module is writing cache entries with wrong cache tags or contexts (see Render API and Cache Metadata and Performance and Caching).

Recovering a broken site

Missing modules after an upgrade

Typical symptoms are a warning in the Status report or on the Extend page that a module or theme "is missing from the file system", a fatal error on every request after a deployment, or a configuration import that fails. The cause is a core.extension entry (installed module or theme) whose code is no longer present. The official page How to fix "The following module is missing from the file system…​" warning messages lists the causes and cures. In summary:

  1. The module was removed from the code without being uninstalled. The recommended repair is to put the code back (composer require drupal/example), then uninstall it properly (drush pm:uninstall example) so that it can clean up its configuration, tables and fields, and only then remove the code again. Manually deleting its rows is discouraged: the page calls it "not recommended" because the module’s cleanup is skipped, and shows deleting the system.schema key-value row and the core.extension entry (drush config:delete core.extension module.example), then a cache rebuild, as the last resort.

  2. The module moved within the code base, for example from modules/ to modules/contrib/. Clear caches and restart the web server or PHP-FPM so that cached paths are dropped, or move it back.

  3. Fields, configuration or views still depend on it. Get a stack trace to find the caller, then report it in the module’s issue queue; a cron run or cache rebuild sometimes cleans such leftovers.

  4. A profile or another module still lists it as a dependency. Check the *.info.yml files.

For the Meridian site this is realistic after an upgrade from Drupal 10 to 11, where core modules were removed (Actions UI, Activity Tracker, Book, Forum, Statistics and Tour went in 11.0; Color, Quick Edit, RDF, Aggregator and HAL in 10.0; Ban, Contact, Search, Toolbar, Telephone and others move to contrib in Drupal 12). The rule is the opposite of the one above: before upgrading, install the contrib version of any removed module you use, and do not uninstall it. Uninstalling the removed module first deletes its data, and the upgrade then fails with the missing-module symptoms. See Removed Core Modules and Replacements and Upgrading Drupal 10 to 11.

Old tutorials describe drush dis, drush cc all and drush sql-query against the system table. Those belong to Drupal 7; on Drupal 10 and 11 the commands are pm:uninstall and cache:rebuild, and extension state is in the core.extension configuration object, not the system table.

Broken configuration

A configuration import (drush config:import) can fail validation, enable modules in the wrong order or leave the site in a state where the Status report reports the configuration as out of sync. The recovery path is:

  1. Run drush config:status to see which items differ (Only in DB, Only in sync dir, Different).

  2. If the import enabled a module whose code is absent, deploy the code first (composer install), then import again. Configuration import works on the premise that the code and the configuration belong to the same commit.

  3. If a single configuration object is wrong in the database, fix it in place: drush config:set system.logging error_level hide, or edit the YAML and use drush config:import --partial --source=.... Export afterwards so Git matches the site.

  4. If the site cannot boot at all, restore the previous release and its database backup rather than editing the database by hand.

# What differs between the active configuration and the exported files?
ddev drush config:status

# Read one object and compare it with the exported YAML
ddev drush config:get meridian_collection.settings

# Fix one value, then export so that Git agrees
ddev drush config:set meridian_collection.settings api_timeout 20 -y
ddev drush config:export -y

# Re-run the supported deployment sequence
ddev drush deploy -v

Locked out and other emergencies

  • Locked out of the administrator account: drush user:login prints a one-time login link; drush user:password admin CHANGE_ME sets a new password (use a unique value and change it immediately). Both run with the site’s own credentials and do not need the web login.

  • Site stuck in maintenance mode after a failed deployment: drush maint:set 0, then drush cache:rebuild.

  • The Anonymous user record is missing (user ID 0): the Troubleshooting page has a recovery procedure, because many access checks assume that row exists.

  • The administrator theme or a custom theme is broken: set $config['system.theme']['admin'] = 'claro'; and $config['system.theme']['default'] = 'olivero'; temporarily in settings.php, or use drush theme:enable; this uses themes that exist in core 11 (Claro and Olivero are removed in Drupal 12, where Default Admin replaces Claro).

  • Database access lost after a restore: check the environment variables the settings.php reads, not the database server first.

The general recovery rule is to prefer the supported tool: Composer for code, drush deploy for the sequence, a backup for the database. Module and theme upload from the administrative interface was removed in Drupal 10.4/11.0, and updating from the interface in 11.2, so there is no browser-based repair for code problems any more.

Devel and Webprofiler

Two contributed projects help to find the cause of a problem on a development site. Both are developer tools and neither is part of core.

  • Devel (drupal.org/project/devel) adds helper functions such as dpm() and dump() (through Symfony VarDumper in current releases), a toolbar and entity "Devel" tabs that show an entity’s raw field values, a render array or a loaded object, a mail system that writes outgoing mail to files, and Drush commands. Its page states that it is safe on a production site only if just developers hold the Access development information permission. The current 5.x releases need PHP 8.3 and Drush 13.7 or later in the 5.5 line; its compatibility table and the release block on the project page differ slightly on the supported core versions (10.4 or later in the table, ^10.3 || ^11 || ^12 in the release block), so read the version constraint Composer resolves. The Devel Generate submodule creates sample content in bulk, for example 200 Meridian exhibitions to test a view.

  • Webprofiler (drupal.org/project/webprofiler) adds a toolbar at the bottom of every page, in the style of the Symfony profiler, showing the number and text of database queries, the services used, the cache hits, the render time and the request details. The current 11.2.x release series supports Drupal 11; for other majors use the matching branch listed on the project page. It is the tool to use when a Meridian page is slow or a view runs hundreds of queries.

Install them only with --dev so they are absent from production:

ddev composer require --dev drupal/devel:^5 drupal/webprofiler:^11.2
ddev drush pm:install devel webprofiler -y
ddev drush config:set system.logging error_level verbose -y

Never enable these modules on production: they expose internal data (queries, service lists, field values, backtraces), add overhead and may widen the attack surface. Uninstall them before exporting configuration for the production environment, or keep the configuration split so that the development-only modules are not exported (see Configuration Management). For debugging PHP code line by line, a debugger such as Xdebug in DDEV is more effective; see Debugging and Profiling PHP and Errors and Exceptions in PHP. Template problems are covered in Twig Templates and Debugging.

Version notes and corrections of older material

  • Drupal 10 ends on 2026-12-09. Troubleshooting steps here assume Drupal 11 and Drush 13 or 14; the same commands work on 10.2 and later with Drush 13.

  • Older books such as Drupal 10 Development Cookbook (2023) and Drupal 10 Masterclass (2023) show the Drush 12 command set, including drush.services.yml for custom commands. That file no longer exists since Drush 13; commands are written with PHP attributes and autowiring. Use watchdog:show, core:requirements, cache:rebuild and config:status as shown here.

  • Older tutorials still tell you to clear caches with drush cc all, to disable modules with drush dis, or to edit the system table. These are Drupal 7 idioms; on Drupal 8 and later use drush cr, drush pm:uninstall and the core.extension configuration.

  • Tutorials that start from the Standard installation profile and a prebuilt "Article" or "Page" are out of date: from Drupal 11.4 the Standard profile no longer creates those content types, so a diagnosis of "content type not found" after an install is expected until the types are created (see Content Types and Fields).

  • The Statistics module that older books use to count views was removed in 11.0; do not look for its report. The core/jquery.ui library and Quick Edit are also gone, which explains "library not found" errors in old themes.

  • Code that tests against the integer constants REQUIREMENT_OK and REQUIREMENT_ERROR should move to the RequirementSeverity enum before Drupal 12.

  • The assertion handling in the .htaccess file changed in the 11.4.11 and 10.6.20 security releases, which swap assert.active for zend.assertions; a site with a customised .htaccess should compare it with the shipped one when an unexpected 500 appears after a core update (see Security Hardening).

  • The Drupal 12 requirements (PHP 8.5, Symfony 8) mean that after upgrading, the most common 500 is a PHP version mismatch between the CLI and the web server; see Preparing for Drupal 12 and Where Older Books and Tutorials Are Outdated.

Common errors and pitfalls

  • Logging at the wrong severity or without placeholders, which produces unreadable logs and a stored cross-site scripting risk.

  • Relying on Recent log messages as an audit trail; cron and administrators can clear it, so send audit-relevant events to Syslog or an external service.

  • Leaving error_level at verbose, or rebuild_access set to TRUE, on a production site.

  • Running drush cr as a ritual instead of finding which step of the deployment is missing.

  • Uninstalling a removed core module before an upgrade instead of installing its contrib replacement.

  • Editing the system table or the key_value table by hand when pm:uninstall or config:delete would do the job.

  • Reading the web login for evidence when the real cause is in the PHP or web server log, or testing in the browser with a different PHP version from the CLI.

  • Installing Devel or Webprofiler with plain composer require so that they reach production.