---
title: "When WYSIWYG Lies: Debugging Drupal's Image Derivative Race Condition In CKEditor"
url: https://www.axelerant.com/blog/drupal-ckeditor-image-derivative-race-condition
published: 2026-07-25T10:00:00Z
author: "Bassam Ismail, Director of Digital Engineering"
source: Axelerant Thinking
---

# When WYSIWYG Lies: Debugging Drupal's Image Derivative Race Condition In CKEditor

> Why Drupal shows broken images only to first visitors after a cache clear, why QA misses it, and the pre-generation, web server, and CDN fixes that resolve it.

Drupal does not serve uploaded images directly at their original resolution. When a site defines image styles (thumbnail, hero, card, etc.), Drupal generates derivative files: resized, cropped, and optimized versions stored in a separate directory. This generation happens on demand.

The first time a browser requests a styled image URL, Drupal intercepts the request, generates the derivative, saves it to disk, and serves it. Every subsequent request serves the file directly.

This on-demand generation is a deliberate design choice. It avoids pre-generating every possible derivative for every uploaded image. A site with 12 image styles and 10,000 images would need 120,000 pre-generated files. On-demand generation means derivatives are only created when actually needed.

The system works reliably under normal conditions. A Twig template references an image with a specific style. On first page load, Drupal's image style routing generates the file inline, before the HTML response is sent to the browser. On every load after that, the web server serves the static file without involving Drupal at all.

The problem starts when CKEditor enters the picture.

## How CKEditor Breaks The Contract

When a content editor inserts an image through CKEditor in Drupal, the editor generates HTML markup that gets stored in the body field. This markup includes the image URL.

Here is where the race condition emerges. CKEditor's embed markup references a styled image derivative URL. But at the moment the editor inserts the image, the derivative may not exist yet. The editor preview works because it triggers a request to Drupal, which generates the derivative on the fly. The stored markup now contains a URL pointing to a file that was generated during the editing session's preview request.

This seems fine. The derivative exists. The URL is valid.

The race condition appears under a specific sequence:

- Step 1: Editor uploads image, inserts via CKEditor. Preview triggers derivative generation.

- Step 2: Cache clear, deployment, or image style config change occurs. Derivative files are flushed.

- Step 3: First visitor loads the page. Browser requests the derivative URL from stored CKEditor markup.

- Step 4: Drupal receives the request, begins generating the derivative. File does not yet exist on disk.

- Step 5: Browser times out or receives a 404 / 302. Broken image displayed.

- Step 6: Second visitor loads the same page. Derivative now exists, image renders correctly.

Step 5 is the critical failure. Drupal's image derivative generation is not instantaneous. For large source images, complex crop calculations, or sites under load, the generation takes noticeable time.

If the web server's timeout is short, or if a CDN sits in front and caches the failed response, the first visitor gets a broken image. In the CDN scenario, the broken response gets cached and served to subsequent visitors until the CDN cache expires.

## Why QA Almost Never Catches This

This bug has a [structural invisibility problem](/what-we-do/engineering/quality). The people most likely to encounter it are the least likely to be testing the site when it happens.

Content editors always see a working image because their editing session triggers derivative generation. QA engineers testing on staging environments have usually loaded the page multiple times, meaning derivatives already exist. The only person who sees the bug is the first visitor after a cache clear or deployment. In a typical QA workflow, that visitor does not exist as a defined test case.

The typical symptom pattern: intermittent "image not loading" reports that no one can consistently reproduce. The image works sometimes. It does not work other times. The reproduction steps appear to be "load the page," which is unhelpful because the bug only manifests on the very first uncached load after a derivative flush.

The key to reliable reproduction:

Once the precondition is understood (derivatives must not exist), the reproduction rate is 100%. Broken image on first load. Working image on second load. Every time.

## The CKEditor Amplification Factor

Standard Drupal image rendering through Twig templates handles this race condition gracefully. When a template renders an image with an image style, it uses Drupal's image rendering pipeline. The system generates the derivative inline during the page render process if it does not exist, meaning the HTML is not sent to the browser until the derivative URL is valid.

CKEditor bypasses this pipeline entirely. The image URL is baked into the stored body field HTML as a static string. When Drupal renders the body field, it outputs that string verbatim.

There is no opportunity for the image style system to intervene, check whether the derivative exists, and generate it before the page is sent.

This is the core architectural mismatch:

- Twig template with image style: derivative check at render time, generation inline before HTML output. Low race condition risk.

- CKEditor embedded image in body field: no derivative check, generation deferred to browser request. High race condition risk.

- CKEditor with media entity embed: partial check depending on implementation, timing varies. Medium race condition risk.

The media entity embed path (using the Entity Embed or Media Embed module) offers more protection because it can re-render the image reference at page render time rather than serving a static URL. But many Drupal implementations still use direct file URL insertion in CKEditor, especially sites built before the media system was stabilized in core.

## The Fix: Three Layers, Not One

There is no single configuration toggle that resolves this. The fix requires changes at three levels.

### Layer 1: Pre-Generate Derivatives On Upload

The most direct fix is to trigger derivative generation at upload time rather than at first request. Drupal does not do this by default. A custom module with a hook_entity_presave implementation on media or file entities can iterate through all image styles and force generation:

This ensures that by the time CKEditor references the derivative URL, the file exists on disk. The tradeoff is upload time. Each image style requires a separate generation pass, proportional to the source image resolution and the complexity of the style's effects (scale, crop, sharpen). Content teams generally accept this tradeoff once they understand it eliminates broken images for visitors.

### Layer 2: Configure The Web Server Fallback

For derivatives that are not pre-generated (because of cache clears, config changes, or edge cases), configure the web server to proxy missing derivative requests through to Drupal rather than returning a 404.

Nginx configuration:

Apache .htaccess (typically already present in Drupal's default .htaccess, but verify it is not overridden by custom rules):

The critical detail: ensure no custom rewrite rule intercepts requests to /sites/default/files/styles/ and returns a 404 before Drupal's image style route can handle them. This is a common misconfiguration on sites with aggressive static file handling rules.

### Layer 3: CDN Cache Rules For Image Derivative Paths

If a CDN sits in front of the site, configure it to not cache error responses from image derivative paths. Without this rule, a CDN caches the failed response from Step 5 in the race condition sequence, transforming a single-visitor problem into one that persists for every visitor until the cache expires.

Cloudflare Cache Rules example:

Fastly VCL snippet (in vcl_fetch / vcl_backend_response):

The principle is the same regardless of CDN vendor: only cache responses from image derivative paths when the response status is 200 and the Content-Type header indicates an image. Any 302, 404, or 5xx response from these paths should pass through uncached.

- Pre-generate on upload prevents the race condition for new images, with the tradeoff of slower upload processing proportional to the number of defined image styles.

- Web server fallback configuration prevents 404 responses for missing derivatives, requiring verification of existing rewrite rules.

- CDN cache rules for derivative paths prevents cached broken responses, requiring CDN configuration access and vendor-specific syntax.

All three layers should be implemented together. Layer 1 handles the common case (new images). Layer 2 handles the edge case (cache clears and config changes). Layer 3 prevents any remaining failures from being amplified across visitors.

## The Diagnostic Checklist

For teams suspecting this issue on their own [Drupal sites](/platforms/drupal):

1. Clear sites/default/files/styles/ entirely on staging.

1. Open a page with a CKEditor-embedded image in incognito.

1. Image breaks on first load, works on refresh? Race condition confirmed.

1. Repeat step 2 through your CDN from a different IP.

1. CDN serves broken image even after origin has generated the derivative? CDN is caching the failure response.

1. If step 3 confirms: implement all three fix layers.

1. If step 5 confirms: prioritize Layer 3 first (widest blast radius).

## What This Pattern Reveals About CMS Architecture

This bug is not a CKEditor deficiency or a Drupal deficiency in isolation. It is an architectural seam between two systems that make different assumptions about when an image file will exist.

Drupal's image style system assumes derivatives are generated on first request by Drupal's routing layer. CKEditor's embed system assumes the URL it stores will be valid whenever a browser requests it.

Both assumptions are reasonable independently. Together, they create a window where neither system is responsible for ensuring the image exists.

The Drupal image derivative race condition is a class of bug where two components in a CMS pipeline make incompatible assumptions about asset availability, resulting in failures that only manifest under specific sequencing conditions that normal testing workflows do not cover.

This pattern recurs across CMS platforms wherever a rich text editor stores resolved asset URLs rather than asset references. It shows up in the same shape when teams move from a legacy CMS toward [AI-composed content pipelines](/blog/acquia-source-cms-ai-content-teams), which is one reason we track it closely in our [Drupal engineering practice](/platforms/drupal).

The fix is conceptually the same in every case: ensure the asset exists before the URL is stored, or ensure the rendering pipeline can generate the asset before serving the page. Direct URL storage in body fields trades reliability for simplicity.

For any Drupal site with active content editors and a caching layer in front, that trade is not worth making. The reliability cost shows up as intermittent broken images that no one can reproduce on demand, and the debugging cost compounds every time a new engineer joins the team and tries to make sense of it.

## Frequently Asked Questions

### Does This Affect CKEditor 5 Or Only Older Versions?

The race condition affects any Drupal + CKEditor configuration where image derivative URLs are stored directly in the body field markup. CKEditor 5 with the updated media integration may reduce exposure if configured to use entity embed references rather than static file URLs, but the fix depends on implementation choices, not the editor version.

### Why Does The Image Always Work On The Second Page Load?

The first request for a missing derivative triggers Drupal's image style generation system, which creates and saves the file. But by that point, the browser has already rendered the page with a broken image or placeholder. On the second load, the derivative file exists on disk and the web server serves it directly as a static file. The bug only fails once per derivative per cache clear cycle, which is why it appears intermittent.

If your team is seeing intermittent broken images on Drupal and needs a second set of eyes on the fix, our [Drupal engineering practice](/platforms/drupal) runs [one-page scoping sessions](/contact) to map the derivative pipeline, CDN behavior, and CKEditor configuration end to end.

---

Read on the web: https://www.axelerant.com/blog/drupal-ckeditor-image-derivative-race-condition
