LiteSpeed Cache plugin protocol support for nginx. WordPress, Drupal, Magento and any other CMS that ships with an LSCache plugin get full-page caching on nginx without LiteSpeed Web Server.
Native C nginx module. The LiteSpeed Cache plugin (e.g. the WordPress one with 5M+ installs) emits X-LiteSpeed-Cache-Control, X-LiteSpeed-Tag, X-LiteSpeed-Vary and friends on every PHP-FPM response; this module observes them, stores cacheable responses on disk, serves subsequent matching requests via sendfile, and honors out-of-band and inline purges.
Designed for shared hosting providers migrating from Apache+LSCache or LiteSpeed Web Server to nginx, and for any nginx deployment that wants the LSCache plugin family to "just work".
- LiteSpeed Cache plugin protocol on nginx
- Full-page caching for WordPress, Drupal, Magento, OpenCart, PrestaShop, Joomla
X-LiteSpeed-Cache-Controlparsing (public, private, max-age, no-cache, no-store, s-maxage, no-vary)X-LiteSpeed-Varywith cookie and value axesX-LiteSpeed-Tagand tag-based purge- Out-of-band
PURGEmethod with IPv4 / IPv6 CIDR allowlist - Inline
X-LiteSpeed-Purgefrom backend responses LSC-Cookieside channel (stored in cache, replayed asSet-Cookie)- Private (per-client) cache slots with
_lscache_vary/wp_session/PHPSESSIDpriority - Session-leak guard (
Set-CookiewithoutLSC-Cookieis never stored as public) If-Modified-Since/If-None-Match-> 304 from cache- Range request serving from cached entries
- Size-based eviction (
lscache_max_size) lscache_min_uses,lscache_bypass,lscache_no_cache,lscache_strip_cookies- Native C nginx module, no Lua, no OpenResty
- Linux only
Platform: Linux only. The module uses POSIX
rename(2)/open(O_EXCL)for atomic cache writes, SHA-1 from the nginx core, andinet_pton/ IPv6 helpers from libc.
Nginx has no native equivalent to LiteSpeed Web Server's cache layer. Shared-hosting providers wanting to move off LiteSpeed or Apache hit a wall: their customers' WordPress installs with the LiteSpeed Cache plugin (millions of sites) silently stop benefiting from full-page caching once the front-end becomes nginx, because nginx ignores the plugin's X-LiteSpeed-* headers.
This module fills that gap at the lowest possible overhead: native C, compiled directly into nginx, with sendfile-based serving and atomic-rename-based writes.
Combined with the sibling ngx_http_htaccess_module, nginx becomes a drop-in replacement for Apache+LSCache in shared-hosting deployments.
| Header | Status | Notes |
|---|---|---|
X-LiteSpeed-Cache-Control |
Full | public, private, max-age=N, s-maxage=N, no-cache, no-store, no-vary, esi (ignored) |
X-LiteSpeed-Tag |
Full | Comma list. public: prefix accepted and stripped. |
X-LiteSpeed-Vary |
Full | cookie=NAME, value=NAME (resolved against X-LSCache-Vary-NAME request header) |
X-LiteSpeed-Purge (response form) |
Full | Triggers an inline purge as a side effect of the request. |
LSC-Cookie |
Full | Stored in .meta, replayed as Set-Cookie on every served response. |
Set-Cookie |
Guarded | Public response with Set-Cookie is refused (no-cache) unless LSC-Cookie is also present. |
Vary (RFC 7234) |
Guarded | Accepted only when the value is empty or just Accept-Encoding. Anything else refuses caching. |
| Header | When |
|---|---|
X-LiteSpeed-Cache: hit,public |
Served from cache, public entry |
X-LiteSpeed-Cache: hit,private |
Served from cache, private (per-client) entry |
X-LiteSpeed-Cache: miss |
Module is active for this scope; response was generated by upstream |
X-LiteSpeed-Cache: no-cache |
Module wanted to cache but refused (Set-Cookie, Vary, etc.) |
X-LiteSpeed-Purge-OK: <N> |
Response to a successful PURGE request |
X-LSCache-Debug-Key, X-LSCache-Debug-Vary |
Only when lscache_debug on (do not enable in production) |
All X-LiteSpeed-* and LSC-Cookie upstream headers are stripped from the client-visible response so that internal protocol state never leaks.
Send the PURGE HTTP method from an IP allowed by lscache_purge_allowed. Body of the X-LiteSpeed-Purge request header:
| Header | Effect |
|---|---|
* |
Purge all public entries for this Host |
private, * |
Purge all private entries for this Host |
url=/foo |
Purge all variants of /foo on this Host (public) |
private, url=/foo |
Same, private side |
tag=t1, tag=t2 |
Purge entries carrying any of these tags (public) |
private, tag=home |
Same, private side |
pub-block ; private-block |
; separates blocks (each with its own public,/private, prefix) |
stale, ... marks matching entries as needs-refresh (lazy invalidation) instead of unlinking them. The data file stays on disk; the next matching request treats it as a cache miss and the captured upstream response overwrites it. This is non-destructive - if the purge command itself is wrong, the entries will be rebuilt cleanly on first traffic instead of disappearing instantly.
- HIT path:
open()the cache file, read a 24-byte header, send the body viasendfile()straight from disk. No user-space body buffers, no PHP-FPM round-trip. - WRITE path: capture upstream response into a tmp file, atomic
rename(2)on completion. Concurrent writers cannot corrupt each other. - Size-based eviction runs in-band every ~100 writes per worker. No background thread, no shared memory, no extra processes.
- Entries are sharded by the first 4 hex chars of the SHA-1 key (two 2-char levels = up to 65 536 leaf dirs), so
readdirstays fast at any cache size.
| Directive | Context | Default | Description |
|---|---|---|---|
lscache |
http, server, location | off |
Enable/disable for this scope |
lscache_path <dir> |
http | (required when on) | Cache root. Must exist and be writable by the worker user. |
lscache_max_size <size> |
http | 1g |
Soft cap on cache dir size. Eviction runs every ~100 writes. |
lscache_methods <method>... |
http, server, location | GET HEAD |
Methods eligible for caching. POST is opt-in. |
lscache_min_uses <n> |
http, server, location | 1 |
Cache only after the Nth request on the same base key (per worker). |
lscache_min_age <seconds> |
http, server, location | 1 |
Reject max-age lower than this. |
lscache_max_age <seconds> |
http, server, location | 86400 |
Cap max-age to this. |
lscache_purge_allowed <CIDR>... |
http | 127.0.0.1 ::1 |
IPs/CIDRs allowed to send PURGE. Supports IPv4, IPv6, CIDR, all. |
lscache_bypass <var> |
http, server, location | none | If $var is non-empty/non-zero, skip cache lookup (proxy_cache_bypass parity). |
lscache_no_cache <var> |
http, server, location | none | If $var is non-empty/non-zero, do not store the response. |
lscache_strip_cookies <name>... |
http, server, location | none | Cookie names to drop from the cache key (e.g. analytics cookies). |
lscache_debug |
http, server, location | off |
Emit X-LSCache-Debug-Key and X-LSCache-Debug-Vary response headers. Never enable in production. |
| Variable | Value |
|---|---|
$lscache_status |
HIT / MISS / BYPASS / PURGE / - |
$lscache_key |
40-char hex SHA-1 cache key, or empty if no key was computed |
load_module modules/ngx_http_lscache_module.so;
http {
lscache_path /var/cache/nginx/lscache;
lscache_max_size 1g;
lscache_purge_allowed 127.0.0.1 ::1;
server {
listen 80;
server_name example.com;
root /var/www/example.com;
location ~ \.php$ {
lscache on;
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
}
}
}The WordPress LiteSpeed Cache plugin (installed at the WP level) then takes over: it emits the protocol headers in its PHP responses, this module observes them, caches accordingly, and the plugin's "Purge All" buttons work automatically because the plugin sends PURGE requests to the same host.
The module auto-advertises itself to the upstream: when lscache on fires for a location, an X-LSCache: on header is injected into the request so fastcgi_pass / proxy_pass forwards it to PHP as HTTP_X_LSCACHE. The LiteSpeed Cache plugin reads that and recognises this nginx as an LSCache-compatible front-end - without it the plugin would silently disable its cache output. No extra fastcgi_param is needed.
load_module modules/ngx_http_lscache_module.so;
http {
lscache_path /var/cache/nginx/lscache;
lscache_max_size 4g;
lscache_purge_allowed 127.0.0.1 ::1 10.0.0.0/8;
server {
listen 80;
server_name example.com;
root /var/www/example.com;
location / {
lscache on;
lscache_methods GET HEAD;
lscache_min_uses 1;
lscache_min_age 1;
lscache_max_age 3600;
lscache_strip_cookies _ga _gid _fbp wp_woocommerce_session;
lscache_bypass $cookie_nocache;
lscache_no_cache $arg_nocache;
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
lscache on;
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
}
}
}PURGEis gated bylscache_purge_allowed. Default is127.0.0.1 ::1only - without this an attacker could wipe any hosting customer's cache by sending a single HTTP request.- A response that carries
Set-Cookieand is declaredpublicis refused (X-LiteSpeed-Cache: no-cache) unless anLSC-Cookieis also present. This is the canonical session-leak guard: serving user A'sSet-Cookieto user B is a session hijack vector. - An upstream
LSC-Cookievalue containing CR, LF or NUL is treated as a header-injection attempt: the response is refused for caching (X-LiteSpeed-Cache: no-cache) rather than smuggled into aSet-Cookieline on subsequent hits. - All
X-LiteSpeed-*andLSC-Cookieupstream headers are stripped from the client-visible response. Internal cache state never leaks. - Client-supplied
X-LSCacherequest headers are stripped before the module advertises itself to the upstream, so a remote client cannot impersonate an LSCache-compatible front-end to the LiteSpeed Cache plugin. - Tag names, host names and
url=purge values pass throughlsc_safe_name: reject..,\0, any byte < 0x20 or 0x7F,/,\\, leading., length > 1024. These are the only externally-influenced bytes that flow into a filesystem path component. - Inline purges driven by an upstream response (
X-LiteSpeed-Purgefrom a backend) are scoped to the current request'sHost. A compromised tenant on a shared cache root cannot wipe other tenants' tagged entries. - Cache writes go to
<lscache_path>/tmp/<pid>-<seq>-<random>opened withO_CREAT|O_EXCL|O_CLOEXECand finalize viarename(2). Two workers competing for the same key cannot corrupt each other's tmp file or the final entry. - Cache files are created mode
0640and directories0750, so other system users on a shared host cannot read in-flightLSC-Cookiematerial out of<lscache_path>. - Response bodies past
UINT32_MAXbytes (4 GiB) are refused for caching - the on-disk header records a 32-bit body length, so silent truncation combined with byte-range serving is impossible. RFC 7234 Varyresponses with anything beyondAccept-Encodingare refused (cannot be safely shared across clients).X-LiteSpeed-Purgefrom an upstream response is hard-capped at 64 KB and 64;-separated blocks per request to bound worst-case directory walks under a misbehaving backend.
git clone https://github.com/X-Vlad/nginx-lscache-module.git
cd nginx-lscache-module
sudo ./install.shThe installer will:
- Detect your nginx version (or offer to install nginx from the official repo)
- Download matching nginx source
- Build the dynamic module
- Install
.soto the modules directory - Add
load_moduletonginx.conf
./install.sh --check # check installation status
sudo ./install.sh --uninstall # remove module# Download nginx source matching your installed version
nginx -v # note the version
wget https://nginx.org/download/nginx-X.Y.Z.tar.gz
tar xzf nginx-X.Y.Z.tar.gz
cd nginx-X.Y.Z
./configure --with-compat --add-dynamic-module=/path/to/nginx-lscache-module
make modules
sudo cp objs/ngx_http_lscache_module.so /usr/lib/nginx/modules/make build NGINX_VERSION=1.30.1 # downloads source, builds .so
sudo make install NGINX_VERSION=1.30.1 # full nginx + module install
make docker-test # build the docker image, run all tests./configure --add-module=/path/to/nginx-lscache-module
make && sudo make install- Linux (POSIX
rename(2),mkdir,opendir) - PCRE (libpcre or libpcre2) - inherited from nginx
- Worker user must own
lscache_pathand itsdata,tags,hosts,vary,tmpsubdirectories
On Debian/Ubuntu:
apt-get install build-essential libpcre3-dev libpcre2-dev zlib1g-dev libssl-devOn RHEL/CentOS/AlmaLinux:
yum install gcc make pcre-devel pcre2-devel zlib-devel openssl-develThe whole t/*.t.sh suite is exercised against each release on every build:
| Version | Status |
|---|---|
| 1.24.0 | green |
| 1.28.2 | green |
| 1.30.1 (default in Dockerfile) | green |
docker build -t nginx-lscache-test .
docker run --rm nginx-lscache-test # all t/*.t.sh
docker run --rm nginx-lscache-test bash /tests/t/stress.sh # stress harness
make test-versions # whole nginx matrixt/*.t.sh covers every feature listed above. t/stress.sh drives the module with ab under concurrent PURGE storms and fails the run on any non-zero error rate, worker RSS growth above 8 MB, [alert]/[crit]/[emerg] in the error log, broken liveness probe, or a corrupted cache file.
demo/ boots nginx + PHP-FPM + MySQL + the real WordPress LiteSpeed Cache plugin via Docker Compose; demo/smoke-test.sh asserts miss then hit,public against a sample post end-to-end. See demo/README.md.
BSD-2-Clause