Guide
Drupal config import (drush cim) failed — how to diagnose ConfigImportException
1. Problem
You ran drush cim -y on staging or production, expecting the usual quiet success, and the terminal threw it back at you. Something like:
[error] The configuration cannot be imported because it failed validation
for the following reasons:
Configuration depends on the 'webform' module that will not be installed
after import.
In ConfigImporter.php line 740:
There were errors validating the config synchronization.
ConfigImportException
Or it stopped halfway with Drush\Exceptions\UserAbortException, or with a raw PHP fatal during the import step. Either way the deploy is in an awkward state — some config may have been written, some not, and drush config:status now shows a long list of differences that were not there ten minutes ago. You searched "drush cim config import failed" because the message points at a class name and a line number, not at what to actually do.
The Drupal UI cannot help here. The Configuration synchronization page (/admin/config/development/configuration) shows the same error with no extra detail. There is no "see why" link. The truth is in three places: Drush stderr, the watchdog table, and the PHP error log. This guide walks through getting the actual root cause out of those, fixing the most common variants of ConfigImportException, and making sure the next failed import does not silently sit in CI for an hour before someone notices.
2. Impact
A failed config import is not a cosmetic problem. It blocks deploys, and a half-applied import is worse than no import at all.
Concrete impact:
- Deploys blocked. Every CI/CD pipeline that ends in
drush cimis now red. Hotfixes cannot ship until the import works. - Drift between code and database. The exported YAML in
config/syncno longer matches what is active in the site. New developers pulling the branch get a different site than production. - Half-imported state. Drupal imports config in dependency order. If it fails midway, some entity types, fields, or views may have been updated while the rest were not — leading to broken admin pages, missing fields on content forms, or
EntityStorageExceptionon save. - Module install/uninstall stuck. A common failure mode is "module X depends on module Y" where Y is supposed to be installed in the same import. The import aborts, X is not installed, and any code that assumes X exists (event subscribers, hook implementations) fatals on the next request.
- Production downtime. If the import partially ran on production before failing, anonymous traffic can hit a fatal because a service definition references a class from a module that did not finish installing.
3. Why It’s Hard to Spot
Several reasons this fails quietly until someone looks:
- The error is structural, not symptomatic. The site keeps running on whatever config was active before the failed import. If your CI does not watch the exit code, the site looks healthy. The failure is in the deploy pipeline, not in user-facing behavior — until a feature relying on the new config is exercised.
- Drush exits non-zero, but only sometimes. A fatal
ConfigImportExceptionreturns a non-zero exit code. A "soft" abort (user said no to a prompt, or--no-interactionaborted on a warning) sometimes exits 0, sometimes 1, depending on Drush version. - The Drupal UI swallows detail.
/admin/config/development/configurationshows "There were errors" without the underlying validation list when run via the admin form. The full list only appears in Drush stderr or thewatchdogtable. - Half-imported state masks itself. If phase 2 fails partway through, Drupal does not roll back. The next
drush config:statusshows fewer differences than before — making it look like the import worked partially. It did not. It corrupted state. - Module install errors are buried. When
hook_installthrows insideModuleInstaller::install(), the exception is caught, logged towatchdog, and re-thrown as a genericConfigImportException. The original message ("Class X not found") is inwatchdog, not in the Drush output. - Nobody monitors
watchdogin real time. Reports > Recent log messages is a manual page. Errors logged there during a 30-secondcimrun are gone from anyone's attention by the time someone investigates.
4. Cause
When Drush runs cim, it boots Drupal, loads the YAML files in config/sync, and hands them to Drupal\Core\Config\ConfigImporter. The importer does three phases:
- Validate. Compare staged config against active config, build a list of creates/updates/deletes, and check that every dependency (modules, themes, config entities) is satisfiable.
- Process. Apply changes in dependency order — module installs first, then config entities, then simple config.
- Notify. Fire
ConfigImporterEventevents so modules can react (clear caches, rebuild routes, etc.).
A config.import signal records this lifecycle. A successful import emits config.import with status=success and a count of changes. A failed import emits config.import with status=failed and a reason field — validation_error, missing_dependency, schema_mismatch, php_fatal, or event_listener_exception.
The signal carries the validation messages Drupal collected during phase 1, the staged-vs-active diff summary, and the exception class and line if the failure was a thrown exception. That is the information you need — it is just not on screen unless you go looking.
The supporting signals matter because the failure rarely originates in ConfigImporter.php itself. A module.install signal with status=failed during a drush cim run usually means a module's hook_install threw — and that is what surfaces upward as ConfigImportException. A php.fatal during the import window means a class autoload failed (typically a service referenced before the module providing it finished installing). A watchdog row with severity=3 (Error) and type=config is Drupal's own internal record of what went wrong.
5. Solution
5.1 Diagnose (logs first)
The Drush summary is the starting point but never the answer. Go directly to the three real sources.
1. Get the full Drush output, not just the last line. Re-run with verbosity:
drush cim -y --verbose 2>&1 | tee /tmp/cim.log
grep -E "ConfigImportException|missing|dependency|validation" /tmp/cim.log
This produces the config.import signal with status=failed and the validation reason. The full validation list (one line per failure) is what you need.
2. Read the watchdog table directly. This is the single best source for what Drupal itself logged during the import:
drush sql:query "SELECT FROM_UNIXTIME(timestamp), type, severity, \
SUBSTRING(message,1,200) AS msg \
FROM watchdog \
WHERE severity <= 3 \
AND timestamp > UNIX_TIMESTAMP(NOW() - INTERVAL 10 MINUTE) \
ORDER BY wid DESC LIMIT 50;"
Severity 3 is Error, 2 is Critical, 1 is Alert, 0 is Emergency. Filter for type='config', type='module', or type='php' depending on what the Drush output hinted at. A type=php row with severity 3 surfaces as a php.fatal signal — that is the actual exception that broke the import.
3. Tail the PHP error log during the next attempt. If watchdog itself failed to write (which happens when the database is mid-transaction during the import), PHP-FPM's error log is the fallback:
tail -f /var/log/php-fpm/www.error.log &
drush cim -y --verbose
Look for PHP Fatal error:, Uncaught Error:, or Uncaught Drupal\Core\Config\ConfigImporterException:. Each of those produces a php.fatal signal correlated by timestamp with the config.import failure. The stack trace usually points at the offending module's service or class.
4. Get the structured diff. Before re-running, see exactly what was supposed to change:
drush config:status
drush config:status --state=Different --format=list
drush config:diff
config:diff shows the actual YAML difference. Cross-reference modules in the diff with the validation errors to find which module's config is unsatisfiable.
5. Check module install state. If the failure mentioned module.install, find which module aborted:
drush sql:query "SELECT name, schema_version FROM key_value \
WHERE collection='system.schema' ORDER BY name;"
drush pm:list --status=enabled --no-core --format=list
Compare against core.extension.yml in config/sync. If a module is listed in core.extension.yml but not enabled, that is your missing-dependency culprit and it produces a module.install signal with status=failed.
The pattern: config.import with status=failed is the headline. Find the correlated php.fatal, module.install failure, or watchdog error within ±60 seconds of the import — that is the actual cause.
5.2 Root Causes
(see root causes inline in 5.3 Fix)
5.3 Fix
Most ConfigImportException failures fall into one of five buckets. Fix in order — diagnose first, then act.
Cause 1: Missing module dependency
The most common variant. A config entity references a module not present in core.extension.yml.
Symptom in Drush:
Configuration depends on the 'paragraphs' module that will not be installed
Signal: config.import with reason=missing_dependency. Often correlated with a module.install event for the missing module that never fires.
Fix:
# add the module to your codebase
composer require drupal/paragraphs
# enable it locally and re-export
drush en paragraphs -y
drush cex -y
# commit the changed config/sync/core.extension.yml plus new paragraphs.* yml
git add config/sync && git commit -m "Add paragraphs module to config"
Re-run drush cim after deploy. The validation passes once core.extension.yml lists the module.
Cause 2: Schema mismatch
The staged config has a key the active schema does not recognize, usually because module versions differ between where you exported and where you import.
Symptom: validation error Schema errors for KEY with the following errors: missing schema.
Signal: config.import with reason=schema_mismatch. Watchdog type=config row records the exact key.
Fix: align module versions. Check composer.lock parity between environments, then re-export from a canonical environment:
composer install --no-dev
drush updb -y # run pending DB updates first
drush cex -y # re-export against the now-consistent schema
Cause 3: PHP fatal during import
A module's hook_install or an event subscriber threw an exception. The Drush output says ConfigImportException but the actual error is in PHP error log.
Symptom: Uncaught Error: Class "Drupal\custom_module\Foo" not found in /var/log/php-fpm/www.error.log.
Signal: php.fatal correlated within seconds of config.import failure. Look for the class name in the stack trace.
Fix: check that:
- The class file exists and is autoloaded (
composer dump-autoload). - The module providing the class is listed before any dependent module in
core.extension.yml. - Custom modules have correct
*.info.ymldependencies:entries.
Then re-import:
drush cr
drush cim -y
Cause 4: Field or entity in use
You staged a deletion of a field that has data. Drupal refuses to drop it during import.
Symptom: The field is in use and cannot be deleted.
Signal: config.import with reason=validation_error and a field_storage reference.
Fix: purge field data first, or use drush field:delete interactively, then re-import.
Cause 5: UUID conflict
Config entities are matched by UUID. If two environments created config entities independently, the UUIDs differ and Drupal sees a "create" instead of "update", which can trip dependency checks.
Fix: align UUIDs. Either re-export from the source of truth, or use drush config:import --partial only after manually inspecting the diff. Never use --partial on production without a staged dry run.
For all causes: if the import partially applied before failing, re-run after fixing — Drupal is idempotent on a clean re-import. If the site is wedged, restore the database to the pre-import snapshot and try again on the now-fixed codebase.
5.4 Verify
Re-run the import end to end and confirm the failure signals disappear.
drush cr
drush cim -y --verbose
echo "exit code: $?"
drush config:status
You are looking for three things:
- Drush exits 0 and prints
There are no changes to import(clean state) orThe configuration was imported successfully. This corresponds to aconfig.importsignal withstatus=success. - No new
config.importfailures in the next 10 minutes. If the import is part of a CD pipeline that runs on every deploy, watch for the signal pattern to stabilize across at least two consecutive deploys. - No correlated
php.fatalormodule.installfailures during the import window. Verify with:
drush sql:query "SELECT COUNT(*) FROM watchdog \
WHERE severity <= 3 \
AND timestamp > UNIX_TIMESTAMP(NOW() - INTERVAL 5 MINUTE);"
A healthy post-fix state is: zero severity-3-or-worse rows in watchdog during the import window, exit code 0 from drush cim, and drush config:status reporting no differences. If any of those is wrong, the import did not actually succeed — check the log again.
For ongoing health, the absence of config.import failure signals over a 24-hour window across all environments is the real signal that the deploy pipeline is healthy. A single re-occurrence on staging is the canary that prevents the same failure landing on production.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.
This issue surfaces as config.import.
Config imports fail silently in two ways: in CI nobody is watching, and on a low-traffic admin endpoint nobody is hitting. Either way, by the time someone notices, the deploy is hours stale and somebody is rolling back.
The fix is not "watch the CI dashboard harder". It is treating drush cim as a production event with its own signal. A failed import emits a config.import event with status=failed and a reason. A correlated php.fatal or module.install failure narrows the cause to a single module or class. Those signals exist whether or not anyone is monitoring them.
This is exactly the surface Logystera reads. The Drupal agent ships config.import, module.install, php.fatal, and watchdog error events into the platform, where rules detect a failed import within seconds and correlate it with the PHP fatal or module install error that caused it. You get one alert with the actual root cause, not a CI red mark with a class name and a line number.
The deploy pipeline does not need to be rewritten. It needs to be observed at the layer where the failure actually happens — Drupal's own config and module subsystems — instead of at the layer where the symptom shows up — a 500 on a page nobody visited yet.
7. Related Silent Failures
- Drupal database updates pending (
drush updb) but not run — surfaces asupdate.runabsence and triggers schema mismatch on next config import. - Module uninstalled but config entity still references it — produces
config.importwithreason=missing_dependencyeven though no module was added. - Cache rebuild failures during deploy —
cache.rebuildfailure followed by stale service container, leading tophp.fatalon first request. - Entity update hooks failing on deploy —
entity.updateexceptions logged to watchdog but ignored, producing inconsistent storage and laterEntityStorageException. - Composer post-install scripts failing silently — autoload not regenerated, leading to
php.fatalon classes that did exist before deploy.
See what's actually happening in your Drupal system
Connect your site. Logystera starts monitoring within minutes.