Drupal’s October 2026 change records are unusually useful for maintainers because they expose the practical shape of the next upgrade cycle before Drupal 12 reaches its planned stable release window. The important story is not one isolated feature. It is the gradual removal of technical debt across database APIs, file handling, command-line tooling, testing and frontend asset management.
For teams running Drupal in production, this is the point at which an upgrade stops being a future project and becomes a backlog of small, testable engineering tasks. Drupal 12 is currently scheduled for the week of 7 December 2026, while Drupal 10 reaches end of life on 9 December 2026. That leaves a relatively short window for organisations still operating Drupal 10 to complete compatibility work, testing and deployment planning. ([drupal.org](https://www.drupal.org/about/core/policies/core-release-cycles/schedule?showAll=true&utm_source=openai))
What changed in Drupal core during October 2026?
The recent Drupal core change records include several changes with direct consequences for developers and site builders:
- Database query objects can no longer be serialised in the Drupal 11.5 development line.
- New image styles created through the user interface convert images to WebP by default.
- Drupal 12.1 introduces work around toggleable public and private uploaded files.
- The core
drcommand-line application is gaining commands for listing and uninstalling extensions. - Drupal 12 development requires Node.js 24 for core development work.
- Drupal 12 removes or continues deprecating a number of older APIs and administrative components.
These are not all migration blockers. Some affect only custom code, some affect development environments and some change defaults for newly created configuration. The correct response is therefore not to make a large, speculative rewrite. Instead, audit your codebase, classify the impact and deal with each category independently.
Start with a supported-version inventory
Before changing code, record what is actually running in each environment. A surprisingly common upgrade failure is a mismatch between the version in composer.json, the version in composer.lock, the deployed code and the database state reported by Drupal.
Run the following commands from the project root on a non-production checkout:
composer show drupal/core-recommended drupal/core-composer-scaffold drupal/core-project-message
composer outdated --direct
git status --short
vendor/bin/dr status
The first command confirms the installed core packages. composer outdated --direct identifies direct dependencies that may need attention, while git status helps ensure that an update is not being performed on top of uncommitted configuration or code changes.
The dr entry point is part of Drupal’s newer core command-line direction. Drupal’s documentation recommends vendor/bin/dr as the preferred entry point, replacing the older core/scripts/drupal path, which is deprecated in current Drupal development. ([drupal.org](https://www.drupal.org/node/3584928?utm_source=openai))
If your project uses Drush, there is no requirement to remove it simply because core has a CLI. Drush remains useful for project-specific workflows and contributed commands. The practical approach is to use the tool that provides the clearest and most stable command for a particular task, while updating documentation that still references the deprecated Drupal script.
Audit custom code for serialised database queries
One of the most important October changes for module maintainers is the removal of support for serialising Drupal database query objects in the Drupal 11.5 line. The change record relates specifically to the deprecation and removal of Query::__wakeup() and Query::__sleep(). ([drupal.org](https://www.drupal.org/list-changes/drupal?_pxhc=1657483200125&utm_source=openai))
Most Drupal code never serialises a query object intentionally. The problem tends to appear in custom caching code, queue payloads, batch operations, long-running workers or integrations that place arbitrary objects into a cache or session.
This pattern is unsafe for forward compatibility:
$query = \Drupal::entityQuery('node')
->condition('type', 'article')
->accessCheck(TRUE);
$cache->set('article_query', $query);
A cache backend may serialise the object while storing it. Even if the code appears to work on the current release, it couples the cache entry to the internal structure of the query object and makes the cached value difficult to invalidate safely.
Cache the result, or cache a small data structure from which the query can be rebuilt:
$cache_id = 'example.article_nids';
$cache = $this->cacheBackend->get($cache_id);
if ($cache) {
$article_nids = $cache->data;
}
else {
$article_nids = \Drupal::entityQuery('node')
->condition('type', 'article')
->accessCheck(TRUE)
->execute();
$this->cacheBackend->set(
$cache_id,
$article_nids,
CacheBackendInterface::CACHE_PERMANENT,
['node_list']
);
}
In a service, prefer dependency injection rather than calling the global service locator. The important design decision is that the cache contains scalar values or arrays, not an active query object.
For queries that must be reconstructed later, store parameters:
$definition = [
'entity_type' => 'node',
'conditions' => [
['field' => 'type', 'value' => 'article'],
['field' => 'status', 'value' => 1],
],
];
The worker or callback can then create a fresh query when it runs. This is more robust than attempting to carry a database query object across a queue boundary, a cache boundary or a process boundary.
How to find likely problem areas
Search custom and contributed code for explicit serialisation first:
rg -n --glob '*.php' \
'serialize\(|unserialize\(|__sleep|__wakeup|cache->set|queue.*createItem' \
web/modules/custom web/themes/custom
rg -n --glob '*.php' \
'QueryFactory|entityQuery|select\(|insert\(|update\(|delete\(' \
web/modules/custom
These searches will produce false positives, so they are an inventory rather than an automated diagnosis. Inspect each result and ask three questions:
- Could this value be placed in a cache, queue, batch or session?
- Is the value an object that contains a database connection or query state?
- Would storing the query result or query definition be clearer?
Do not blindly replace every query with raw SQL. The compatibility issue is object lifecycle, not the use of Drupal’s query APIs. Rebuilding a fresh entity query or database query at execution time is normally the correct solution.
Test the new WebP default deliberately
Another October change affects site builders rather than existing image styles: new image styles created through the UI convert images to WebP by default. This follows Drupal’s broader move towards WebP output for core-provided image styles. Drupal has already documented that shipped image styles include WebP conversion, and the newer change extends the expected default behaviour to newly created styles. ([drupal.org](https://www.drupal.org/list-change-updates/drupal?utm_source=openai))
This is generally positive for performance, but it should not be treated as a guarantee that every image pipeline is correct. Test the complete path:
- Original image upload.
- Derivative generation.
- WebP conversion by the configured image toolkit.
- Delivery from the public or private file system.
- Responsive image markup and browser selection.
- Cache invalidation after changing an image style.
Check which image toolkit is enabled and confirm that it can write the chosen derivative format. A basic environment check might include:
php -m | grep -Ei 'gd|imagick'
vendor/bin/dr list | grep -Ei 'image|cache'
The exact capabilities depend on the PHP extensions and ImageMagick build installed by your hosting provider. A development machine using GD may not behave identically to production using ImageMagick, particularly for colour profiles, animated formats and less common source images.
Existing image styles are not automatically redesigned
The change concerns newly created image styles. It should not be interpreted as a command to convert every existing style immediately. Existing styles may be part of a carefully managed design system, an external CDN contract or a content migration process.
Before changing an established style:
- Export configuration in a branch.
- Generate representative derivatives from large, small, portrait and landscape originals.
- Check visual quality and file sizes.
- Test cache and CDN behaviour.
- Review references in responsive image styles and theme templates.
Pay particular attention to private files. Drupal’s private file system is designed to prevent direct web-server access, but derivative generation and access checks still need to be tested together. Private files are served through Drupal, which adds processing overhead but allows access control to be applied. ([drupal.org](https://www.drupal.org/docs/getting-started/installing-drupal/securing-drupal-file-directories?utm_source=openai))
Understand the public and private upload work
Drupal 12.1 change records now include toggleable public and private uploaded files. This is an important area for editorial platforms that mix public media with restricted documents, but the change should not be reduced to “private files are now automatically secure”. File visibility still depends on the storage scheme, entity access and the way the file is linked to content.
The distinction remains fundamental:
- Public files can normally be served directly by the web server.
- Private files are delivered through Drupal so that access checks can be applied.
- A private file attached to publicly viewable content may still be downloadable by visitors who can view that content.
- A private directory must be outside the web root where possible, or protected correctly when that is not possible.
Review your file system settings in settings.php and in the administrative interface:
$settings['file_temp_path'] = '/var/www/example-site/shared/tmp';
$settings['file_private_path'] = '/var/www/example-site/shared/private';
The paths above are examples only. Use absolute paths appropriate to the deployment platform, keep the private directory outside the document root when possible and ensure that the PHP process can write to it without making the directory web-accessible.
Do not assume that changing a field’s upload destination is a complete access-control design. Test the following scenarios with an anonymous user, an authenticated user and a user who has access to the parent content:
- Published content with a private attachment.
- Unpublished content with the same attachment.
- Content access revoked after a file URL has been discovered.
- Media entity access differing from node access.
- Image derivatives requested directly rather than through the page.
Drupal’s own documentation stresses that private storage prevents direct web-server access, but the permissions governing the entity that references the file still determine whether Drupal will serve it. ([drupal.org](https://www.drupal.org/docs/administering-a-drupal-site/uploaded-file-management?utm_source=openai))
Use the evolving core CLI without making deployment brittle
The core CLI is becoming more capable. Recent change records add an extension-list command and uninstall commands for modules and themes in the Drupal 12.1 development line. The wider CLI work is intended to make commands discoverable from core and contributed extensions, not to eliminate every use case for Drush. ([drupal.org](https://www.drupal.org/list-change-updates/drupal?utm_source=openai))
For deployment scripts, avoid depending on commands that exist only in an unreleased development version. Pin the Drupal version in your project, run the command list in the target environment and treat command availability as part of the compatibility contract.
vendor/bin/dr list
vendor/bin/dr cache:rebuild
For a project that supports several Drupal minor versions, use a capability check rather than assuming that every environment exposes the same command:
if vendor/bin/dr list 2>/dev/null | grep -q 'cache:rebuild'; then
vendor/bin/dr cache:rebuild
else
vendor/bin/drush cr
fi
This is deliberately conservative. A production deployment should fail loudly if cache rebuilding is essential, so adapt the fallback logic to your release process and monitoring standards. Do not hide a failed cache rebuild behind a shell expression that always returns success.
Review Node.js requirements separately from site runtime requirements
Drupal 12 development requires Node.js 24 for Drupal core development. That requirement is primarily relevant to contributors and teams building Drupal core, running core’s frontend toolchain or maintaining a development workflow that uses core’s JavaScript assets. It does not automatically mean that every production Drupal server must run Node.js.
Separate your toolchains:
- PHP, Composer and the database runtime operate the Drupal application.
- Node.js and package managers build or test JavaScript and CSS assets.
- CI images may need Node.js even when production containers do not.
Declare the expected development version in the repository rather than relying on each developer’s workstation:
{
"engines": {
"node": "24.x"
}
}
Only add this metadata if it matches the project’s actual tooling and policy. It does not install Node.js or enforce the version by itself. Pair it with a CI image or version manager configuration that uses the same major version.
Build a compatibility branch now
A useful upgrade branch should be small enough to review and broad enough to expose problems. A practical sequence is:
- Update the local environment and record the current baseline.
- Run the existing test suite before changing dependencies.
- Update Drupal core and contributed modules within the supported minor line.
- Fix deprecations and serialisation problems in custom code.
- Test image derivatives and restricted files.
- Run configuration import and database updates on a disposable copy.
- Test the application under the target PHP and frontend toolchain versions.
- Deploy to staging and repeat the smoke tests with production-like files.
For Composer-managed projects, update the lock file in a controlled branch:
git switch -c chore/prepare-drupal-12
composer update "drupal/core-*" --with-all-dependencies
vendor/bin/drush updatedb:status
vendor/bin/drush config:status
vendor/bin/drush cr
git diff -- composer.json composer.lock
The core release documentation uses the same general Composer pattern for updating Drupal core with all dependencies. ([drupal.org](https://www.drupal.org/project/drupal/releases/11.0.0?utm_source=openai))
If the project uses the core CLI for commands that are already available in your installed version, use vendor/bin/dr consistently. Otherwise, keep Drush commands in the example above where the project already depends on Drush and the command is part of its established operational tooling.
Add targeted regression tests
Do not rely only on a full browser test suite. Add narrow tests for the areas changed by this upgrade:
- A kernel test that executes custom entity and database queries.
- A functional test covering access to private attachments.
- A media test that requests an image derivative.
- A deployment test that imports configuration on a clean database.
- A command smoke test for any core CLI command used by automation.
For custom modules, check that test fixtures do not serialise query objects indirectly. A test that passes because it uses a single request may not expose a queue or cache lifecycle problem. Exercise the actual boundary where the value is stored and restored.
Common upgrade mistakes to avoid
Updating core without updating custom documentation
Deprecated command paths often survive in README files, Makefiles, DDEV recipes and CI scripts long after the PHP code has been updated. Search the entire repository:
rg -n \
'core/scripts/drupal|core/scripts/dr|vendor/bin/dr|drush|node-version|private_path' \
. \
--glob '!vendor/**' \
--glob '!web/core/**'
Treating WebP as a universal replacement for source images
WebP derivatives are an output concern. Keep original source files in an appropriate format unless there is a specific editorial or storage reason to replace them. Check downloads, metadata workflows, image focal-point tools and any third-party integration that expects a particular extension.
Assuming a private directory solves authorisation
Storage protection and content authorisation are separate controls. A private directory can prevent direct HTTP access while Drupal still serves a file to anyone authorised to view the referencing entity. Model the desired access rules at the content or media layer and test direct URLs.
Making a large dependency update impossible to diagnose
Do not combine a major Drupal upgrade, a PHP upgrade, a frontend toolchain replacement and a theme rewrite in one unreviewable commit. Use separate branches or logically grouped commits so that failures can be assigned to a specific change.
A practical checklist for October 2026
- Confirm whether the site is on Drupal 10, 11 or an unsupported release.
- Record the exact Composer lock-file versions deployed in each environment.
- Search custom code for query objects entering caches, queues, sessions or batch data.
- Replace serialised query objects with query results or simple query definitions.
- Test newly created and existing image styles with the production image toolkit.
- Verify WebP derivatives for public and private file schemes.
- Audit the access rules for private files attached to published and unpublished content.
- Replace deprecated
core/scripts/drupalexamples withvendor/bin/drwhere supported. - Check CI and local development images for the Node.js version required by the target workflow.
- Run configuration import, database updates, cache rebuilds and smoke tests on staging.
- Track Drupal 12’s release schedule and Drupal 10’s 9 December 2026 end-of-life date.
Conclusion
The October 2026 Drupal changes are a reminder that successful upgrades are mostly about boundaries: the boundary between a query and its result, a source image and its derivative, a file’s storage location and its access policy, or a development tool and a production runtime.
Teams that address those boundaries now will have a much easier Drupal 12 upgrade later in the year. Start with an inventory, make small compatibility fixes, add regression tests around file and query behaviour, and keep deployment tooling explicit about which Drupal version it supports. The result is not merely a site that can install the next core release, but a codebase whose assumptions are visible, testable and maintainable.