Skip to main content

NGINX

NGINX Concat Module: Combine CSS and JS Files

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.

The NGINX concat module combines multiple CSS and JavaScript files into a single HTTP response, reducing request overhead and improving page load performance. Every additional HTTP request adds latency — DNS lookups, TCP handshakes, TLS negotiations, and round-trip delays. This module eliminates that overhead for static assets.

Originally developed by Alibaba for the Tengine web server that powers Taobao.com, the NGINX concat module brings Apache mod_concat-style file concatenation to NGINX. It works by reading multiple files from disk and streaming their combined contents back to the client in one response.

In this guide, you will learn how to install, configure, and use this powerful concatenation module to optimize static asset delivery. You will also learn when concatenation still makes sense in the HTTP/2 era — and when it does not.

How the NGINX Concat Module Works

The module uses a special URL syntax with double question marks (??) to request multiple files in a single HTTP request. The server reads all requested files from disk, concatenates their contents, and returns them as one response.

Here is an example of a concatenated request:

https://example.com/static/css/??reset.css,layout.css,theme.css

This single request tells NGINX to combine reset.css, layout.css, and theme.css into one response. The browser receives all three files in a single HTTP response with the correct Content-Type header.

URL Syntax

The concat module recognizes requests that match this pattern:

/path/to/directory/??file1.ext,file2.ext,file3.ext

Key points about the URL syntax:

  • The path must end with a slash (/) before the ??
  • File names are separated by commas (,)
  • An optional version string can follow a third ? for cache busting: ??file1.css,file2.css?v=1234
  • The module only processes GET and HEAD requests

What Happens Internally

When a concatenated request is processed, the module performs these steps:

  1. Validates the request: Checks that concat is enabled, the URI ends with /, and the query string starts with ??
  2. Parses file names: Splits the comma-separated file list from the query string
  3. Checks file count: Rejects the request with HTTP 400 if the file count exceeds concat_max_files
  4. Validates MIME types: If concat_unique is enabled, verifies all files share the same Content-Type
  5. Opens each file: Uses NGINX’s open file cache for efficient disk access
  6. Builds the response: Chains file buffers together (with optional delimiters) and sends them as a single response
  7. Sets Last-Modified: Uses the newest modification time among all concatenated files

Is Concatenation Still Useful with HTTP/2?

This is the critical question. HTTP/2 introduced multiplexing, which allows the browser to download multiple files simultaneously over a single TCP connection. This eliminates the “six connections per origin” limit of HTTP/1.1 that originally made concatenation essential.

However, concatenation is not obsolete. Here is when the NGINX concat module still provides measurable benefits:

When Concatenation Helps

  • Many small files: If your application loads 30 or more tiny CSS or JavaScript files, the per-request overhead (HTTP headers, stream framing, priority signaling) adds up even with HTTP/2 multiplexing
  • Better compression ratios: A single larger file compresses more efficiently with gzip or Brotli than many small files individually. Compression algorithms build better dictionaries with more data
  • Reduced server load: Serving one concatenated response requires fewer file descriptors, fewer buffer allocations, and fewer calls to sendfile() than serving each file separately
  • HTTP/1.1 clients: Mobile networks, corporate proxies, and older clients may still negotiate HTTP/1.1 where concatenation dramatically reduces page load time
  • CDN edge caching: A single concatenated URL is one cache entry instead of many, which simplifies cache invalidation and reduces storage at the edge

When Concatenation Hurts

  • Cache granularity: If you change one file, the entire concatenated bundle’s cache is invalidated. Separate files allow browsers to cache unchanged assets independently
  • Selective loading: Modern web applications use code splitting to load only what each page needs. Concatenation can force downloading CSS or JavaScript that the current page does not use
  • HTTP/2 prioritization: HTTP/2 allows browsers to prioritize critical resources and 103 Early Hints can preload them before the main response arrives, both of which work better with individual resources

The Practical Recommendation

Use the NGINX concat module when you have a set of small, stable, commonly-loaded files that are needed together on most pages. A typical example is a base CSS framework split into reset.css, grid.css, typography.css, and theme.css — files that rarely change independently and are always loaded together.

Do not use concatenation as a replacement for proper build-time bundling with tools like webpack, Vite, or esbuild. Server-side concatenation is most valuable when you cannot modify the frontend build process or when you need server-side control over asset grouping.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the NGINX concat module from the GetPageSpeed RPM repository:

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

After installation, load the module by adding this line to the top of /etc/nginx/nginx.conf (before the events block):

load_module modules/ngx_http_concat_module.so;

Debian and Ubuntu

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

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

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

Verify the Installation

Confirm that the module loads correctly:

nginx -t

If the configuration test passes and you have concat directives in your config, the module is loaded and working. You can also confirm the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_concat_module.so

Configuration Reference

The NGINX concat module provides six directives, all of which can be set at the http, server, or location level.

concat

Enables or disables file concatenation.

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

location /static/ {
    concat on;
}

concat_max_files

Sets the maximum number of files that can be concatenated in a single request. Returns HTTP 400 (Bad Request) if a request exceeds this limit.

Syntax: concat_max_files number;
Default: 10
Context: http, server, location

location /static/ {
    concat on;
    concat_max_files 20;
}

concat_unique

When enabled, all files in a concatenated request must have the same MIME type. For example, you cannot mix .css and .js files in one request. Returns HTTP 400 if types do not match.

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

This is a safety feature. Mixing CSS and JavaScript in a single response produces invalid output because the browser interprets the entire response according to the Content-Type header. Keep this enabled unless you have a specific reason to disable it.

concat_types

Specifies which MIME types are eligible for concatenation. Files with types not in this list are rejected.

Syntax: concat_types mime-type ...;
Default: application/javascript text/css
Context: http, server, location

To add additional types (for example, plain text files):

location /static/ {
    concat on;
    concat_types text/css application/javascript text/plain;
}

concat_delimiter

Inserts a string between each concatenated file. This is useful for debugging, ensuring valid syntax between concatenated files, or adding visual separators.

Syntax: concat_delimiter string;
Default: none (empty string)
Context: http, server, location

For JavaScript files, a semicolon and newline delimiter prevents syntax errors when one file does not end with a semicolon:

location /static/js/ {
    concat on;
    concat_delimiter "\n;\n";
}

For CSS files, a comment delimiter helps identify file boundaries when debugging:

location /static/css/ {
    concat on;
    concat_delimiter "\n/* --- */\n";
}

concat_ignore_file_error

When enabled, the module skips files that return 404 (Not Found) or 403 (Forbidden) instead of failing the entire request. The remaining files are still concatenated and returned.

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

location /static/ {
    concat on;
    concat_ignore_file_error on;
}

This is useful in development environments where files may be temporarily missing. Do not enable this in production unless you understand the implications — silently serving incomplete bundles can cause broken layouts or JavaScript errors.

Complete Configuration Example

Here is a production-ready configuration that combines the NGINX concat module with gzip compression and browser caching:

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

    # Concatenated CSS assets
    location /assets/css/ {
        concat on;
        concat_max_files 15;
        concat_unique on;
        concat_delimiter "\n";

        gzip on;
        gzip_types text/css;
        gzip_min_length 256;

        add_header Cache-Control "public, max-age=2592000, immutable";
    }

    # Concatenated JavaScript assets
    location /assets/js/ {
        concat on;
        concat_max_files 15;
        concat_unique on;
        concat_delimiter "\n;\n";

        gzip on;
        gzip_types application/javascript;
        gzip_min_length 256;

        add_header Cache-Control "public, max-age=2592000, immutable";
    }
}

Note that we use a single add_header Cache-Control directive instead of combining expires with add_header. Using both would produce duplicate Cache-Control headers in the response.

In your HTML, reference concatenated assets like this:

<link rel="stylesheet" href="/assets/css/??reset.css,grid.css,typography.css,theme.css">
<script src="/assets/js/??utils.js,app.js,analytics.js"></script>

Cache Busting with Version Strings

The concat module supports version strings for cache busting. Add a version parameter after a third ?:

<link rel="stylesheet" href="/assets/css/??reset.css,grid.css,theme.css?v=20260302">

The module strips the version string before processing files, so the actual files on disk do not need version numbers in their names. When you update any file, change the version string to force browsers to fetch the new concatenated bundle.

Testing the Configuration

After configuring concatenation, verify that everything works correctly.

Step 1: Test Configuration Syntax

nginx -t

Step 2: Reload NGINX

sudo systemctl reload nginx

Step 3: Test Concatenated Requests

# Test CSS concatenation - check response headers
curl -I "http://localhost/assets/css/??reset.css,layout.css"

# Verify response body contains all files
curl -s "http://localhost/assets/css/??reset.css,layout.css"

A successful response returns HTTP 200 with Content-Type: text/css, a single Cache-Control header, and the combined contents of all requested files.

Step 4: Test Error Handling

# Should return 400 - mixed CSS and JS types with concat_unique on
curl -I "http://localhost/assets/css/??reset.css,script.js"

# Should return 404 for missing files (unless concat_ignore_file_error is on)
curl -I "http://localhost/assets/css/??reset.css,nonexistent.css"

Performance Considerations

Compression Efficiency

Concatenated files compress better than individual files. A single larger file allows gzip and Brotli to find more repeated patterns and build better compression dictionaries. This compression advantage grows with the number of files combined.

Open File Cache Integration

The module leverages NGINX’s built-in open_file_cache. Enable it to avoid repeated disk lookups:

open_file_cache max=1000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;

Last-Modified Header Behavior

Concatenated responses use the newest modification time among all included files as the Last-Modified header value. This enables proper browser caching with conditional requests (If-Modified-Since). However, note that concatenated responses do not include an ETag header, which means If-None-Match validation is not available for combined bundles.

Sendfile Optimization

The module sends file contents directly from disk using NGINX’s internal file buffer chain. This works efficiently with the sendfile directive when available, avoiding unnecessary memory copies between kernel and user space.

Security Best Practices

Limit the File Count

Always set concat_max_files to a reasonable value. Without a limit, an attacker could craft requests with hundreds of file paths, causing NGINX to attempt opening many file descriptors simultaneously:

concat_max_files 20;

Keep concat_unique Enabled

The concat_unique on setting (which is the default) prevents mixing file types. Disabling it could allow an attacker to inject JavaScript content into what a browser expects to be a CSS response, potentially leading to MIME-type confusion attacks.

Restrict to Static Asset Directories

Only enable concatenation in specific locations that serve static files:

# Good: Restricted to known asset paths
location /assets/css/ {
    concat on;
}

# Bad: Enabled site-wide
location / {
    concat on;  # Don't do this
}

Path Traversal Protection

The module uses NGINX’s built-in ngx_http_parse_unsafe_uri() function to validate each file path in the concatenated request. This prevents directory traversal attacks like ??../../../etc/passwd. Therefore, it is safe to use without additional path sanitization.

Troubleshooting

Concatenation Not Working

Symptom: Requests with ?? syntax return 404 or serve the directory index instead of concatenated files.

Solutions:

  1. Verify the module is loaded. Add a directive like concat on; to your config and run nginx -t. If NGINX reports “unknown directive”, the module is not loaded.

  2. Ensure the URI path ends with a / before the ??. The module only activates when the base path ends with a slash:

# Works
/static/css/??file1.css,file2.css

# Does NOT work (no trailing slash)
/static/css??file1.css,file2.css

HTTP 400 Bad Request

Possible causes:

  • Mixed MIME types with concat_unique on: All files must have the same extension and MIME type
  • Too many files: Request exceeds the concat_max_files limit
  • Empty file name: A request like ??file1.css,,file2.css with consecutive commas

HTTP 404 Not Found

File paths are resolved relative to the location’s root or alias. Verify that:

  • The root directive points to the correct directory
  • All listed files actually exist on disk
  • File permissions allow the NGINX worker process to read them

Delimiter Not Appearing

The delimiter is only inserted between files, not before the first or after the last. If you concatenate a single file, no delimiter appears. This is expected behavior.

Alternatives to Consider

Before choosing the NGINX concat module, consider these alternatives:

  • Build-time bundling (webpack, Vite, Rollup): Best for most modern applications. Produces optimized bundles with tree-shaking, minification, and content-hash file names
  • HTTP/2 multiplexing: If your server and all clients support HTTP/2, the penalty for many requests is drastically reduced. However, it does not eliminate per-request overhead entirely
  • 103 Early Hints: Lets the server tell the browser about resources before the main response arrives, enabling parallel downloads without concatenation
  • Inlining small assets: For critical CSS under 14 KB, inlining directly into the HTML document eliminates the external request entirely

Server-side concatenation is most effective when you cannot control the frontend build pipeline, or when you need to dynamically group assets based on server-side logic.

Conclusion

The NGINX concat module provides a simple, effective way to combine multiple CSS and JavaScript files into single HTTP responses. While HTTP/2 multiplexing has reduced the need for concatenation in many scenarios, this module still delivers measurable performance gains when serving many small static files, when compression efficiency matters, or when clients negotiate HTTP/1.1.

Install it from the GetPageSpeed RPM repository or the APT repository, enable it with a single directive, and start reducing unnecessary request overhead.

The source code is available on GitHub.

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.