Skip to main content

Images / NGINX

NGINX Automatic Image Conversion: WebP and AVIF

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

Images account for over half of a typical webpage’s total weight. A single product page might serve dozens of JPEG and PNG files, each one bloated by decades-old encoding. Modern formats like WebP and AVIF offer 25-50% smaller file sizes at equivalent visual quality. Yet most sites still serve the old formats because NGINX automatic image conversion has traditionally been difficult to set up.

The usual approaches each have drawbacks. Pre-generating WebP copies during deployment doubles your storage and complicates your build pipeline. Running a separate image proxy service like imgproxy adds infrastructure overhead and latency. Content negotiation with a map block and try_files works, but only for static files with pre-generated WebP versions already on disk.

What if NGINX could handle the conversion itself – transparently, for any image source, without changing a single URL?

That is exactly what ngx_immerse does. It is a filter module that enables NGINX automatic image conversion by intercepting image responses in the filter chain. When a client’s browser supports WebP or AVIF, the module converts the image on the fly using a thread pool (keeping workers non-blocking) and caches the result. It works with static files, proxy_pass, FastCGI, and anything else that produces image responses. No URL rewriting. No separate service. No application changes.

How ngx_immerse Works

The module inserts itself into the NGINX output filter chain – the same mechanism that modules like gzip use to transform responses. Here is the request flow:

  1. A client requests an image (for example, /photos/sunset.jpg)
  2. NGINX fetches the response from whatever source is configured – a local file, a proxied backend, or FastCGI
  3. ngx_immerse inspects the response Content-Type to check if it is image/jpeg, image/png, or image/gif
  4. The module parses the client’s Accept header to determine which modern formats the browser supports
  5. If a supported modern format is found, the module either serves a cached conversion or triggers a new one
  6. The converted image is returned with the correct Content-Type and a Vary: Accept header

The module detects input formats by inspecting magic bytes in the response body, not by looking at file extensions. This means it works correctly even when URLs lack traditional image extensions.

Lazy vs. Sync Conversion Modes

ngx_immerse supports two conversion modes that handle cache misses differently:

Lazy mode (default) serves the original image immediately with zero latency overhead. If the image source is file-backed, the module queues a background conversion in a thread pool. The next request for the same image gets the cached modern format. This is ideal for static file serving.

Sync mode buffers the entire response body, converts it in a thread pool (the NGINX event loop is not blocked), and serves the converted image in the same request. Use this for proxied content where you need every response in a modern format.

Accept Header Parsing

The module parses the Accept header per RFC 7231 with full quality factor support:

Accept Header Result (default format priority)
image/avif, image/webp AVIF (higher priority in config)
image/webp WebP
image/avif;q=0.8, image/webp;q=0.9 WebP (higher quality factor)
image/avif;q=0, image/webp WebP (AVIF explicitly rejected)
text/html, image/jpeg Original image (no modern format)

A quality factor of q=0 means the client explicitly rejects that format. The module respects this and never serves a rejected format.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the GetPageSpeed repository and then the module package:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-immerse

Then load the module by adding this line at the top of /etc/nginx/nginx.conf, before the events block:

load_module modules/ngx_http_immerse_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-immerse

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

Configuration

Prerequisites

ngx_immerse requires NGINX’s built-in thread pool support for non-blocking conversion. Declare at least one thread_pool in the main context:

thread_pool immerse threads=4;

Start with a thread count equal to your CPU cores. Image encoding is CPU-bound, so more threads than cores provides no benefit.

You must also declare a cache path at the http level:

immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;

Without this directive, the module logs an error and passes all images through unchanged.

Minimal Configuration

Here is the simplest configuration that enables NGINX automatic image conversion for static files:

load_module modules/ngx_http_immerse_module.so;

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;

    server {
        listen 80;
        server_name example.com;

        location /images/ {
            immerse on;
            immerse_thread_pool immerse;
            alias /var/www/images/;
        }
    }
}

With this configuration, any JPEG, PNG, or GIF served from /images/ converts automatically to AVIF or WebP based on browser support. AVIF takes priority when both formats are accepted because it typically achieves better compression.

Full Configuration Example

This example demonstrates all available directives:

load_module modules/ngx_http_immerse_module.so;

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2
                       max_size=2g inactive=60d;

    # Defaults for all locations
    immerse_formats avif webp;
    immerse_webp_quality 82;
    immerse_avif_quality 63;

    server {
        listen 80;
        server_name example.com;
        root /var/www/html;

        # Static images with custom size thresholds
        location /images/ {
            immerse on;
            immerse_thread_pool immerse;
            immerse_min_size 2k;
            immerse_max_size 5m;
            alias /var/www/images/;
        }

        # Thumbnails - WebP only, lower quality
        location /thumbnails/ {
            immerse on;
            immerse_formats webp;
            immerse_webp_quality 75;
            immerse_thread_pool immerse;
            alias /var/www/thumbs/;
        }

        # CDN-facing location - disable debug header
        location /cdn/ {
            immerse on;
            immerse_x_header off;
            immerse_thread_pool immerse;
            alias /var/www/cdn/;
        }
    }
}

Proxied Content

For images served through proxy_pass, use sync mode. This ensures the converted format is served on the first request:

load_module modules/ngx_http_immerse_module.so;

thread_pool immerse threads=4;

http {
    immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;

    upstream backend {
        server 127.0.0.1:8080;
    }

    server {
        listen 80;
        server_name example.com;

        location /api/images/ {
            immerse on;
            immerse_mode sync;
            immerse_thread_pool immerse;
            proxy_pass http://backend;
        }
    }
}

In sync mode, the module buffers the upstream response, converts it in the thread pool, and serves the modern format. The NGINX event loop remains free because conversion runs in a separate thread.

Directives Reference

immerse

Syntax: immerse on | off;
Default: off
Context: location

Enables or disables automatic image format conversion for the location. Requires immerse_cache_path at the http level and a valid thread_pool in the main context.

immerse_cache_path

Syntax: immerse_cache_path path [levels=levels] [max_size=size] [inactive=time];
Default: none (required)
Context: http

Sets the cache directory for converted images. The directory is created automatically if it does not exist.

  • path – filesystem directory for cached conversions
  • levels – subdirectory hierarchy depth as colon-separated digits (default: 1:2)
  • max_size – maximum total cache size with suffixes k, m, g (default: no limit)
  • inactive – removal time for unused cached files, with suffixes s, m, h, d (default: 30d)

immerse_formats

Syntax: immerse_formats format ...;
Default: avif webp
Context: http, server, location

Sets the preferred output formats in priority order. When the client supports multiple formats with equal quality factors, the first format listed wins. Valid values: avif and webp.

# Prefer WebP over AVIF
immerse_formats webp avif;

# WebP only (skip AVIF encoding entirely)
immerse_formats webp;

immerse_mode

Syntax: immerse_mode lazy | sync;
Default: lazy
Context: location

Controls how cache misses are handled. In lazy mode, the original image is served immediately and conversion happens in the background. In sync mode, the response is buffered and converted before being sent.

immerse_webp_quality

Syntax: immerse_webp_quality quality;
Default: 80
Context: http, server, location

WebP encoding quality from 1 to 100. Higher values mean better visual quality at larger file sizes. Values around 75-85 are a good balance for most web content.

immerse_avif_quality

Syntax: immerse_avif_quality quality;
Default: 60
Context: http, server, location

AVIF encoding quality from 1 to 100. AVIF achieves good visual quality at lower numeric values than WebP or JPEG. Values around 50-70 are typical for web delivery.

immerse_min_size

Syntax: immerse_min_size size;
Default: 1k
Context: http, server, location

Minimum response body size for conversion. Images smaller than this pass through unchanged. This avoids wasting CPU on tiny images like favicons or tracking pixels.

immerse_max_size

Syntax: immerse_max_size size;
Default: 10m
Context: http, server, location

Maximum response body size for conversion. Images larger than this pass through unchanged. This prevents resource exhaustion from very large images.

immerse_thread_pool

Syntax: immerse_thread_pool name;
Default: default
Context: http, server, location

Name of the NGINX thread pool for conversion tasks. Must match a thread_pool directive in the main context.

immerse_x_header

Syntax: immerse_x_header on | off;
Default: on
Context: http, server, location

Controls the X-Immerse debug response header. When enabled, every processed response includes a status header:

Value Meaning
hit Served a cached conversion
miss Cache miss – converted (sync) or original served (lazy)
error Conversion failed – original served as fallback

Disable this in production to avoid exposing internal state to clients.

Testing Your Configuration

After installing and configuring the module, verify that NGINX automatic image conversion works correctly.

Verify the Module Loads

Test your configuration syntax:

nginx -t

If the module loads and your directives are valid, you will see syntax is ok and test is successful.

Test Format Negotiation with curl

Send a request with a WebP-capable Accept header:

curl -s -D- -H "Accept: image/webp,image/jpeg" \
  http://localhost/images/photo.jpg -o /dev/null

On the first request with lazy mode, the original format is served with X-Immerse: miss:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Vary: Accept
X-Immerse: miss

On subsequent requests, the cached WebP conversion is served:

HTTP/1.1 200 OK
Content-Type: image/webp
Vary: Accept
X-Immerse: hit

Verify Fallback for Unsupported Browsers

Send a request without modern format support:

curl -s -D- -H "Accept: image/jpeg" \
  http://localhost/images/photo.jpg -o /dev/null

The original image is served unchanged, with Vary: Accept still present:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Vary: Accept

Inspect the Cache

Check the cache directory to confirm conversions are stored:

find /var/cache/nginx/immerse -type f -ls

You should see .webp and/or .avif files in the directory hierarchy defined by levels.

Performance Considerations

CPU Impact

Image encoding is CPU-intensive. A single JPEG-to-WebP conversion takes approximately 10-50ms depending on image dimensions and CPU speed. The thread pool ensures this work does not block the NGINX event loop. Size your thread pool appropriately:

  • Low traffic (under 100 images/second): 2-4 threads
  • Medium traffic (100-1000 images/second): 4-8 threads
  • High traffic (over 1000 images/second): Use lazy mode and let the cache absorb the load

Cache Warming

In lazy mode, the first request for each image serves the original format. The cache warms organically as images are requested. For sites with a CDN, the CDN caches the original format first. Modern format delivery begins after the CDN cache expires.

For time-sensitive deployments, warm the cache by crawling your image URLs with a WebP-capable Accept header:

curl -s https://example.com/sitemap.xml | \
  grep -oP '<loc>[^<]+\.(jpg|png)</loc>' | \
  sed 's/<\/?loc>//g' | \
  xargs -P4 -I{} curl -s -H "Accept: image/webp" -o /dev/null {}

Cache Invalidation

Cache invalidation is automatic. The cache key includes the source image’s modification time (mtime). Updating an original image invalidates its cached conversions. No manual cache purging is needed.

To manually clear the entire cache:

rm -rf /var/cache/nginx/immerse/*

No NGINX reload is required. The module recreates subdirectories as needed.

Memory Usage

The module buffers the original image in memory during sync mode. The immerse_max_size directive (default 10 MB) caps the maximum buffer. For servers with limited memory, reduce this value:

immerse_max_size 2m;

Security Best Practices

Disable Debug Headers in Production

The X-Immerse header reveals internal module behavior. Disable it in production:

immerse_x_header off;

Set Size Limits

Prevent resource exhaustion by setting appropriate min/max size limits:

immerse_min_size 1k;    # Skip tiny images (favicons, tracking pixels)
immerse_max_size 5m;    # Skip huge images that would consume too much CPU

Cache Directory Permissions

Ensure the cache directory is owned by the NGINX worker user:

sudo mkdir -p /var/cache/nginx/immerse
sudo chown nginx:nginx /var/cache/nginx/immerse
sudo chmod 700 /var/cache/nginx/immerse

Graceful Error Handling

ngx_immerse follows a strict “never break what works” policy. If anything goes wrong, the original image is served unchanged:

Condition Behavior
Conversion fails (codec error) Serve original, log error
Cache write fails (full disk) Serve converted from memory, log warning
Corrupt or truncated input Serve original, log error
Image below min_size or above max_size Pass through unchanged
No modern format in client Accept Pass through unchanged
Thread pool not found Fall back to synchronous conversion
Unknown image format Pass through unchanged

The module never returns a 500 error due to a conversion failure. This makes it safe to deploy without risking image delivery outages.

The Vary Header and CDN Compatibility

The module automatically adds Vary: Accept to every response when active, even for pass-through responses. This header is critical for correct CDN behavior. Without it, a CDN might cache a WebP response and serve it to a JPEG-only client.

If you use a CDN in front of NGINX, verify that your CDN respects the Vary header. Most major CDNs (Cloudflare, Fastly, AWS CloudFront) support this correctly.

For more details on why Vary: Accept matters, see our guide on serving WebP images automatically with NGINX.

Comparison with Other Approaches

How does ngx_immerse compare to other NGINX automatic image conversion methods?

Feature ngx_immerse Pre-generated WebP imgproxy PageSpeed
AVIF support Yes No Yes No
Works with proxy_pass Yes No Yes Yes
Requires URL changes No No Yes No
Separate service needed No No Yes No
Thread pool integration Yes N/A N/A No
Built-in caching Yes Manual External Yes
Input format detection Magic bytes File extension File extension Mixed
Quality factor parsing RFC 7231 Basic N/A Basic

Troubleshooting

Images Not Being Converted

If images are served in their original format despite the module being enabled:

  1. Check the Accept header – verify the client sends image/webp or image/avif
  2. Check image size – images below immerse_min_size (1 KB) or above immerse_max_size (10 MB) are skipped
  3. Check the cache path – ensure immerse_cache_path is declared and the directory is writable
  4. Check the error log – look for immerse: prefixed messages
  5. Enable the debug header – set immerse_x_header on and check X-Immerse

Cache Not Populating

If the cache directory remains empty after requests:

  1. Permissions – verify the NGINX worker user can write to the cache directory
  2. Lazy mode timing – in lazy mode, the cache populates after the first request (the second request gets the conversion)
  3. Disk space – check available space; the module logs a warning on write failures

High CPU Usage

If CPU usage spikes after enabling the module:

  1. Reduce thread pool size – fewer threads means fewer concurrent conversions
  2. Raise immerse_min_size – skip small images that save few bytes
  3. Lower immerse_max_size – skip large images that take longest to convert
  4. Use lazy mode – spreads conversion load over time instead of blocking requests

Conclusion

The ngx_immerse module eliminates the gap between knowing that modern image formats save bandwidth and actually serving them. Instead of maintaining conversion pipelines or running separate services, you add a few lines of NGINX configuration. Every image response is then automatically optimized based on browser support.

For simpler setups that only need WebP for static files, see our guide on serving WebP images automatically with NGINX. For sites that need both WebP and AVIF support across static files, proxied content, and FastCGI responses, ngx_immerse provides complete NGINX automatic image conversion with zero application changes.

The module is available from the GetPageSpeed repository for RHEL-based distributions.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.