Stop Making Users Pay for Work Your CI Pipeline Could Do
Most slow Drupal sites are not slow because the code is wrong. They are slow because expensive work runs at request time when it could have run in the CI pipeline instead. Cold caches, image derivatives, search indexes. The pattern repeats. The fix is the same in every case: move the cost off your users and onto a controlled environment where nobody is waiting. This is a principle, not a tip. Three examples below to make it concrete.
The Principle
There is a question I have started asking on every performance review: "Could this work have been done before the user arrived?"
The question sounds simple. In practice it changes how you architect systems.
Most performance discussions focus on making request-time work faster. Tune the query. Reduce the render passes. Cache the result. These are real wins. They are also second-order. The first-order question is whether the work should be happening at request time at all.
Some work genuinely has to. Anything personalized to the user. Anything that depends on data that did not exist before the request. Anything subject to real-time decisions. Fine. Optimize it.
But a meaningful percentage of "Drupal is slow" complaints trace back to work that did not need to happen at request time and could have been pre-computed in CI, in a queue, or during a scheduled job. That work is invisible until you ask the right question, and the right question is: who is paying for this, and could someone else have paid for it earlier?
Three examples make this concrete.
Example 1: Cache Warming After Deploy
The Drupal cache is empty right after a deploy or a cache rebuild. The next user to hit any page pays the cost of rebuilding the container, resolving every plugin, reloading every config object, and rendering every block, view, and entity that composes their page.
For a simple page on a small site, that is maybe 500ms. For a complex page on a Commerce site with paragraphs, views, and a multilingual content model, it is 5 to 10 seconds. The next ten users on nearby pages pay similar costs as their pages rebuild from cold. (Cache invalidation on distributed Drupal makes this worse — every tag bump restarts the rebuild for the affected entities.)
The Drupal community has tools for this. The Warmer contrib module enqueues entities and URLs for cacheability processing. CDN-level pre-fetching exists in Cloudflare and Fastly. Pantheon and Acquia have post-deploy hooks. None of these are wrong.
But for most sites, a shell script in the CI pipeline that hits the top 10 URLs after deploy is the right starting point:
#!/bin/bash BASE_URL="${1:-https://example.com}" URLS=("/" "/about" "/products" "/blog" "/contact") for path in "${URLS[@]}"; do curl -s -o /dev/null "${BASE_URL}${path}" doneThirty seconds added to the deploy. Cold-cache cost moves off your users entirely. The next real visitor hits a warm cache.
The question that surfaces this fix: who pays for the first request after deploy? The user, by default. Your CI pipeline, if you set it up to.
Example 2: Image Derivatives Generated on First Request
Drupal generates image style derivatives lazily by default. Upload a 4000x3000 photo, define a style that crops it to 800x600, and the derivative does not exist until someone requests it. The first request to that styled URL spawns ImageMagick or GD, processes the image synchronously, blocks the request thread, and writes the derivative to disk.
On a content-heavy site, this happens hundreds of times per day. Editorial uploads a new article with a hero image. The first user to load the article triggers derivative generation. The first user to see the article on the homepage triggers a different derivative for the smaller card thumbnail. The first user on mobile triggers a yet another for the responsive variant. Each one is slow.
The fix is to pre-generate derivatives at upload time, not request time. Several approaches:
- A custom hook on file save that triggers derivative generation for the styles the site actually uses
- A Drush command run after content imports that walks the media library and pre-generates everything
- The Image Style Warmup module, which automates the above pattern
In all three cases, the principle is the same. The derivative generation happens in a controlled context. Users never wait for ImageMagick to finish a resize.
The question that surfaces this fix: when an editor uploads an image, who generates the derivatives? Whoever loads the page first, by default. Your media-upload hook, if you wire it up.
Example 3: Search Index Updates on Content Save
Search API integrations (Solr, Elasticsearch, database-backed search) typically queue index updates when content changes. Most Drupal teams accept the default queue behavior and let updates flow through cron.
The problem is that cron defaults to every three hours on many sites. So a piece of content saved at 10am does not appear in search results until potentially 1pm. Editorial complains. Someone "fixes" it by triggering an index update on entity save synchronously, which now means every save takes an extra two seconds while waiting for Solr to acknowledge the update.
The right answer is in between. Move search index updates to a dedicated queue with its own worker, running every minute via supervisord rather than every three hours via cron. Now updates flow through asynchronously (the save returns instantly) but with low latency (search results stay fresh).
# /etc/supervisor/conf.d/search-indexer.conf [program:drupal_search_indexer] command=/usr/bin/drush queue:run search_api_indexing_queue directory=/var/www/html autorestart=true startsecs=5 user=www-dataThe question that surfaces this fix: when an editor saves a piece of content, who waits for the search index to update? The editor, if you run it synchronously. The next reader hitting search, if you wait for cron. A background worker, if you set it up.
The Pattern Across All Three
In each example, the principle is the same. There is work that has to happen at some point. The question is who pays for it.
The default behavior in Drupal (and most frameworks) puts the cost on whichever user happens to arrive first. The first user after deploy pays for cache rebuild. The first user to view an image pays for derivative generation. The first user to search after a content save pays for the index update.
That default is convenient because it requires no setup. It is also bad UX, because it makes the experience randomly slow for whoever wins the unlucky lottery of being first.
Shifting the work to CI, supervisord, or a background queue does not eliminate the work. It moves the cost to a context where waiting is acceptable, so the user-facing experience is consistently fast.
This Generalizes Beyond Drupal
The same pattern applies in any framework. A few examples I have seen on non-Drupal projects:
- Next.js sites with on-demand ISR that regenerate stale pages at request time, when they could be revalidated by a webhook on content change.
- Node services that compute aggregations on read that could be materialized into a view or denormalized table during write.
- React Native apps that fetch and process large lists at app open, when the previous app session could have pre-fetched and cached the data.
In every case, the question is the same. Who is paying for this work, and could someone else have paid for it earlier?
The framework-specific answers differ. The principle does not.
Why This Pattern Gets Missed
Three reasons it keeps showing up as a missed optimization.
First, the cost is invisible to the team that built it. Developers test on warm caches and seeded databases. The cold-start cost only appears in production, under traffic patterns the dev environment never simulates.
Second, the default behaviors are pragmatic, not optimal. Drupal generates image derivatives lazily because it works without configuration. Most defaults trade some user-facing performance for ease of setup. That trade is sometimes wrong.
Third, the fix usually requires touching the deploy pipeline. Many developers think of CI as someone else's responsibility, especially in agency settings where the DevOps person is different from the Drupal developer. Cache warming, image pre-generation, queue worker configuration all live at the boundary between code and infrastructure, and that boundary gets ignored.
Closing Thought
The senior move is not to make request-time code faster. It is to ask whether the work needs to happen at request time at all.
If it does, optimize it. If it does not, move it.
The pattern repeats across cache warming, image derivatives, search indexes, and a dozen other places where Drupal sites accumulate request-time cost that did not need to be there. Once you start looking for it, you cannot stop seeing it.
If you have shifted a different kind of work from request time to your CI pipeline or a background process, I would be curious which one and what the impact was. The interesting variations are not in cache warming. They are in the less obvious cases where someone realized the default was making users wait for something that did not need to happen on their thread.
Read next
- May 15, 2026
Drupal Got Cache Invalidation Right. The Rest of the Industry Is Catching Up.
Drupal's cache tag system is one of the better-designed cache invalidation models in modern web frameworks. A practitioner's look at why it works, where it breaks across system boundaries, and the mental model that travels to any stack.
- May 11, 2026
Drupal's Queue API is the Most Underused Feature in Core
Most Drupal teams reach for cron, Batch API, or external job runners when Queue API would solve the problem with less code and fewer moving parts. Three patterns where Queue API earns its place, with snippets and architecture diagrams.
- May 06, 2026
Anatomy of a Drupal Performance Crisis: When MySQL Is on Fire, More MySQL Isn't the Fix
A forensic walkthrough of stabilizing a high-traffic Drupal 9 platform under sustained bot pressure. Why the obvious fix was the wrong fix, and what actually worked.