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:
- Validates the request: Checks that
concatis enabled, the URI ends with/, and the query string starts with?? - Parses file names: Splits the comma-separated file list from the query string
- Checks file count: Rejects the request with HTTP 400 if the file count exceeds
concat_max_files - Validates MIME types: If
concat_uniqueis enabled, verifies all files share the same Content-Type - Opens each file: Uses NGINX’s open file cache for efficient disk access
- Builds the response: Chains file buffers together (with optional delimiters) and sends them as a single response
- 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_moduledirective 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:
- Verify the module is loaded. Add a directive like
concat on;to your config and runnginx -t. If NGINX reports “unknown directive”, the module is not loaded. -
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_fileslimit - Empty file name: A request like
??file1.css,,file2.csswith consecutive commas
HTTP 404 Not Found
File paths are resolved relative to the location’s root or alias. Verify that:
- The
rootdirective 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.
