Release Notes
mod_pagespeed 2.1 release notes — every release of the module and the optimizer worker, with security updates and what has changed since the last upstream release.
On this page
This page lists every release of mod_pagespeed 2.1, newest first, on one page. The product has two parts. The module runs inside Apache or nginx and applies the classic rewriting filters in the request itself. The optimizer worker is a separate process, introduced by the 2.0 re-architecture, that does the heavy optimization work off the request path. With the Apache module, the two map the same Cyclone cache. The reverse-proxy deployment pairs nginx with the worker over that same cache. The native nginx module runs on its own; to use the optimizer worker with nginx, run the reverse-proxy deployment. Both parts are released together.
Version numbers follow the two parts. The native packages of both parts are versioned 1.16.0; the product as a whole is 2.1.0, and that is the version the container images, the Helm chart and the NuGet packages carry. Earlier releases carry the numbers they shipped under. The module line started at 1.1.0 with package revisions r1–r24, was renumbered to 1.15.0 on 2026-06-01 so that version comparisons against the open-source 1.14.x line sort correctly in apt, dnf and control panels, and continued as 1.15.0+r2 through 1.15.0+r22. The optimizer worker line ran from 2.0.0 to 2.0.42 in plain SemVer. Some 2.0.x numbers are missing from this list: several were published only as the ASP.NET Core middleware package, which keeps its own release history.
We recommend running the latest release: it always carries the most recent performance and security work. Where a release fixes a security problem, its entry leads with that, and Security updates lists the security fixes we have published, with the release that carried each one. Those entries give the impact class, name the released versions affected and recommend updating; exploit specifics are kept out of public notes by policy, and CVE identifiers are given where a published advisory applies to a component we ship. Many of the bug and security fixes below repair defects inherited from the open-source line — code that shipped in 1.14.36.1 or the historical ngx_pagespeed and was never fixed upstream. Entries note this where it is the case, so it is clear what was repaired relative to the last open-source release.
Every release on this page comes after the last upstream release; what changed between that release and this line is set out in Since the last upstream release below.
The current release
mod_pagespeed 2.1.0 2026-09-17 module + optimizer worker
mod_pagespeed 2.1.0 is the module and the optimizer worker, released together and installed as a matching pair — the first general-availability release of the converged line, in which the two are parts of one product. In the signed apt and yum repositories both parts carry the package version 1.16.0; 2.1.0 is the version stamped on the container images, the Helm chart and the NuGet package.
Changed (plan for this before you upgrade): start the optimizer worker before you restart the web server. The worker owns the shared cache volume, and an Apache restarted after the worker package is upgraded but before the worker itself has restarted and created the volume in the new on-disk format refuses to start and says so, rather than quietly running a cache of its own; starting the worker and then restarting Apache clears it. At boot there is nothing to do — the worker service is ordered ahead of Apache and nginx and counts as started only once its notification socket exists, so a web server that uses the worker comes up and finds it ready instead of retrying. The ordering never makes one service require the other, so each still starts without the other. nginx ordering is new in 2.1: a host still carrying a 2.0-era worker package orders Apache alone. A web server running under some other unit name is not ordered against the worker and needs an ordering drop-in of its own.
Changed: restart the web server after upgrading. The module packages add the web server’s user to the worker’s group in their post-install step, and group membership only takes effect in a fresh web-server process.
Changed: the cache now refuses a single object larger than 64 MiB
(67,108,864 bytes). The cache library enforces a per-object ceiling it
previously declared but never applied, and this release has no directive to
raise or disable it. Most deployments cannot reach it:
ModPagespeedMaxCacheableContentLength defaults to 16777216 (16 MiB) and
excludes larger responses well before the new ceiling, so you are exposed
only if you raised it above 67108864 or set it to -1. An affected response
is never cached, so it is fetched from the origin on every request —
permanently, not as part of a post-upgrade warm-up — while everything else
caches normally; that asymmetry is how it is told apart from a full cache,
which fails every write. Each refusal counts in cyclone_cache_failures,
and the sampled log warning now names the size limit instead of reporting
that the cache may be full. If you were relying on caching larger objects,
set ModPagespeedMaxCacheableContentLength to 67108864 or less so those
responses are excluded before they reach the cache.
Changed: on Red Hat family hosts running SELinux in Enforcing
mode, set httpd_can_network_connect. The stock policy denies the web
server outbound network connections — including to the server itself over
loopback — and the module fetches subresources over HTTP, so on RHEL,
AlmaLinux and Rocky the fetcher is blocked and resources are served
unoptimized. Set the boolean persistently with
sudo setsebool -P httpd_can_network_connect 1; it is the same setting
other Apache modules that fetch over HTTP require. The restriction is not
new in this release — it applies to earlier versions on the same hosts — and
it does not affect Debian and Ubuntu, which confine with AppArmor rather
than SELinux.
Security: the bundled apr-util is updated to 1.6.5, picking up the upstream fixes published 2026-08-06 for CVE-2026-32327 (CVSS 9.1), CVE-2026-34191 (CVSS 9.1), CVE-2025-49506 (CVSS 7.5), CVE-2026-34501 (CVSS 7.5) and CVE-2026-34502 (CVSS 7.5); the affected upstream versions are apr-util 1.6.3 and earlier. The affected apr-util components are not built into or called by any mod_pagespeed release, so no earlier release is believed to be exposed; the bundle is updated so that it carries no known-vulnerable version. Update recommended.
Added: the Apache module packages install the configuration that
points the module at the optimizer worker. The Apache deb and rpm ship
pagespeed_daemon.conf — under /etc/apache2/conf-available/ on Debian and
Ubuntu, enabled by the package and disabled again when it is removed, and
under /etc/httpd/conf.d/ on the Red Hat family — which sets
ModPagespeedDaemonSocketPath and ModPagespeedDaemonVolumePath to the
worker package’s own defaults, so an upgrade from 1.15 reaches the worker
without editing any configuration. The package manager treats it as a
configuration file, so edits to it survive later upgrades, and commenting
out both directives returns the module to its own in-place path. A server
that already carries the two directives in a file of its own keeps working:
both files are read, and the packaged one, whose name deliberately sorts
later, is the effective value. A file created by hand earlier at the
packaged file’s own path is treated as a locally modified configuration
file: on Debian and Ubuntu dpkg asks what to do with it, which stops an
unattended upgrade at that question, so remove such a copy before upgrading
(or install with --force-confold to keep it); on the Red Hat family the
packaged copy is written beside it as pagespeed_daemon.conf.rpmnew. If the
worker is not running when the web server starts, in-place optimization is
off for that web-server process and one startup line names the cause. The
wiring is supported as a matched pair: the module checks the worker’s
published interface at startup, and against an older worker it leaves
in-place optimization off and says so in one line rather than running a
pairing it cannot trust.
Added: the Apache module serves in-place-optimized responses from
the optimizer worker’s shared cache, and records origin responses into it. A
request the cache can answer is answered with the variant the worker
produced for that request’s own capabilities: a variant in an image format
the request did not advertise is never served, and where no variant fits,
the origin’s own bytes come back from the cache — which saves a trip to the
origin but is not an optimized response and is not counted as one. Where
neither exists, or the entry has aged out, or the client asked for a reload,
the request is served by the ordinary path exactly as it would be on a
server with no worker configured. Responses carry an Age stating how old
the stored copy is, so a cache in front of the server does not add the full
lifetime again; a weak ETag that answers an If-None-Match; and
Vary: Accept where the bytes were chosen from the request’s Accept or
where the origin negotiates on Accept itself. A response carrying headers
this path cannot reproduce — a Set-Cookie, a CORS or security header, a
Vary on another axis — is served by the ordinary path instead:
unoptimized, but complete, rather than served later with those headers
missing. Two counters, ipro_daemon_served and ipro_daemon_fallthrough,
report how many in-place-eligible requests this path answered and how many
it passed through.
Added: the Enterprise Linux module rpm ships and installs the optimizer worker’s SELinux policy. On a stock enforcing host the distribution policy gives the web server no access to the worker’s cache volume and notification socket, so in-place optimization could never be turned on there; previously that confinement had to be built by hand. The rpm now carries a policy module and installs it in its post-install step: the worker runs confined in its own domain, its cache directory and runtime sockets get their own file contexts — registered in the policy store, so they survive a relabel — and the web-server domains get exactly the access the module needs: the cache volume, and the notification socket only, not the worker’s management sockets. Installation happens only where SELinux is enabled on the host — Permissive counts as enabled — and a failure there does not fail the package install — the module logs its degraded state at startup instead. Removing the rpm removes the policy again. The deb packages are unchanged: Debian and Ubuntu confine with AppArmor, not SELinux.
Added: the Linux module and optimizer packages now install a
THIRD-PARTY-NOTICES file alongside the package’s existing notice files,
listing every bundled third-party component with its version, its upstream
location and the terms it is distributed under — including the components
statically linked into the worker, so they travel with the binaries the way
the container images and the NuGet packages already carried them. The Apache
and nginx deb and rpm packages carry it, including the cPanel EasyApache 4
build, and the attribution notices themselves were corrected in the same pass
so that they cover every component actually present in the shipped binaries
and nothing that is not. The bundled Cyclone cache library carries the Apache
License 2.0 as well, matching the rest of the distribution.
Added: the admin console can show the optimizer worker alongside
the module. Three read-only panels — worker status (health, version, uptime,
checks, browser-analysis state), worker cache (entries, size, serve-savings
counters) and back-pressure (thread-pool occupancy, dropped notifications,
cache cooldowns) — read the worker’s management API over a unix socket named
by a new DaemonApiSocketPath directive, available on Apache and nginx and
disabled by setting it empty. The path behind those panels is
read-only by construction: GET and HEAD only, a fixed allow-list of upstream
paths, and no client query string, header or body reaches the worker. Where
the worker is unreachable or not configured the panels say so plainly, and
the module’s own pages are unaffected.
Added: the worker’s cache-console panel shows live serve statistics when the module serves in place. In the topology where the worker only writes optimized variants and the module answers requests from the shared cache, the worker never responds to a request itself, so its serve-savings counters — original against optimized bytes, and hit counts, per content type — stayed at zero forever. The module now records each optimized in-place serve through the worker’s published client interface, so those counters reflect real traffic and the panel has live data in this topology. Only serves of worker-produced optimized variants with a recorded origin size are counted, matching the gate the worker’s own front ends apply. The serve-class note further down says what all-zero counters mean under a front end that does not record them.
Improved: the admin console’s graphs page can plot per-interval change instead of cumulative totals. A “Show per-interval deltas” checkbox next to the refresh controls switches every graph from the raw cumulative counter to how much that counter moved during each sample interval, so the curves read as traffic rather than as ever-climbing totals. Restarting the server resets the statistics counters, and in this view such a reset is drawn as a gap rather than as a drop to zero or a negative spike. The cumulative view remains the default, and the choice is remembered in the browser.
Improved: the admin console’s statistics page now explains what the counters mean. Hovering a counter name shows a plain-language description of what it counts, when it moves and what a non-zero value suggests; an optional description column shows the same text inline, and the choice is remembered in the browser. The search box matches the descriptions as well as the names, so searching for “in-place” or “certificate” finds the relevant counters without knowing the naming scheme. Clicking “Value” now sorts biggest-first on the first click, so the counters that are actually moving come to the top instead of the hundreds that sit at zero; “Name” still sorts A to Z first.
Fixed: after content was re-optimized, replaced or purged, the in-memory cache tier could keep serving the copy it had replaced until that copy happened to be evicted. The stale in-memory entry is now dropped when the content behind it is re-recorded or removed, so a refresh takes effect immediately instead of after an unpredictable delay.
Fixed: two JavaScript minifier misreadings that could break a
script or leave it unminified. A let declaration whose binding is preceded
by an HTML-style comment marker lost a required line break, producing a
syntax error that breaks the entire script; and a script that spreads the
result of a function call inside an object literal — {...f(), key: value},
a common shape in bundler output — was not minified at all, and because a
script that fails to minify does not count as optimized, it could not be
moved onto the rewrite domain either. Both are fixed, in the module and in
the worker’s own copy of the minifier, so scripts served through in-place
optimization are minified exactly as the module minifies them. Spread in
every other position was never affected, those comment forms are rare in
modern JavaScript, and output for all other valid JavaScript is unchanged.
One deliberate change on invalid input: a spread element carrying a property
colon is refused rather than tolerated, which again leaves the script byte
for byte as received. Affects all earlier releases of both parts. Update
recommended if you serve JavaScript through the optimizer or the ASP.NET Core
middleware.
Fixed: an image in a format the client did not ask for is no
longer served from cache. Two separate cache paths could hand an AVIF
representation to a request that never advertised image/avif — a cached
response an origin had selected by content negotiation, and an optimized
AVIF resource reused from a warm cache for a later, differently capable
client — and the affected client received an image it cannot decode, a
broken image rather than a slower one. Because the representation is chosen
once and then cached, one capable client’s request could decide what later
clients got for the cached lifetime of that resource, including through a
shared cache or CDN. Separately, the check that keeps format-negotiated
images from crossing clients did not recognise every legal spelling of the
origin’s Vary header, so a stored WebP or AVIF response could reach a
client whose own request would not have selected it. Both cache paths now
check the request’s own Accept header before reusing an entry, and the
header check recognises the legal spellings. Nothing changes for clients
that do advertise the format, no response gains a Vary header it did not
already have, and .pagespeed. resource URLs were never affected, because
their format is committed in the URL. Update recommended for any deployment
that serves AVIF or that sits behind an origin that negotiates on
Accept.
Fixed: a converted image is no longer served with the origin’s
media type. When a request was answered from the worker’s cache with a
variant in a different image format from the one the origin sent, the
response carried the origin’s Content-Type — AVIF or WebP bytes labelled
image/png. The negotiation itself was right, since the format served was
one the request had advertised, but a client decodes by the declared media
type, so the response fails in the consumer: a shared cache or CDN stores
the mislabelling and repeats it to everyone, and any client that trusts the
declared type rather than sniffing the bytes cannot use the response. The
media type now describes the bytes actually being sent; a response whose
format did not change, and any format outside the four this path can
produce, keeps the origin’s own field exactly as before. Update recommended
for any deployment serving from the worker’s cache with image-format
conversion in play.
Fixed: a 304 Not Modified now states the same Vary as the
200 it revalidates, on the path that serves from the worker’s cache and on
the classic in-place path alike. The two responses could disagree about
which request headers the response varies on — the 200 listing
Accept-Encoding where the 304 listed nothing, or listed Accept alone —
and because a cache updates its stored headers from the 304, it could be
left believing the response varies on fewer request headers than it had
keyed on. Both emitters now state
the same set, for the media types the module’s own compressor predicate
selects. Nothing about a 200 changes. Update recommended for deployments
with a shared cache or CDN in front of a server using in-place
optimization.
Fixed: the module now attaches to the worker’s cache volume in cases where it previously gave up. A volume path carrying a file extension, or one whose name ends in a dot, made the module look for a filename the worker never writes, so it concluded the volume did not exist and logged the misleading advice to start the worker first while the worker was running and healthy — with in-place optimization silently off. The packaged default path was never affected. Where an attach genuinely fails, the log line now carries the reason the worker reports alongside the error class, and the attach is retried on a widening backoff for about six minutes instead of being tried once per worker process and left off for that process’s whole lifetime. The module also reports a permission problem distinctly from an absent artefact, naming the likely missing group membership and its one-command fix, and a mismatch between the cache layout the module is built for and the one the worker publishes is now a loud startup failure with in-place optimization off, never a quiet attach to a layout that shares nothing.
Fixed: an image optimized in place at its original URL no longer
falls back to the origin permanently once its cache entry ages out. An
optimized URL was served from cache only until its freshness lifetime
elapsed; after that, every request went back to the origin and was never
re-optimized, because the worker treated the URL as already processed. The
server now tells the worker when it declines an aged-out variant for genuine
expiry, so the worker discards the stale variants and rebuilds them from the
freshly fetched original. Client reloads and origin no-cache responses do
not trigger this, so a plain browser reload can never evict a URL’s
optimized variants. Only deployments running the optimizer worker were
affected; the classic in-place cache was not.
Fixed: the built-in help for AgentOptimize no longer describes a
gate that does not exist. The feature is gated by its operator flag alone,
which is what the rest of the feature already said, and the help text now
agrees.
Note for operators: the Debian 11 (bullseye) apt suite stays available and keeps serving the final 1.15 packages, so existing bullseye systems continue to work as they do today. The 2.1 serving components require glibc 2.34 or newer, which Debian 11 does not provide, so new releases are published for Debian 12 (bookworm) and newer, where the package names and configuration are unchanged.
Security and hardening — update recommended for every deployment:
- Hardened: the optimizer worker no longer runs as root on package installs. It runs as an unprivileged system user, the cache and socket permissions are owner-plus-group, and the API token is no longer passed on the process command line. This shrinks the impact of any future compromise of the worker.
- Hardened: new defaults in 2.1 — the management API fails closed without a credential, and headless Chrome runs sandboxed. Both are default flips with operator-visible remedies; the migration guide lists them.
- Hardened: the packaged worker enforces its system-call allow-list by default, terminating on the first call outside it instead of only logging. Browser analysis ships a companion allow-list and works under enforcement with no extra step.
Changed (plan for this before you upgrade): the disk cache starts empty. This release moves the optimizer to a new on-disk cache format. Every deployment begins with a cold cache that refills as traffic arrives, so expect a temporary drop in cache hit rate and a matching rise in origin fetches. Check that the cache directory has room for the old and new volumes to coexist during the upgrade — keeping the old file is what makes a rollback warm. A running worker holds the old cache file open, so deleting it does not return the space until the worker restarts. On package installs the worker keeps its cache in a versioned directory, and a package upgrade starts that cache cold by design. The migration guide has the full checklist.
Changed: mod_pagespeed 2.1 is licensed under the Apache License 2.0 — the same license the original codebase carried — and the relicensing is retroactive across the line. The source is published at github.com/We-Amp/mod_pagespeed. Every feature is available to everyone, and the standard signed packages are free.
The optimizer makes no outbound request of its own accord; the only egress is what you configure.
Added: the deb and rpm packages carry the hardened pagespeed-optimizer
worker — its service unit, permission model, and the client library the
serving module binds — so the module and the worker upgrade as a matching
pair.
The converged engine also carries cache-overwrite staleness fixes (a refreshed, replaced, or purged resource can no longer keep serving its superseded copy from memory), JavaScript and CSS minifier correctness fixes carried into both parts, image-quality verification hardening, and browser-analysis robustness under the unprivileged worker.
Action required only if you configured the module’s daemon paths by hand: the worker’s default cache and socket paths move with the unprivileged user. The module packages install a configuration file that points at the new defaults, so a package upgrade needs no edit; a hand-written configuration that names the old paths must be repointed, after which restart the web server. Until then in-place optimization stays off and the log reports the socket as absent even though the worker is running. The migration guide lists both paths.
Check this before you upgrade if you use the management API: an absent token now fails closed, so a deployment that relied on an unset token answering requests has to set one, and opening the read endpoints is an explicit choice rather than a consequence of leaving the token unset. A group-scoped unix socket is the recommended local transport — reaching the socket already means being in the worker’s group, so nothing further is asked for and no TCP port is bound. A TCP bind stays available and is loopback-only unless remote access and a token are both set. Package installs generate an API token and a purge token once, at install time, into a file readable only by root and the worker’s group, and never overwrite a value you set.
Check this before you upgrade if browser analysis is enabled: where the kernel does not permit an unprivileged sandbox, browser analysis refuses to initialize, names the cause in the health output, and the worker keeps serving without it. It never falls back to an unsandboxed browser, and there is deliberately no mode that does; the documented opt-out warns at startup and on every browser start. In containers the runtime’s default system-call profile has to permit user namespaces. The health and statistics endpoints report the resolved sandbox state, and whether a kernel system-call filter is attached, so a monitoring check can see an unsandboxed browser without reading logs or unit files.
What enforcement of the system-call allow-list means on a host upgrade: the package restarts the service on install, and the host enforces from that moment. Browser analysis needs no extra step, and a documented opt-out example ships with the package for hosts that want to soak first. The same unit closes off host surfaces the worker never needs — kernel tunables, modules and the kernel log, control groups, the system clock and the hostname, other users’ processes, SUID/SGID bits, realtime scheduling — restricts the socket families it may open, and disables core dumps. If you depend on one of those — a core dump for crash triage being the realistic case — restore it with a drop-in of your own.
Container and Helm deployments: the optimizer runs as an unprivileged user and the images share the cache through a fixed group instead of a world-writable volume, so a deployment that pins an explicit user, sets a Kubernetes group for the volume, or mounts the cache into a second container has to match that identity. A volume from an earlier release is adopted in place on first start and keeps its contents; where that cannot be done safely the container refuses to start, names the path and prints a one-line remedy rather than looping. The management API binds loopback and requires a token, and with the API enabled and no token supplied the entrypoint generates one per container start and prints it once to the container log — set your own to keep credentials out of shipped logs and stable across restarts. The Helm chart now sets a seccomp profile on the optimizer pod, which a workload needing a call the runtime’s default profile blocks can remove in its own values. The migration guide has the exact values and every opt-out.
Security (denial of service): the optimizer worker no longer terminates on an image it cannot decode; such input is now rejected cleanly. Deployments that optimize images reachable from untrusted sources are the exposed case. Update recommended.
Security (dependency advisory, GHSA-29g2-3rmr-qm68, medium severity): container and Helm deployments pick up the web console’s SvelteKit dependency update in this release; the ASP.NET Core packages carried it from 2.0.42. No optimization or serving code is affected and no configuration change is needed. Update recommended.
Changed: a cache purge, and the automatic recovery the serving module runs when it cannot open the cache, no longer delete a cache file written in an earlier release’s on-disk format — either could silently remove the volume an operator was keeping in order to roll back. Files from another format are left alone, which also means they stay on disk until you delete them yourself.
Changed: the same-URL markdown variant for AI agents, the
synthesized /llms.txt index and the browser-analysis suite are enabled by
their operator flags alone. Monitoring that keyed on the per-feature health
entry this release removes should key on the overall ready and status
fields instead.
Added: the statistics endpoint and its stream carry a serve-class block whenever the shared serve-statistics file is available — the five-class serve partition (optimized, or original bytes because the optimized copy was cold, still being produced, declined, or a version mismatch was detected) plus the drop counters that keep the partition honest — and the file records how busy the optimizer’s work queue is, as a running sum, a sample count and a high-water mark, so a monitoring tool can report average and peak backlog over an interval instead of guessing from a single poll. The serve classes are written by the serving front end: live where the module fronts the cache, and all-zero under an nginx 2.0 or ASP.NET front end, where all-zero means “not instrumented”, not “nothing happened”. Note for operators: this bumps the file’s version, which resets its counters once on upgrade, and a front end left on the older version stops recording until it is upgraded — the optimizer now logs a warning naming both versions instead of leaving flat counters as the only symptom.
Added: version skew between the optimizer and the components that notify it is reported instead of being absorbed. Refused notifications are counted by cause, and a version disagreement is logged with both versions named, rate-limited so a misconfigured sender cannot flood the log. A new health check covers the shared configuration file the worker writes and the serving module reads: if that file declares a schema version the running build cannot read, the build falls back to its compiled-in defaults, and the health endpoint now reports that as failing with both versions named instead of leaving an install running on settings nobody chose.
Changed: the management API serves the web console’s static bundle —
GET and HEAD under /console — without requiring the API token, matching
the documented behaviour; the bundle carries no operational data, and every
data endpoint and every non-GET request still requires the token when one is
configured. The health and statistics endpoints now report the connection
figures of the management API listener itself rather than the notification
listener’s separate limit.
Fixed: a URL whose optimized variants were re-recorded many times
could permanently stop accepting new ones. Every re-record left the earlier
copy linked behind the new one, so the stored chain grew on every refresh
until it hit its limit and further writes were refused. Reads kept succeeding,
so nothing looked wrong, but the browser was served an increasingly stale
version that was never replaced. The earlier copy is now unlinked as its
replacement is written; a URL that already reached the limit, or whose
variants can no longer be replaced or removed, has its cache entry cleared at
the next revalidation and is optimized again from the fresh origin response.
The same chain growth reached the content-hash record, the /llms.txt pair
and the agent markdown variant, and is fixed with it.
Fixed: a URL whose optimized variants were evicted while the optimizer still remembered processing them is no longer skipped forever, with the front end falling through to the unoptimized original and no path back. A duplicate-work hit is now validated against the cache, and where nothing servable remains the stale record is dropped and the URL re-optimized, bounded to at most once every ten seconds so a terminal decision cannot become a per-request re-optimization loop. An origin-content refresh can also no longer rebuild a URL’s optimized variants from stale bytes and republish them as freshly optimized; where the refreshed origin has not been recorded yet, the rebuild is deferred until the fresh content arrives.
Fixed: an origin refresh no longer purges and rebuilds a URL whose origin has not changed. Both front ends signal a refresh on every age-expired re-fetch and the optimizer purged the whole variant set on arrival, so every expiry cost a purge plus a deferred re-optimization of identical bytes — and where the module never serves stale, a recurring window with nothing optimized to serve at all. The optimizer now decides changed-versus-unchanged first, against the stored content hash, and restamps an unchanged variant set in place; a genuinely changed origin purges and rebuilds exactly as before. Text resources carry no content hash and keep the per-cycle purge. Update recommended — deployments without far-future origin cache headers benefit the most.
Fixed: a resource whose origin negotiates on Accept is now cached
and served faithfully, and is excluded from optimization. Such a response used
to be optimized from whichever representation the first visitor’s Accept
elicited, and every later visitor was served from that capture; on the way out
the origin’s own Vary was dropped from a cache-served response, so a shared
cache or CDN in front of the install could store it keyed without Accept and
hand one visitor’s representation to another. The response is now stored as
the origin sent it, never handed to the optimizer, and served with Accept in
Vary wherever the entry records that the origin negotiated on it — and only
there, so resources that do not vary are not multiplied downstream. Expect a
one-time revalidation for affected resources: entries recorded as
Accept-negotiated carry a different entity tag, so each visitor costs one full
response, once, after which conditional requests resume as normal.
Fixed, with wider reach: an expired resource is re-cached after it
is refreshed. The serving stage attached its own Vary to a response before
it was known whether the response would come from cache or from the origin; on
an expired resource it came back attached to the origin’s reply, where the
cache read it as the origin’s own and judged the refreshed image uncacheable.
Such an image was fetched from the origin again on every request and never
re-optimized. This reached images whose origin does not support conditional
requests, or whose content had changed; images from an origin that answers
304 Not Modified were never affected.
Fixed: a request whose Accept header advertised no image format at
all could be answered with a WebP or AVIF variant it never said it could
decode, and which of the two it received depended on cache listing order. A
stored WebP or AVIF variant is now served only to a request whose negotiated
formats name it, with the original format remaining the universal fallback;
where no variant is compatible the request falls through to re-optimization,
the same as a cache miss, and ties no longer resolve by listing order. This is
strict in one direction: a client that sent image/* or */* and could in
fact decode a stored AVIF may now miss the cache where it previously hit,
because acceptance cannot be proven from that header. A request that sends no
Accept header at all is now classified as */*, per RFC 9110 — it was the
only shape that resolved differently from the wildcard it stands for.
Fixed: lossy WebP images are optimized rather than passed through. A
WebP origin encoded above the quality this pipeline targets used to be served
as it arrived; it now re-encodes, with the result kept only when it is
strictly smaller than the origin, the same rule JPEG and PNG already get.
Lossless WebP origins keep the earlier behaviour, because every WebP encoder
here is lossy and a size gate cannot see a fidelity change. One gap remains: a
client whose Accept names AVIF finds no AVIF variant for a WebP origin and
is served the origin’s own bytes rather than the smaller WebP sibling it also
accepts. Separately, an image whose pixel decode fails — some GIFs, for
example — is no longer stored only under a format-specific variant it was
never converted to, so it is served from cache instead of falling through to
the origin’s copy for every client that does not advertise that
format.
Fixed: a stored image variant is bound to its quality-verification verdict. A WebP or AVIF candidate measured below the configured acceptance floor is declined instead of stored, and serving falls back to the remaining variants and the original; a candidate whose quality cannot be measured at all — which previously fired for every monochrome image — is declined on every format arm rather than shipped unverified. Re-encoding a lossy WebP in its own format is bound to the same verdict, and so is the re-encode of the original-format slot on a resized viewport. A failure inside the measurement itself is reported on its own channel, so a decline no longer cites a score that was never produced, and refused variants are remembered per source image, so repeated notifications no longer re-run the whole attempt ladder. Update recommended where quality verification is enabled.
Fixed: AVIF candidates were shipped without the verify-and-re-encode
quality check that guards the other formats, so a badly degraded encode could
ship instead of being caught and re-encoded; they are now decoded and verified
like the other formats. Enabling that decode path also makes origin-served
AVIF decodable and transcodable for the first time; decode is capped at 50 MB
of pixels and runs in the isolated worker process, not in the web server.
Re-optimizing a JPEG could also encode it at a higher quality than the
original was saved at, producing a larger file with no visible improvement; a
JPEG re-encode is now capped at the quality detected in the source.
--no-quality-cap turns the cap off and now takes effect;
--quality-cap-margin is still accepted but no longer raises the cap.
Fixed: a stylesheet with an invalid value in a complete longhand
family no longer loses the whole family through minification. A browser drops
one declaration for an invalid longhand but everything the shorthand would
have set for an invalid shorthand, so collapsing the four padding longhands
around a typo turned one ignored declaration into the loss of all of them.
Each member value is now checked against what that longhand accepts, so the
typo costs exactly what it costs a browser. This is not free on well-formed
CSS: a family the check cannot positively recognise is left as longhands,
which costs a few bytes — math and environment functions, url() values, the
modern space-separated and slash-alpha colour forms, colour functions beyond
rgb(), rgba(), hsl() and hsla(), and numbers in scientific notation no
longer collapse.
Fixed: with browser analysis enabled and a persistent profile
directory, the headless browser could not start after the optimizer’s
container was recreated or a host’s name changed — the profile kept a lock
from the earlier browser, and every launch failed until it was removed by
hand. A lock belonging to another host, or to a process that is no longer
running, is now removed before each launch and its owner logged, a locked
profile is named in the launch-failure log line, and the failure count and
retry delay reported under browser in the health output clear once a launch
has stayed up for a minute. A profile directory must not be shared between
optimizer instances. Separately, the sandbox probe that decides how to run
the headless browser is run once per process, and a probe that never replied
was banked as a timeout verdict for the life of that process; the probe is
now retried, so a slow first answer does not settle the question wrongly.
Update recommended if browser analysis is enabled.
Fixed (ASP.NET Core): the host now provisions its own cache
directory, so the worker it starts comes up on a machine where the optimizer
was never installed from a package. The worker refuses to start when its cache
directory is absent — right for the packaged service, whose directory is
created ahead of it, but the NuGet package has no such install step, and
whichever of the host and the request-side cache ran first decided whether the
worker survived startup. The directory is now created before the worker
starts, owner-only, and an existing one is left exactly as it is; no
configuration change is required. Read-back also honours the true
origin-revalidation signal and the write path persists the origin’s
Cache-Control state, so entries carry the correct revalidation requirement
and aggressive-mode stale-if-error is suppressed when the origin requires
it; for a private cache only no-cache and must-revalidate trigger
revalidation, per RFC 9111.
Note for embedders: the C API gains a documented stability contract,
with a published structure’s layout pinned at compile time. Size-aware
initializers are added for the cache, HTML and critical-CSS configuration
structures, and the three original initializers now write only their earlier
prefix, so a caller compiled against older headers must use the sized forms;
four entry points that were documented and built but never exported are
exported now. New this release and API-only, with nothing in the engine
writing or serving them yet: a storage class for the bytes the origin sent, a
storage class for a response’s request-independent headers, and accessors that
record and read back an origin’s caching state and answer the freshness,
Cache-Control and Vary questions directly. Every addition is additive, so
existing callers and binaries are unaffected. Two things to check: a private,
origin-scoped integration must set the cache scope explicitly, because the
default is shared; and anyone who pinned max_metadata_size should raise it,
since the default rises to 878 bytes and a ceiling below the largest blob the
format can produce refuses the write whole, which in production looks like a
cache that never warms.
All releases
The releases below are grouped into four periods, newest first inside each, and every entry says which part it belongs to: the module or the optimizer worker. The current release is written out in full above; everything here is collapsed, so open a release to read it in place, or use the index below to jump straight to a version. Where the module shipped several package revisions within a few days they are grouped into one entry, listed under the lowest revision in the group.
The converged line — September 2026
mod_pagespeed 2.1.0 is the module and the optimizer worker released together — both carry package version 1.16.0 in the apt and yum repositories, and 2.1.0 on the container images, the Helm chart and the NuGet package. See the current release above for the full entry.
Two parts, shipping in parallel — late May to August 2026
2.0.42 2026-08-10 optimizer worker Security: web console dependency update (GHSA-29g2-3rmr-qm68). Update recommended.
Security: the web console’s SvelteKit dependency is updated to 2.70.2 (from 2.69.1), fixing a medium-severity vulnerability, GHSA-29g2-3rmr-qm68. Impact: dependency update. Affected: ASP.NET Core packages before 2.0.42; container and Helm deployments before 2.1.0. No optimization engine code is affected and no configuration change is needed. Update recommended.
This release shipped as the ASP.NET Core middleware packages on NuGet, which embed the web console.
2.0.41 2026-08-08 optimizer worker Security: base-image update, plus above-the-fold CSS accuracy and stylesheet-deferral correctness fixes.
Security: the published worker and nginx images are rebuilt on an updated base image, picking up the current distribution security updates. This clears several fixable medium-severity vulnerabilities in bundled system libraries. No ModPageSpeed code is affected and no configuration change is needed. Update recommended.
Improved: a deferred stylesheet is now requested at normal priority using a standard preload, instead of a low-priority technique that could leave the browser waiting longer before the full stylesheet applied. The deferred stylesheet is also announced in early hints again, so it starts downloading sooner. Pages without JavaScript are unaffected — the no-script fallback is unchanged. The ASP.NET Core middleware emits the same primitive.
Note for operators: the async-CSS helper script is served at a content-addressed path that changes with this release. Front-end and worker components must be upgraded together, as with every release. The deferral changes below are not retroactive either: HTML pages already in the cache were written before deferral required measured evidence and can keep serving that way until they revalidate, so purge or reset the cache after upgrading to have every cached page pick up the new behavior immediately.
Improved: the optimizer now measures which elements are actually above the fold in a real browser render, per viewport, instead of estimating from the first elements in the document. Pages with substantial markup in the document head — which previously exhausted the estimate before reaching visible content — get more accurate above-the-fold CSS, so stylesheet deferral applies to more pages.
Fixed: deferring a page’s stylesheet could produce a flash of unstyled content when the inlined above-the-fold CSS did not in fact cover the fold. Deferral now applies only to a page whose above-the-fold appearance has been confirmed unchanged, and that confirmation is tied to the exact stylesheet it was made against — so publishing new styles re-checks before deferring again. Everywhere it does not apply, the stylesheet stays render-blocking and the above-the-fold CSS is still inlined, so those pages keep the inlining benefit and lose only the deferral. A page whose analysis has not completed keeps its stylesheet render-blocking. A confirmation only ever authorizes the above-the-fold CSS it was actually made about. If none can be measured for the specific page being served, that page’s stylesheet stays render-blocking whatever its size — a small stylesheet gets no shortcut past the check — and whatever above-the-fold CSS was produced is still inlined.
That confirmation now happens. While a page is analyzed, it is rendered twice at each viewport — once with its whole stylesheet, once with only the above-the-fold CSS that would be inlined — and the two are compared. Pages whose above-the-fold appearance is unchanged get their stylesheet deferred again; pages where it is not keep the stylesheet render-blocking and still get the above-the-fold CSS inlined. Deferral therefore applies to pages that have been analyzed, and only while they still serve the stylesheet the check was made against.
Three limitations are worth knowing, because they are permanent. The comparison render runs without JavaScript, and without loading images, web fonts or imported stylesheets, so a fold whose appearance depends on any of those is not something the check can see. And the check covers a page template rather than each individual page: pages sharing a template share one confirmation, so a sibling page whose fold needs something the checked page did not can still be deferred. Verify such pages yourself, or turn deferral off for the site.
Fixed: a page could have its stylesheet deferred while a content-security policy declared on that same page suppressed the inlined above-the-fold CSS, leaving nothing to paint with until the full stylesheet arrived. Whenever that inlining is refused, the deferral is now withdrawn with it.
Changed: on the embedding library’s low-level HTML transform entry point,
ps_html_transform_create, the async-CSS configuration flag is now ignored.
That entry point has no view of the page’s stylesheets and no browser, so it
cannot establish that deferring them is safe, and unsafe deferral is a flash of
unstyled content. It always leaves stylesheets render-blocking, and still
inlines the above-the-fold CSS it is given. The flag is off by default and the
ASP.NET Core middleware does not use this entry point, so this reaches only an
embedder that called it and set the flag explicitly; for stylesheet deferral,
use the main HTML processing entry point, ps_html_process, which gathers the
page’s stylesheets and gates on them.
Fixed: custom-property registrations and related at-rules were omitted from the inlined above-the-fold CSS. On stylesheets that rely on them — modern utility frameworks in particular — properties depending on those registrations computed incorrectly while the full stylesheet was still loading, so borders and similar details rendered wrong for a moment. These declarations are now always retained.
Improved: above-the-fold CSS detection now correctly retains rules the browser was observed to use at first paint even when those rules sit inside a cascade layer or a responsive breakpoint, so less of the fold is left unstyled while the full stylesheet loads.
Fixed: the JavaScript minifier corrupted valid code when a comment sat between
let and the name it declares, matching the same fix in mod_pagespeed 1.15.
The minifier did not look past the comment when deciding whether let starts
a declaration, and removed a line break that JavaScript’s automatic-semicolon
rules needed: let /*c*/ row followed by a line starting +4 was served as
the unparseable let row+4, breaking the script. The declaration scan now
skips comments and the line break is preserved.
Fixed: the CSS minifier corrupted custom-property values when the property
name contained an escaped character. Custom properties store raw token
streams — read back verbatim by var() and getPropertyValue() — so the
minifier leaves their values untouched; but the check that recognizes a
custom property broke on names with escapes, such as the escaped space in
--\ \>. Those names are valid per CSS Syntax 3 (an escaped char is a name
char), so the value was minified as ordinary CSS and could lose digits:
a{--\ \>:0.} was served as a{--\ \>:.}. The name scan is now
escape-aware (a character preceded by an odd-length backslash run is name
content, not a boundary), and such properties keep their values byte-for-byte.
Fixed: the CSS minifier’s decimal optimization still rewrote identifiers that
use hexadecimal escapes. 2.0.40 stopped the 0.5 → .5 shortening from firing
inside plain identifiers, but a CSS identifier is built from decoded escapes
while the guard read raw bytes: the class selector .a\35 0.5 — the class named
a50 — was served as .a5, silently restyling pages that use escaped
identifiers. Escape structure is now decoded before the guard decides, including
a hex escape’s consumed whitespace terminator and escape content in front of a
-, so such selectors survive minification unchanged while genuine numbers
still minify.
1.15.0+r22 2026-08-08 module In-place optimization now serves one cacheable variant per image, dropping per-client Vary headers.
Highlights
-
In-place optimization now serves one variant to every client, and its responses no longer carry a
Varyheader. When PageSpeed optimizes an image at its original URL (rather than at a rewritten.pagespeed.URL), it now optimizes that image identically for every client and serves the same bytes to all of them. It no longer picks WebP, AVIF, a mobile quality or a Save-Data quality from the request, so its responses carry noVary: Acceptand noVary: User-Agent, and are no longer downgraded toCache-Control: privatefor Internet Explorer. They are cacheable by browsers, proxies and CDNs with no special configuration, and one cache entry now serves every client instead of one per browser capability. Conversion to a universally supported format still happens in place — a photographic PNG still becomes a JPEG whenconvert_png_to_jpegis enabled — but never based on who is asking. WebP and AVIF are still selected on rewritten URLs, where the chosen format is part of the URL, so noVaryheader is required. Image inlining into CSS is likewise suppressed on the in-place path: whether a browser supportsdata:URIs is a per-request property, so CSS optimized in place now always renders as the no-inlining variant, byte-identical for every client.This is an intermediate step, not the end state: it brings in-place optimization in line with the request-independent model the optimizer worker uses, and future releases will continue modernizing in-place optimization by converging on that architecture.
What changes for you: the
in_place_optimize_for_browserfilter and theAllowVaryOnandPrivateNotVaryForIEdirectives are retired. They are still accepted so that an existing configuration keeps loading — you will see a warning in the error log — but they no longer have any effect and should be removed. If you enabledin_place_optimize_for_browser(directly, or through theOptimizeForBandwidthrewrite level), images served at their original URLs will now be recompressed in place rather than converted to WebP or AVIF, so those particular responses get larger. Sites whose HTML PageSpeed rewrites are unaffected: the smaller WebP and AVIF variants continue to be served from the rewritten URLs the HTML points at. If you had configured a reverse proxy, CDN or Varnish instance to handleVary: AcceptorVary: User-Agenton PageSpeed image responses, that configuration is no longer needed. One behavioral note: configurations that usedAllowVaryOn "None"orAllowVaryOn "Accept"to keep the...QualityForSaveDatasettings from being applied no longer get that suppression — the Save-Data qualities are now used on rewritten URLs whenever they are configured; unset them if you do not want them. Expect a one-time re-optimization pass after upgrading. -
Safari 16+ and Firefox 132+ now receive WebP on rewritten image URLs. Image rewriting chooses an image’s output format from the
Acceptheader the browser sent with the page request. When that header does not list image formats, the browser was served the original format instead — a JPEG where Chrome received a WebP or an AVIF. Nothing reported an error; the only visible symptom was that pages weighed more in those browsers than they needed to. Safari 16 and later and Firefox 132 and later are now recognised as WebP-capable from the browser identification they send and receive the WebP variant, so photographic images that PageSpeed rewrites transfer substantially fewer bytes in those browsers.What changes for you: there is nothing to configure and no cache to clear. Newly eligible browsers begin requesting a format that may not have been produced yet, so expect a short warm-up during which they are served the original image while the WebP is generated in the background — the same behaviour as any cold cache. Existing optimized images stay valid and are not regenerated. AVIF is unaffected and continues to be offered only to browsers that ask for it by name, so these browsers receive WebP rather than AVIF.
-
Optimized
.pagespeed.resources now declareCache-Control: public, immutable. The URL of a rewritten resource embeds a hash of its content — when the content changes, the URL changes — so the response behind a given URL can never change. The one-yearmax-agethese resources have always carried now comes with an explicitpublicand the standardimmutabledirective (RFC 8246): browsers that support it (Firefox and Safari) skip revalidating these resources even on a user-triggered reload — the case where browsers otherwise revalidate every resource on the page despite a valid freshness lifetime — and the explicitpublicmakes the responses cacheable on CDNs that require the token (such as Google Cloud CDN) without extra configuration. Other browsers and caches ignore the new directives, so behavior there is unchanged. The upgrade is applied only to responses that were already publicly cacheable: a resource derived from anyprivate,no-cache, orno-storeinput keeps its restricted caching exactly as before, and responses served under a non-matching URL (for example after a stale link) keep their existing short, private lifetime. Responses served at the original URL by in-place optimization are also unchanged: they keep exactly the caching lifetime the origin gave them. -
Optimized HTML no longer carries an
s-maxagedirective inviting shared caches to store it. Ans-maxagedirective could survive on optimized HTML, allowing a shared cache to store and re-serve an optimized page that was intended for a single visitor. It is now stripped. Deployments using the downstream-cache integration (DownstreamCachePurgeLocationPrefixand related directives) are unaffected: that feature intentionally preserves the origin’sCache-Controlon optimized HTML, and continues to. The same applies withModifyCachingHeaders off, which disables all of PageSpeed’s caching-header rewriting including this fix.s-maxagehandling on non-HTML resources is unchanged.What changes for you: if you operate a shared cache in front of mod_pagespeed, an update is recommended.
-
The JavaScript minifier no longer emits unparseable output for valid JavaScript when a comment sits between
letand its binding. The tokenizer’sletdeclaration lookahead skipped whitespace but not comments, soletinlet /*c*/ row …(orlet//…with the binding on the next line) was misread as an identifier. On that reading a linebreak after the binding looked droppable — but the output re-parses withletas a declaration keyword, where the linebreak can be required:let /*c*/ rowfollowed by+4;on the next line was minified tolet row+4;, a syntax error that breaks the entire script. The lookahead now skips//and/*…*/comments too, so the declaration is recognized and the linebreak is preserved. Output for valid JavaScript is unchanged; the fix only repairs the misread shapes. If any of your JavaScript hit this, an update is recommended.
2.0.40 2026-08-01 optimizer worker Cache-staleness, EXIF orientation and a batch of CSS/JS minifier correctness fixes.
Fixed: two JavaScript minifier edge cases, matching the same fixes
in mod_pagespeed 1.15. A line break
before an arrow function’s => was dropped as expression continuation —
ECMA-262 forbids a line terminator there, so the input was already a
SyntaxError, but the minified output (a = x\n=> y → a=x=>y) was
VALID, silently turning a broken script into running code that masks the
author’s error; the line break is now preserved and invalid input is
served as written. And an object or class-field literal whose generator
method is followed by further members (var o = { *m() {}, b: 2 };)
declined minification, so the whole file was served unminified; such
files now minify normally.
Fixed: several more cache-overwrite paths left a stale in-memory copy of the overwritten entry in place, the same class as the optimized-variant fix below. Most visibly: after a page’s origin content was refreshed, a serving process could keep serving the pre-refresh version from its in-memory cache tier indefinitely, even though the refreshed version was in the cache. And after a revalidation confirmed a cached entry was still fresh, the in-memory copy kept its old timestamp, so that process revalidated the entry against origin on every subsequent request instead of serving it for its refreshed lifetime. Internal bookkeeping entries had the same gap, which could cause already-current work — such as llms.txt builds or image re-optimization — to be redone on every subsequent check after a refresh. All of these overwrite paths now invalidate the process’s in-memory copy when the write completes.
Fixed: writing a freshly optimized variant of a resource did not invalidate the in-memory cache tier’s copy of the version it replaced. A process whose in-memory tier had captured the original bytes could keep serving the resource in its original form indefinitely, even though the optimized version was in the cache. The in-memory copy is now evicted when the write completes, so subsequent reads observe the optimized content.
Fixed: optimized images ignored EXIF orientation. Portrait photos (EXIF Orientation 2-8, typical of phone cameras) were re-encoded with their stored, rotated pixels while the tag was stripped, so the served JPEG/WebP/AVIF rendered sideways or mirrored. The orientation is now baked into the pixels at decode time — every output format renders upright with no reliance on the tag surviving, and reported dimensions (including injected width/height attributes and viewport resizing) use the upright orientation. JPEG paths that cannot rewrite pixels (lossless recompression, oversized images) keep an accurate minimal orientation tag instead, so they continue to render upright; no other metadata is reintroduced. The fix is not retroactive: image variants cached before the upgrade keep their old (sideways) orientation until they expire, so purge or reset the cache to serve upright images immediately.
Fixed: the CSS minifier’s decimal optimization rewrote identifiers as
if they were numbers. The rule that shortens 0.5 to .5
fired wherever a 0 before a . was not preceded by a digit —
including inside identifiers, where 0.5 is not a number: the class
selector .a0.5 was served as .a.5, #id0.5 as #id.5, and the
custom-element selector a-0.5 as a-.5, silently restyling any page
using such names. The strip now fires only at a number-token start: a
0 preceded by an identifier character (letter, digit, _, non-ASCII,
or an escape), or by a - that itself continues a dashed identifier,
is left untouched, while genuine numbers still minify (0.5px →
.5px, -0.5px → -.5px).
Fixed: the CSS minifier rewrote brace groups ({...}) nested inside
declaration values. Its shorthand-collapse pass treated
such a group as a nested block and re-emitted its contents: a leading
or trailing ; inside the group was deleted (a{--z:{;x}} served
a{--z:{x}}, a{--z:{L;}} served a{--z:{L}}), and real longhand
sequences inside custom-property values were collapsed into bogus
shorthands (--z:{padding-top:1px;...} became --z:{padding:1px}).
For custom properties this changes what var()/getPropertyValue()
observe — valid-input corruption; ordinary values were rewritten the
same way. The earlier trailing-semicolon trim had the same blind spot
inside these groups. Both phases now leave brace groups inside
declaration values byte-for-byte opaque; real nested blocks
(@media, nested rulesets including :pseudo-starting selectors
like a{:hover{...}}), the genuine trailing-; trim
(a{b:c;} → a{b:c}), and real shorthand collapse are unaffected.
One deliberate edge: nested rules with bare ident+pseudo preludes
(a{a:hover{...}}) are preserved verbatim rather than collapsed —
arguably the spec-correct treatment anyway, since CSS Nesting’s
relaxed parsing tries ident-starting preludes as declarations first.
Fixed: three more CSS minifier correctness bugs in the same
family. The declaration splitter in the
longhand-to-shorthand pass treated a backslash-escaped ; as a
declaration terminator: a{b:c\;} lost the escaped semicolon (served
a{b:c\}), and a{m:\;;--z:url(x)} glued the escape into the next
declaration name, silently swallowing an entire custom property — both
change what is served for valid input, the latter also what
var(--z)/getPropertyValue() observe. The same gap in the pass’s
top-level scan and brace matcher let an escaped quote open a phantom
string (a{b:c\'d'e{x;}}), misaligning block boundaries so string
content was rewritten. Escapes outside string literals are now
consumed as pairs in all of these scanners, like everywhere else in
the minifier. Inside calc() and its siblings, space tightening
around * and / could glue them into a /* comment token
(a{b:calc(1 / *2)}), which a repeated optimization pass then honors
as a real comment and truncates the stylesheet; the space between
/ and * (and the */ mirror) is now always kept. And the
longhand-to-shorthand collapse accepted empty longhand values, so
{overflow-y:;overflow-x::} collapsed to {overflow:: } with a
trailing space no pass trims, converging one optimization pass late;
empty values now refuse the collapse, as do values whose edge
characters (:, ,, !, …) would let a later pass trim the
separator space the collapse emits (the same one-pass-late class; this also declines to collapse
signed lengths like +1px, which were themselves one-pass-late).
With these three, fuzzing’s strict idempotence oracle has no known
violations left on valid input.
Fixed: the CSS minifier treated custom-property values as ordinary
declarations in two more of its phases. The
trailing-semicolon trim deleted ; inside opaque values
(a{--x:{;}} served a{--x:{}}), and the decimal optimizer rewrote
numbers inside them — :root{--x:0.5} became :root{--x:.5}. Both are
fixed: the trim now skips custom-value content but still removes the
terminating semicolon (a{--x:v;} → a{--x:v}), and decimals inside
custom-property values are left untouched. Behavior note: the
decimal change is intentional — custom-property values are observed
verbatim by var() and getPropertyValue(), so what is served for
them is now byte-exact rather than minified. Escaped characters and
braces inside parenthesized value groups are handled correctly, and
detection of custom-property names no longer misfires across
combinators.
Fixed: the CSS minifier treated the contents of unquoted url() tokens
as stylesheet structure in its later phases.
Semicolons and braces are legal URL code points, but the
trailing-semicolon trim deleted a ; inside a url —
a{background:url(x;}y)} served url(x}y) — and the
longhand-to-shorthand pass could split a declaration mid-url and
rewrite it into garbage
(padding-top:url(x;padding-right:1px);... collapsed into a bogus
padding: shorthand). Those phases now skip unquoted url() content
verbatim, like the earlier phases already did.
Fixed: the streaming CSS minifier corrupted stylesheets containing a
backslash-escaped slash (\/) outside string literals. Its first
phase did not recognize backslash escapes in normal context — a gap left
by an earlier fix that added escape handling to the second phase only —
so \/ was misread as the start of a comment and everything up to the
next */ (or end of input) was deleted. A valid stylesheet such as
a{--x:\/*y*/;b:c} lost its custom-property value, changing what
var(--x) and getPropertyValue() observe. Escaped quotes suffered the
mirror-image misparse (a phantom string), which also made repeated
optimization passes collapse one trailing space per pass instead of
converging in one. The same phase skew also let the second phase scan
unquoted url() content as ordinary CSS, stripping spaces before
operator characters (url(a ;b) lost its space). Repeated optimization
also converged one pass late on custom properties whose value starts
with a comment (a{--x:/*c*/v;...} gained a leading space that the
next pass removed). Escapes are now consumed uniformly, both phases
tokenize url() the same way, and comments before the first value
token contribute nothing, so what is served is preserved and
re-optimization converges in one pass.
Fixed: the CSS minifier deleted an escaped space at the end of an
unquoted url() token. Its trailing-space trims before the
closing paren (and at end of input) popped whitespace without checking
for a preceding backslash escape, so a{background:url(x\ )} — an
escaped space is a legal URL character — was emitted as
a{background:url(x\)}, changing the URL; the rebound \) also made
repeated optimization passes re-tokenize the url on the next pass.
The trims now keep escaped whitespace, matching the guard the
custom-property value trim already had.
Changed: the HTML keyword table now recognizes the
data-pagespeed-srcset-url-hashes attribute, completing the
HtmlName::Keyword union with mod_pagespeed 1.15. Documents
carrying that attribute now have it classified as a known keyword during
parsing instead of an unrecognized name. No shipped filter rewrites on
it, so what is served is unchanged; the two products’ keyword enums are
now identical.
Fixed: the HTML parser never ran node destructors, so node data still owned at the end of a parse — attributes of elements deleted mid-parse, character data of dead nodes — leaked for the lifetime of the parser. The parser’s node arena now tracks every allocated object and runs each node’s destructor exactly once when the parse is cleared, releasing that memory. What is served is unchanged.
Fixed: two per-parse memory leaks in the HTML parser. Adjacent character tokens merged by the parser’s coalescing pass kept the merged-away token’s text alive, and an element whose start tag was cut off by end of input (e.g. unterminated mid-attribute) never released its data; both buffers leaked outright because the parser’s node arena frees memory in bulk without running destructors. The merged-away token now releases its text the moment it is retired, and a never-emitted element releases its data when the parse finishes. What is served is unchanged.
Fixed: generators that yield object literals are minified again. A file
containing yield {…} — or a same-line await {…} or for (x of {…}) —
was served in its original, unminified form: the minifier could not rule
out that the braces opened a block rather than the operand, and declined
the whole file. On a single line the braces can only be the operand, so
such files are now fully minified. The genuinely ambiguous form — a line
break between the keyword and the brace, where the two readings differ —
is still declined and served unmodified, as before.
Fixed: a class with a bare field directly before a generator method is no
longer broken by minification. The line break after a bare field — x on
its own line, followed by *gen() {…} — is what ends the field
declaration; the minifier removed it, fusing the field and the generator
method into one invalid declaration, so the minified script failed to
parse where the original ran. The line break is now preserved. Static
(static x), computed-name ([expr]), and private (#x) bare fields
were affected the same way and are covered by the same fix.
Fixed: the HTML parser classified doctypes with a substring heuristic that
produced confidently wrong results on malformed input — any doctype whose
text contained “strict” (even inside the system-identifier URL or in
garbage) was treated as Strict, an “xhtml” substring flipped XHTML
serialization behaviors on under text/html, and any unrecognized doctype
defaulted to HTML 4 Transitional. Doctype classification now uses the
exact-matching parser converged with mod_pagespeed 1.15:
the tokens are compared against the known doctype spellings and anything
unrecognized degrades to “unknown” instead of a guessed classification.
FPI matching is ASCII case-insensitive (browsers sniff doctypes
case-insensitively), and <!DOCTYPE html SYSTEM "about:legacy-compat">
is now correctly classified as HTML5.
Changed: the HTML parser’s element nesting cap is now 512 (was 1024),
unified with mod_pagespeed 1.15’s limit. Pages
nested deeper than the cap stop parsing at the cap and are passed through
unrewritten. Real-world documents nest far below this bound. The trip is
silent, exactly as in 1.15: the truncated parse is the observable signal
and HtmlParse::size_limit_exceeded() stays reserved for the byte/token
ceilings.
Fixed: JSON resources served through the HTML parser’s content-type table
reported their canonical MIME type as application/javascript; it is now
application/json (ported from mod_pagespeed 1.15). AVIF is
now a recognized content type (image/avif, .avif), classified as an
image for rewriting purposes.
Fixed: the HTML lexer’s Restart() error-recovery path trusted an
internal invariant with only a debug-mode assertion; a release build that
ever hit the violated invariant would attempt to resize a string to
SIZE_MAX and abort. It now degrades gracefully (guard ported from
mod_pagespeed 1.15).
Changed: the HTML keyword tables now recognize the allowfullscreen,
decoding, dialog, fetchpriority, loading, picture, and
playsinline names, completing the union with mod_pagespeed 1.15’s table.
Fixed: the postfix ++/-- line-break fix below did not cover variables
named await or yield (legal as ordinary names outside async and
generator functions): the statement-separating line break after
await++/yield++ was still removed, so the two statements re-parsed as
one and the script was served broken. Such line breaks are now preserved as
well.
Fixed: JavaScript minification could corrupt a script in which a line break
separates a postfix ++/-- from a next statement that begins with an
opening parenthesis, or with a leading-dot number such as .5. That line
break is what keeps the two statements apart — without it the code re-parses
as a call or member access on the value just incremented, which the browser
rejects as a syntax error — but the minifier removed it and reported success,
so the script was served broken with nothing logged. Such line breaks are now
preserved (including when carried inside a comment). Line breaks that a
following binary operator genuinely continues are still removed, and
already-correct minified output is byte-for-byte unchanged.
Fixed: JavaScript minification declined any file containing modern syntax and
served it unminified instead — class declarations and class bodies, generator
functions, object-literal method shorthand ({ foo() {} }), getters and
setters, import and export declarations, dynamic import(), and private
class fields. Together with the destructuring fix below, that covers most of
what current bundlers emit, so a modern site could have the majority of its
JavaScript served at full size with nothing reported as an error. Affected
files now minify; measured against modern ES module bundles, the saving
roughly doubled. Text inside template-literal interpolations (${ ... }) is
now minified as well, where it was previously left as written. Files that
contain none of these constructs minify to exactly the same bytes as before,
and the minifier still declines and serves the original wherever a construct
is genuinely ambiguous rather than risk altering the script.
Fixed: critical-CSS inlining could omit stylesheet rules that are actually
used above the fold — most visibly state-conditional rules such as dark-mode
variants — producing a brief flash of unstyled content on first paint.
Critical CSS is now derived against the page’s actual DOM, preserving
@layer and @media structure, and inlining is skipped when too many of
the rules the page uses would be missing.
Fixed: JavaScript minification could corrupt the script it served when the
source contained an IE conditional-compilation comment (/*@ ... @*/), which
the minifier preserves by design. Where such a comment directly followed a
division operator or a regular-expression literal, dropping the whitespace
between them ran the two together into what the browser then read as the
opening of a comment, silently discarding the rest of the line. The script was
served corrupted with nothing reported. Retained conditional-compilation
comments are now kept separated from their neighbours whenever running them
together would change how the script parses, and a line break that automatic
semicolon insertion depends on is preserved across such a comment. Scripts
that present no such hazard are minified exactly as before.
Fixed: JavaScript minification declined any file containing a destructuring
declaration — const {a, b} = obj, let [x, y] = arr, and their nested,
computed-key, default, rest, and for-of forms — and served that file
unminified instead. Current bundlers emit these patterns routinely, so a
modern site could have a substantial share of its JavaScript served at full
size with nothing reported as an error. Affected files now minify.
Behavior is unchanged wherever a construct is genuinely ambiguous: the
minifier still declines and serves the original rather than risk altering the
script.
1.15.0+r21 2026-08-01 module Apache optimization thread counts now size correctly from the machine; plus minifier and parser fixes.
Highlights
-
Apache: optimization thread counts are now sized from the machine. The two worker pools that do optimization work —
NumRewriteThreadsfor HTML, CSS and JavaScript,NumExpensiveRewriteThreadsfor image transcoding — were meant to be sized from the server’s threading model, but the MPM was asked about it before httpd had processed its configuration. On a distribution package, where the MPM is a loadable module, it answered with zeroes, the MPM read as non-threaded, and the server ran one thread in each pool whatever its hardware or configuration. On an httpd built from source with the MPM linked in, the same question was answered differently on the second configuration pass, and the server ran four threads in each pool per child process — never a chosen number, and never visible, because the line reporting it was emitted below the level the log was open at.Both directives now default to
auto. On Apache each pool is then sized at half the CPUs the process is actually permitted to use, divided by the child-process ceiling httpd reports —MaxRequestWorkers / ThreadsPerChildon worker and event,MaxRequestWorkerson prefork, capped byServerLimitin each case — and never drops below one thread. The number comes from httpd itself, so a configuration httpd resolves differently to the arithmetic above is the one that counts.What changes for you: on Apache, the counts now depend on your cores and your configured child-process ceiling, not on your hardware alone. Because that ceiling is high by default — stock
eventallows 16 child processes — most Apache servers resolve to one thread per pool.If you run a distribution package, that is what you were already running and nothing changes. If you built httpd from source, this is a reduction: four threads per pool per child becomes one per pool for a default configuration, because four per child across sixteen children was more optimization threads than such a machine has cores. If you want the old concurrency back, set
NumRewriteThreadsandNumExpensiveRewriteThreadsexplicitly — but size them against your whole server rather than one child. A server configured with few children on a many-core machine gets more than before, which is the case a fixed default could never serve. Expect somewhat higher CPU use while a cold cache warms where the counts went up.NumRewriteThreadsandNumExpensiveRewriteThreadsremain the opt-out and still override the computed value entirely;auto(or0) asks for the automatic sizing explicitly.On nginx and IIS the thread counts do not change. Those ports do not yet report how many worker processes share the machine, and rather than guess a divisor and risk oversubscribing the host, the policy takes its minimum: one thread per pool. On nginx that is exactly what it ran before. IIS is unchanged for a different reason — the IIS module sizes its own optimization pools and this release does not touch that code, so IIS keeps the counts it has always used. The directive validation and the startup log line below do apply to every port.
Before upgrading, check for a negative
NumRewriteThreadsorNumExpensiveRewriteThreads. A negative value used to be accepted and then crash the server process at startup; it is now rejected when the configuration is read, with a message naming the directive. On Apache an invalid directive value is a fatal configuration error, so a negative value left in place will stop httpd from starting after the upgrade. Change it toautofirst. An implausibly large positive value is clamped rather than rejected, with a warning naming both the requested and the resolved count.Two related fixes come with it. The CPU budget now comes from what the process may actually use — the CPU affinity mask, and the CPU quota on the process’s own cgroup and its ancestors, which covers a container, a Kubernetes pod and a systemd unit with
CPUQuota=alike — instead of the host’s core count. And the resolved counts, along with the CPU budget and child count they were derived from, are written to the error log at startup; previously the line was emitted below Apache’s defaultLogLeveland never reached the log at all. (The same line is emitted on the other ports, but nginx’s compiled-in defaulterror_loglevel iserror, so on nginx it still takeserror_log ... warnto see it.) -
The source tarball no longer contains the
html/documentation archive. Those 82 files are the mod_pagespeed 1.0 documentation, published as the/1.0/archive on modpagespeed.com; shipping them inside a 1.15 source tree placed documentation for a different release next to code it does not describe. Nothing else changes: the archive is still published at/1.0/, and current documentation is at modpagespeed.com under/docs/. -
The AVIF still-image encode budget is now configurable on Apache as
ModPagespeedAvifTimeoutMs(server configuration; also accepted inside a<VirtualHost>). In r20 this setting was only reachable on nginx; on Apache it stayed at its 5000 ms default with no way to change it. The tunable itself is unchanged: as in r20, a larger budget admits more images to AVIF and never selects a lower-quality encoder speed than configured. It only becomes settable on Apache now. -
Data-only
<script>blocks (JSON-LD, plain JSON data, import maps, speculation rules, and templates) no longer emit a spurious “Unrecognized script” info message. These blocks are deliberate, non-executable markup, so the diagnostic was noise; genuinely unrecognized script types still log. -
IPRO recorder statistics are now accurate.
ipro_recorder_failedcounts genuine recording failures only (a write/inflate error or a truncated response). Previously it also absorbed expected outcomes — non-rewritable content types, error (4xx/5xx) and not-modified (304/206) responses, empty responses, and load- or size-limited recordings — which each now have their own counter (ipro_recorder_dropped_content_type,ipro_recorder_error_status,ipro_recorder_skipped_transient,ipro_recorder_empty, alongside the existingipro_recorder_not_cacheable,ipro_recorder_dropped_due_to_load, andipro_recorder_dropped_due_to_size). Each recorder outcome is also logged for diagnosis. -
A stray
;after a rule inside an@mediablock — a common hand-authoring artifact, as in@media screen { .a { color: red }; }— no longer fails the stylesheet. Such sheets previously passed through whole: unminified, excluded from CSS combining, and skipped byprioritize_critical_css. They are now handled like any other stylesheet. Sheets that still fail to parse are served byte-for-byte unchanged, as before. -
hint_preload_subresourcesagain hints<script type="module">subresources, now usingrel=modulepreloadin theLinkresponse header it emits (this filter adds no markup to the page). Module scripts stopped being hinted in r20: the olderrel=preload; as=scripthint does not match how a browser fetches a module, so it could cost an extra fetch rather than save one.rel=modulepreloadmatches the module fetch, so the hint is usable. Modules carryingintegrityorcrossorigin="use-credentials"are left unhinted, because a hint for those cannot be matched reliably. Browsers that do not act on the hint are unaffected. Servers running mixed versions against a shared cache degrade cleanly: older versions skip the new cache entries rather than misread them. -
WebP support is now determined from the browser’s
Acceptrequest header alone. Which flavours of WebP a browser could handle — lossy, lossless, transparent, animated — used to be decided from hand-maintained lists of browser version strings. Those lists had to be updated as browsers shipped and had gone stale: browsers whose major version number reached three digits (current Chrome, Edge and Opera, and Chrome on iOS) were read as incapable of animated WebP, and Chrome on iOS also lost lossless and alpha WebP. A browser that advertises WebP is now taken to support all of it, matching how AVIF has always been handled, so that class of staleness cannot return. Browsers that do not advertise WebP are unaffected. Beyond the browsers named above, the main beneficiary is Safari, which advertises WebP on image requests but was never on the old lists: within_place_optimize_for_browser, Safari can now receive transparent and lossless WebP where those filters are enabled and it previously received PNG. What a given visitor gets still depends on the request headers, and some CDN and proxy configurations hold responses to lossy WebP. Upgrade note: the first start after upgrading to r21 re-optimizes some images once, because which WebP features a browser supports forms part of the image optimization cache key, and this release changes that determination. On a default configuration the browsers named above are the ones affected — a minority of visitors, or most current-browser traffic if you enableconvert_to_webp_animated. Sites runningin_place_optimize_for_browseradditionally see it for the share of their traffic that the old lists never covered. Pages keep being served normally and re-optimization proceeds in the background, as with any cold cache; while the pass completes, affected images are served in their original form, so expect a brief rise in page weight. No purge or manual invalidation of any downstream proxy or CDN is required: where an image’s optimized output changes, it is published under a new rewritten URL and the HTML is updated to point at it, while previously rewritten URLs keep resolving and age out normally. -
The legacy JavaScript minifier has been removed. The tokenizer-based minifier — the default since 1.10.33.0, and the only one that understands modern JavaScript — is now the only JavaScript minifier.
UseExperimentalJsMinifieris deprecated and ignored: configurations that still set it start normally and log a warning naming the directive, which can simply be deleted. Default configurations are unaffected — they were already using this minifier. Upgrade note: sites that setUseExperimentalJsMinifierexplicitly re-optimize their JavaScript once after upgrading; withonthe output and the rewritten URLs are identical and only that one background pass is new, while withoffthe minified output itself changes. Pages keep being served normally and re-optimization proceeds in the background, as with any cold cache; while the pass completes, affected scripts are served in their original form, so expect a brief rise in page weight. No purge or manual invalidation of any downstream proxy or CDN is required: where a script’s optimized output changes, it is published under a new rewritten URL and the HTML is updated to point at it, while previously rewritten URLs keep resolving and age out normally. Two things also begin working on sites that were runningoff:<script type="module">is now minified, andinclude_js_source_mapsnow produces source maps. If you generated your ownModPagespeedLibrarysignatures forcanonicalize_javascript_librariesagainst the legacy JavaScript minifier, regenerate them; until then those libraries are minified normally instead of canonicalized. -
Automated clients are recognised far more reliably, so the measurement data that drives optimization is collected from real browsers only. The list of known non-rendering clients had not been updated since 2013 and missed the entire current generation: AI assistant fetchers that retrieve a page on a person’s behalf, agent infrastructure, and the HTTP client libraries and command-line tools written since. Traditional crawlers were already recognised. These clients no longer run the instrumentation, critical-image and critical-CSS beacons, so the data those beacons collect — which
prioritize_critical_css,inline_preview_imagesand image prioritization optimize from — reflects what actual visitors render rather than what a non-rendering client reported. Matching is exact and case-sensitive against the client identifier, so ordinary browsers are unaffected. Applies to all supported servers. Note thatcurlandwgetare now classified as automated clients: a page fetched with either for a spot check will not contain the beacon scripts, and lazy-loaded images will be served eagerly. -
nginx: a Web Bot Auth signature can now inform that decision, behind the new
WebBotAuthBotDetectiondirective (server configuration, default off). With it on, a request carrying a cryptographically valid Web Bot Auth signature (RFC 9421) is treated as an automated client whatever identifier it presents — so an agent that identifies honestly is classified correctly even when it sends a browser’s user-agent string, which no identifier list can detect. Only a signature that verifies counts; an absent or failed signature changes nothing. RequiresWebBotAuth, the existing directive that turns signature verification on. Off by default, so Web Bot Auth stays observe-only for every existing deployment: with the new directive off, a verification result still only labels the request — it populates the$x_verified_botnginx variable, which you can log or pass to your own configuration, and the opt-in verified-request statistics — exactly as in r20. -
defer_javascriptno longer sends deferred markup to automated clients. Whendefer_javascript(ordisable_javascript) is enabled,<script>elements are rewritten into a form only PageSpeed’s client-side runtime can execute. A client that does not run that runtime received a page whose scripts never ran and whose external JavaScript was never even requested — script-dead markup it had no way to act on. Automated clients are now served the page’s normal, unmodified script markup instead. This covers the filters that share the same gate:defer_javascript,disable_javascript,defer_iframe,fix_reflow, and thesupport_noscriptfallback they share, so such a client gets clean markup rather than clean markup plus a stray<noscript>redirect banner. Browsers are unaffected, anddefer_javascriptremains off by default.This deliberately includes search-engine crawlers, which previously received the deferred form. They now receive the page exactly as it is authored — normal markup, not a degraded version of it, and without the serialized script execution the deferral runtime imposes.
lazyload_imageshas behaved this way for automated clients for years. No configuration change is required. As with the client recognition above, an automated client that presents a browser’s exact user-agent string is still served the deferred form unless a verified Web Bot Auth signature identifies it. -
JavaScript minification: generators that yield object literals are minified again. A file containing
yield {…}— or a same-lineawait {…}orfor (x of {…})— was served in its original, unminified form: the minifier could not rule out that the braces opened a block rather than the operand, and declined the whole file. On a single line the braces can only be the operand, so such files are now fully minified. The genuinely ambiguous form — a line break between the keyword and the brace, where the two readings differ — is still declined and served unmodified, as before. -
JavaScript minification: a class with a bare field directly before a generator method is no longer broken by minification. The line break after a bare field —
xon its own line, followed by*gen() {…}— is what ends the field declaration; the minifier removed it, fusing the field and the generator method into one invalid declaration, so the minified script failed to parse where the original ran. The line break is now preserved. Static (static x), computed-name ([expr]), and private (#x) bare fields were affected the same way and are covered by the same fix. -
JavaScript minification: an object literal whose generator method is followed by further members is minified again. A file containing
{ *gen() {…}, b: 2 }— a generator method (named anything, includingawaitoryield) with a comma and another member after it — was served in its original, unminified form: the minifier mis-modeled the separator after the completed method body and declined the whole file. Such files are now fully minified. -
JavaScript minification: a line break before an arrow’s
=>is now preserved. JavaScript forbids a line break between an arrow head and its=>, soa = xfollowed by=> yon the next line is already a syntax error. The minifier dropped that line break and emitteda=x=>y— turning broken input into valid but different code, masking the authoring error. The line break is now kept, so invalid input is served as it was written. -
JavaScript minification: a division operator no longer merges into a retained IE conditional-compilation comment. When minification removed the space between a division
/and a retained/*@ ... @*/comment, the/and the comment’s opening/*fused into//— a line comment that swallowed the rest of the line and silently changed what the script computes, with both forms valid so nothing failed loudly. A separating space is now kept whenever the two would otherwise join, and the equivalent hazard after such a comment is guarded the same way. -
JavaScript minification: the space between a bare
0and a following property access is now kept.0 .toString()minified to0.toString(), where the period is absorbed as the literal’s decimal point, turning valid code into a script that fails to parse. The space is now preserved after a bare0; other numeric literals are unaffected. -
JavaScript minification: the line break after a postfix
++/--is no longer removed when it is load-bearing. A line break separating a completed postfix++/--expression from a next statement that begins with an opening parenthesis, or with a leading-dot number such as.5, is what keeps the two statements apart: without it the code re-parses as a call or member access on the value just incremented, which the browser rejects as a syntax error — but the minifier removed it and reported success, so the script was served broken with nothing logged. Such line breaks are now preserved (including when carried inside a comment). Line breaks that a following binary operator genuinely continues are still removed, and already-correct minified output is byte-for-byte unchanged. -
HTML parsing: the text of merged character runs is no longer held until end of parse. When the parser coalesces adjacent character tokens into one — a routine step before the rewrite filters run — the token it merged away kept its copy of the text alive for the rest of the parse, so a text-heavy page held more peak memory than it needed to. The merged-away token now releases its text the moment it is retired. What is served is unchanged.
-
The HTML parser is hardened against malformed markup, cross-porting the robustness fixes the optimizer worker accumulated for the same code. Certain malformed HTML could crash the worker process or trip undefined behaviour while a page was being parsed for rewriting. Such markup is now handled safely and the page is served. Update recommended.
Two pieces of modern markup are now recognised where they were previously unknown. A page whose doctype is
<!DOCTYPE html SYSTEM "about:legacy-compat">— the long form the HTML standard reserves for generators that cannot emit the short<!DOCTYPE html>, such as XSLT output — was classified as having an unknown doctype; it is now treated as HTML5 (XHTML5 for XML content types), like any other HTML5 page. And thecrossorigin,integrityandtemplateattributes and elements are now known keywords rather than unrecognised names, which is groundwork only: parsing and rewriting of pages that use them is unchanged in this release. -
CSS: selectors with functional pseudo-classes are no longer mangled. CSS minification could not represent the parenthesized arguments of
:where(),:is(),:not(),:has(),:nth-child()and friends — common in Tailwind v4 and modern CSS resets — so it reported a selector error and silently dropped the argument text:.prose :where(h2)minified to.prose :where, a selector no browser matches. The parser now captures the balanced argument text verbatim and re-emits it on serialization, so these selectors round-trip intact. As a side effect, rulesets that were previously passed through byte-for-byte as opaque regions are now fully parsed, so their declarations are minified and their URLs rewritten like any other ruleset. -
CSS: declarations with a spaced
+addition operator are no longer dropped. CSS minification silently discarded any declaration whose value contained a spaced+—calc(1px + 2px)or a custom property such as--x: 1px + 2px— because the CSS parser treated a+not directly attached to a number as a number-parsing error (e.g.h1 { width: calc(1px + 2px); }becameh1 {}). The parser now lexes such a+as an operator value, so these declarations parse and round-trip; a+directly attached to a number still parses as a signed number. The CSS parser’s regression coverage for modern constructs was expanded alongside this fix (ported from the 2.0 test suite):calc()withvar()operands, calc-operand custom properties, and unicode-range lexing shapes.
1.15.0+r20 2026-07-24 module Security update: output-escaping, input-validation and rewrite-correctness fixes. Update recommended.
Overview
Security and correctness release for the 1.15 line, hardening output escaping, input validation, and rewrite correctness across the HTML rewriter filters. Update recommended.
The largest addition is AVIF image support, bringing the image path to parity with the WebP support it has shipped for years: opt-in AVIF encoding on Apache, nginx, and IIS, served only to browsers that advertise the format.
The release also ships a filter modernization batch: new opt-in filters for critical images and speculation rules, revived Google Fonts CSS inlining, Core Web Vitals reporting from the instrumentation beacon, and support for modern JavaScript and CSS constructs that previously passed through unoptimized.
Provenance. Most of the issues addressed here are long-standing defects that originate in the upstream mod_pagespeed codebase (originally developed by Google as open source) on which the 1.15 line is built; each was verified against the published upstream source. A few are gaps in functionality added more recently (Content-Security-Policy handling and stylesheet charset fidelity). All are now fixed.
Security
- The nginx bundled with the NuGet sidecar package and its container image is updated to 1.30.4, picking up the July 2026 upstream nginx security fixes — including CVE-2026-42533, a request-processing memory-safety defect that the upstream nginx advisory reports as exploited in the wild. Update recommended for sidecar deployments. If you build the nginx module against your own nginx, build against 1.30.4 or later.
- Fixed a cross-site scripting issue where crafted CSS could break out of an
inlined
<style>element when CSS optimization was enabled. - Fixed a cross-site scripting issue in local-storage cache inlining where a crafted image attribute could inject script on repeat page views.
- Fixed a cross-site scripting issue where a crafted image
idcould inject script during inline-image deduplication. - Fixed a cross-site scripting issue where a crafted image URL could be reflected unescaped into inline image-preview JavaScript.
- Hardened image optimization against crafted image dimensions that could bypass the resolution limit and trigger excessive memory use (denial-of-service).
- Fixed a CSS dependency-parsing defect that could misread stylesheet contents
(out-of-bounds read / dropped
@importrules). - Fixed a crash on the image-spriting path that could be triggered by a malformed image file declaring invalid dimensions (denial-of-service). Such inputs are now rejected and the page is served with the original images; sprite sets mixing an unusable image with valid ones now sprite the valid subset. A related defensive guard covers the inline image-preview path, which is not reachable from end-user input.
- Hardened the JavaScript minifier against crafted scripts that could drive unbounded memory growth during parsing, exhausting server memory (denial-of-service). Parsing depth is now bounded; a script that exceeds the bound is passed through byte-for-byte unminified, which is the minifier’s existing behavior for any input it declines to process. Ordinary JavaScript — including large bundles and heavily nested framework output — is unaffected. Affects all 1.15 releases up to and including r19; update recommended for any deployment that optimizes JavaScript it does not control.
- Hardened the HTML parser against crafted documents that could drive memory use to the size of the input regardless of configuration (denial-of-service). A hard ceiling now applies to how much a single HTML token may accumulate, independent of the configurable parse-size limit, whose semantics are unchanged. Documents that trip the ceiling fall back to being passed through rather than rewritten. Affects all 1.15 releases up to and including r19; update recommended for any deployment that rewrites HTML it does not control.
- Fixed a content-integrity defect where an out-of-range numeric HTML character reference decoded to an arbitrary, unrelated character instead of being rejected. Out-of-range references are now reported as a decoding error and the original escaped text is kept verbatim. References within the valid Unicode range are unaffected.
- Defense-in-depth output-escaping consistency across several rewriter filters (these paths are not reachable from end-user input; no action required).
- Defense-in-depth division-by-zero guard in the responsive-image sizing path (this path is not reachable from end-user input; no action required).
- Defense-in-depth bounds guard on a JavaScript string- and regular-expression-scanning path (no out-of-range access was reachable; the affected inputs already ended in the existing graceful error).
- Defense-in-depth null guard on an HTML tag-close path (not reachable on this release; the guard protects the invariant against future drift).
- Fixed a startup race in HTML keyword-table initialization where concurrent first-time initialization could construct the shared table twice and publish it without synchronization. Initialization is now thread-safe.
Features
-
AVIF image support. Images can now be optimized to AVIF, alongside the existing WebP path, on Apache, nginx, and IIS. Four new filters, all opt-in, cover the same ground WebP does:
convert_jpeg_to_avif— convert JPEG sources to AVIF.convert_to_avif_lossless— prefer lossless AVIF where it wins (also the path for images with alpha).convert_to_avif_animated— convert animated images to animated AVIF.recompress_avif— re-encode images that are already AVIF.
Behavior worth knowing before you enable them:
- AVIF is not part of
rewrite_imagesor any rewrite level, by design. AV1 encoding costs substantially more CPU than WebP, so folding it intorewrite_imageswould be a silent cost increase for every existing deployment on upgrade. Enable the filters you want explicitly. - AVIF is served only to browsers that advertise it (
Accept: image/avif). There is no user-agent allowlist: the request header alone decides. Clients that do not advertise AVIF keep getting the WebP or original-format result exactly as before. - The encoder picks the smaller of the AVIF, WebP, and original outputs per image, so enabling AVIF cannot make an image larger; if AVIF encoding fails or times out, the rewrite falls back through WebP to the original format rather than failing the image.
- EXIF, ICC color profiles, and XMP are carried across AVIF re-encoding under the existing metadata-retention options. Images carrying a C2PA content-provenance manifest are skipped, never stripped — such images are served as authored.
- Because AV1 encoding is slow relative to WebP, every still-image encode is
admitted against a time budget (
AvifTimeoutMs, default 5000 ms) before it starts, and the encoder speed is derived from that budget and the image’s pixel count: a larger budget admits more images and never selects a lower-quality speed than configured. Images that cannot fit the budget even at the fastest speed are left to the WebP/original path. An absolute 100-megapixel ceiling applies regardless of budget. - Known limitation, animated AVIF: that budget-derived speed selection
applies to still images only. An animated sequence always encodes at
the configured encoder speed, so animated AVIF encodes cost considerably
more per image than stills and do not get faster when
AvifTimeoutMsis lowered — a long animated encode is bounded by the abort applied between frames rather than by the budget. Account for this before enablingconvert_to_avif_animatedover a large animated-image inventory. A future release is expected to extend budget-derived speed selection to animated sequences. - The module now links the AV1 encoder and decoder, so the installed module is appreciably larger than in r19. Plan package and disk footprint accordingly.
- A full family of
image_avif_*statistics (rewrites, per-source-format timeouts, budget overruns, and success/failure timings) is registered automatically, so encode failures and timeouts are visible on the statistics page.
-
New opt-in filter
prioritize_critical_images: setsfetchpriority="high"on the first two images the critical-images beacon has reported above the fold, so the browser front-loads the fetches that determine Largest Contentful Paint. The filter is a strict no-op without beacon data (a wrong guess would prioritize a below-the-fold image at the LCP image’s expense), an author-suppliedfetchpriorityalways wins, and it backs off onSave-Datarequests, AMP documents, and disallowed URLs. It rewrites attributes only and injects no scripts; enabling it also turns on critical-images beaconing. It is not part of any rewrite level’s filter set — enable it explicitly. -
New opt-in filter
insert_speculation_rules: injects a same-origin prefetch ruleset (<script type="speculationrules">) so supporting browsers prefetch a link as the user starts interacting with it; other browsers ignore the tag. The filter backs off when the page already carries its own ruleset, when a Content-Security-Policy forbids inline scripts, on non-200, cookie-setting, orno-storeresponses, and on AMP documents. It is not part of any rewrite level’s filter set — enable it explicitly. -
Google Fonts CSS inlining is revived: the default size cap (
GoogleFontCssInlineMaxBytes) rises from 3 KiB to 48 KiB. Real Font Service responses run ~6–15 KiB, so the old cap rejected essentially every one and the filter never fired. A scheme-qualified<link rel="preconnect" href="…://fonts.gstatic.com" crossorigin>hint is now emitted ahead of the first recognized font stylesheet, whether the loader CSS ends up inlined or not, unless the author already warms that host with a usablecrossoriginpreconnect. Upgrade note: “not inlined” verdicts cached under the old cap keep applying until they expire (up to a day), so inlining ramps up as the cache re-warms. -
hint_preload_subresourcesnow emits font preload hints (rel=preload; as=font; crossorigin, up to four per page) harvested from@font-facerules in the page’s collected CSS. Fonts are discovered two hops late (HTML, then CSS, then the font file), so a hint saves the longest fetch chain. Harvesting is deliberately conservative: woff2 sources only, only faces gated to media needed to render, and only faces whoseunicode-rangecovers printable ASCII. Fonts referenced only from@imported stylesheets are collected onceflatten_css_importsis enabled. Fleets running mixed versions against a shared cache degrade cleanly: older binaries skip the new cache entries. -
The instrumentation beacon now reports Core Web Vitals — LCP, CLS, and INP — plus navigation timing, collected with
PerformanceObserverand sent in a singlesendBeaconPOST when the page is hidden. This replaces the legacy on-load image GET and thebeforeunloadbeacon; thebeforeunloadhandler disabled the browser’s back/forward cache, so instrumented pages are eligible for it again, and a visit restored from it is measured and beaconed as its own page view. Four new histograms (LCP, CLS, INP, TTFB) appear on the admin console automatically, and beacons sent by pages cached before the upgrade are still accepted.ReportUnloadTimeis deprecated to a no-op. -
The tokenizer-based JavaScript minifier (
UseExperimentalJsMinifier) now handles modern syntax —??,??=,?., optional catch binding, destructuring declarations,super, dynamicimport()/import.meta, and module statement forms — where it previously rejected most ES2015+ input and silently passed modern bundles through unminified. Input it still cannot model keeps its original bytes, as before. -
<script type="module">is now a first-class script kind; previously every JavaScript filter skipped module scripts.rewrite_javascriptminifies them (tokenizer-based minifier only), preserving import specifiers and the resource directory so relative imports keep resolving. Combining treats a module as a barrier — scripts on either side still combine among themselves — and inlining, outlining, and disabling leave modules alone, since those rewrites would change import resolution or execution timing. Modules are never relocated to another host by rewriting or cache extension (their fetches are CORS-mode) and are never substituted by library canonicalization. -
CSS inside
@supports,@layer, and@containerblocks, and media queries using level-4 range syntax such as(width >= 768px), previously failed to parse — so everything inside them passed through unminified and unoptimized, which for framework bundles that wrap the whole stylesheet in@layermeant the entire file. These constructs now parse: such stylesheets minify, images referenced inside the blocks are rewritten, inlined, and cache-extended like any others, andprioritize_critical_csscollects and inlines critical selectors inside them while preserving@layercascade order. Sheets that still fail to parse are served byte-for-byte unchanged, as before. -
insert_dns_prefetchnow emits<link rel="preconnect">for the first two domains of its stable-domain list (dns-prefetch for the rest): preconnect warms the whole connection (DNS + TCP + TLS) where dns-prefetch only resolves the name. Preconnect hints are scheme-qualified and keep non-default ports, and the filter no longer emits hints an author already provides. The legacy IE9-onlyrel=prefetchvariant is removed. Rollout note for mixed-version fleets sharing a cache: until the affected property-cache entries expire, an older binary reading entries written by this version can emit malformed preconnect hrefs of the form//https://host, which browsers ignore. -
prioritize_critical_imagesand native-modelazyload_imagesnow handle images that usesrcsetwithout asrcattribute: the beacon reports the candidate the browser actually displays, beacon-critical candidates getfetchpriority="high", and the rest are lazy-loaded under the same first-image LCP protection assrcimages, honoring authorloading/fetchpriority/decodingattributes.
Correctness
- Fixed a Google Analytics snippet-detection bug where analytics markup could be misidentified across requests, leading to missing analytics on some pages.
- Under a strict Content-Security-Policy that permits inline styles but not inline scripts, critical-CSS prioritization could render pages with the non-critical styles missing; such pages are now left unchanged. Update recommended for sites served with a Content-Security-Policy.
- Fixed a text-integrity issue where an external stylesheet with no declared character set and non-ASCII content could be inlined with garbled bytes; such stylesheets are now left unchanged.
- Fixed an input-handling defect where images with extreme author-specified dimensions could produce an invalid responsive-image candidate.
- CSS minification could incorrectly strip units from zero terms inside the
math functions
calc(),-webkit-calc,min(),max(), andclamp()— after any nested function such asvar()(e.g.calc(var(--x) - 0px)becamecalc(var(--x) - 0)), and throughoutmin()/max()/clamp()/-webkit-calcthemselves — producing invalid CSS that browsers drop. Math-function context is now recognized for all of these functions and preserved across nesting. combine_javascriptnow requires the Content-Security-Policy to permit inline scripts before combining, not justunsafe-eval. The filter replaces script tags with small inline bootstrap scripts; under policies such asunsafe-evalwithoutunsafe-inline(orstrict-dynamic/nonce-based policies) the combined scripts loaded but never executed. Affected pages now keep their original, working script tags. Update recommended for sites served with a Content-Security-Policy.- Fixed a
Varyheader merge defect in in-place optimization: when a response already carried oneVarytoken (such asAccept), another needed token (such asUser-AgentorSave-Data) was not added, so a downstream cache could serve the wrong variant of a resource. Tokens are now merged individually; existing tokens are never removed or duplicated. - Fixed a class of defects in the tokenizer-based JavaScript minifier
(
UseExperimentalJsMinifier) that fused valid statements into a syntax error: a line opening with(or a regular-expression literal following animport/exportdeclaration, a plainlet/vardeclaration, or a block-bodied arrow function could be joined onto the previous line when the source relied on automatic semicolon insertion. These declaration boundaries are now modeled; input the minifier cannot model is still passed through byte-for-byte. - Both JavaScript minifiers now treat a block comment containing a line break
as a line break, as the language specification requires. Previously such a
comment was collapsed to a plain space, so a statement boundary that relied
on it disappeared: code of the shape
return/*<newline>*/xwas minified toreturn x, silently changing what the script returned, and comparable inputs were fused into outright syntax errors. Conditional-compilation comments are still retained verbatim. - Pages that combine a
<base>element with a Content-Security-Policybase-uridirective are no longer excluded from optimization outright. Where the policy provably neutralizes every<base>element (base-uri 'none'or an empty source list), the browser ignores the tag, so it cannot affect relative-URL resolution and rewriting now proceeds. Anybase-urivalue that could still match a<base>—'self', host or scheme lists,*— backs off exactly as before, as do pages with no such directive. Strict policies of this shape previously paid a rewriting penalty for being strict. - IIS: fixed a use-after-free in the server’s internal fetcher when a request completed synchronously, which could crash the worker process. Deletion of the completed fetch is now deferred until the originating call has fully returned.
- Scripts carrying Subresource Integrity (
integrity=) are now left untouched by JavaScript rewriting and minification (rewrite_javascript), by combining (an integrity-bearing script acts as a barrier; scripts on either side still combine among themselves), by outlining of inline scripts, and — stylesheets included — by cache extension when it would relocate the resource to another host (domain sharding or mapping). Previously such a rewrite changed or moved the bytes so the hash no longer matched, and the browser blocked a resource that was valid as authored. inline_javascriptno longer inlines external scripts carryingasyncordefer: those attributes are ignored on inline scripts, so inlining silently turned a deferred script into a parser-blocking one that ran mid-parse, out of order. Scripts carrying only one of thefor/eventattribute pair — which per HTML5 never execute — are likewise left alone by inlining and combining, where previously the rewrite made them run.- HTML responses whose bytes depend on the
Save-Datarequest header now carryVary: Save-Data, so a downstream cache cannot serve the data-saver variant to a full-data client or vice versa. ExistingVarytokens are preserved. - CSS minification now recognizes the math functions
calc(),min(),max(), andclamp()case-insensitively, as the specification requires; uppercase or mixed-case forms (e.g.CALC(100% - 0px)) previously had units incorrectly stripped from zero terms, producing invalid CSS that browsers drop. - Stylesheets containing an
@importrule that uses syntax the CSS parser does not understand — such as cascade layers (@import url(x) layer(base);) or other unrecognized import syntax — are no longer import-flattened, a transformation that could drop or reorder rules. Other optimizations (minification, image rewriting) still apply to such stylesheets. - A stylesheet whose only encoding declaration is a leading
@charsetrule could lose that declaration when import-flattening was enabled but no imports were inlined (including results served from the flatten cache), leaving the browser to guess the encoding of non-ASCII content. The declaration is now preserved in that case; stylesheets that do have imports inlined keep the standards-required behavior of dropping it. Note that an already-minified stylesheet with no imports that carries@charsetnow serializes byte-identically, so under the default configuration its rewrite is dropped as a no-op — including any in-CSS image rewrites it previously kept alive. - CSS scanning now recognizes
URL()and@IMPORTcase-insensitively, as the specification requires. Uppercase references were previously invisible to URL rebasing when stylesheets were combined, inlined, or outlined, and to URL rewriting inside style attributes, which could leave stale or unexpectedly relative URLs in place. - Fixed a
local_storage_cachedefect where unavailable or failing browser storage (for example in some private-browsing modes) made an inlined resource disappear from the page entirely — and could break every inlined resource on the page — instead of falling back to the network. Inlining now degrades gracefully. The cookie that records which resources a browser already holds in local storage is now scoped to the whole site (path=/) instead of the current page’s directory, so the server recognizes them site-wide. - Removed an obsolete Firefox workaround from
defer_javascriptthat ran deferred inline scripts through adata:URL (for a Firefox bug fixed in 2013). Content-Security-Policy rules that permit inline scripts do not permitdata:script URLs, so under such policies every deferred inline script was blocked on Firefox and the page broke. cache_partial_htmlno longer triggers the no-script redirect machinery for clients without JavaScript: likedefer_iframe, the filter name is still accepted for configuration compatibility but has no rewriting effect, so it must not mark pages as requiring script execution.elide_attributesnow also elides values matching current-HTML defaults in HTML5 documents —loading=eager,decoding=auto, andfetchpriority=autoon images,media=allon stylesheet links, andfetchpriority=autoon links and scripts — and strips values from the modern boolean attributesopen(dialog),disabled(fieldset),allowfullscreen(iframe), andplaysinline(video). Entries for long-dead markup (command,keygen,seamless, and similar) were removed and no longer alter such elements.remove_quotesnow also strips quotes from attribute values containing/, so most URL-valued attributes (such ashref="/foo/bar") are emitted unquoted. Values ending in/are kept unambiguous by the output writer’s existing guard.- The canonical link inserted by the no-script redirect handling is now built
as a real element. Output is unchanged under the default configuration;
attribute-level filters such as
remove_quotesnow apply to it consistently, as they do to all other elements. - Fixed a diagnostic message in low-resolution image resizing that printed the target width twice instead of width×height.
Statistics
show_ads_snippets_not_convertednow reflects recognized-but-unconverted AdSense snippets (it previously always reported 0).num_js_inlinedno longer over-counts scripts that are intentionally left external in XHTML documents.- Fixed a rare corruption of the image byte-savings counter that could occur under a non-default configuration where an optimized image ended up larger than its input.
- The reported page-load time (the Beacon Reported Load Time histogram and the
total_page_load_msaverage) is now measured from navigation start, so it additionally includes DNS, connection setup, and time to first byte. The histogram steps up once at rollout; this is a measurement change, not a site regression. - The beacon no longer records scripts skipped by administrator
configuration as author opt-outs (
has_pagespeed_no_defer), matching the module-script and CSP-backoff paths. flatten_imports_unparseable_importcounts stylesheets for which import-flattening was declined because an@importrule uses syntax the CSS parser does not understand (such as cascade layers or other unrecognized import syntax).- The
image_ongoing_rewritesgauge no longer leaks a count on a failure path that could not be triggered in practice (defensive fix). - Histogram percentiles (median, 90th, 95th, 99th) are no longer reported as a
-5000no-data placeholder when a histogram has too few samples to estimate them. The cells are now rendered empty instead — for the raw/histogramsoutput as well as the admin console, which previously hid the placeholder for itself only. Anything scraping/histogramsshould expect an empty value rather than-5000. - Latency and duration statistics (HTTP cache lookup and insert, cache hit and
insert, fetch, and backend first-byte latency) are now measured against a
monotonic clock. A backward step of the system wall clock — from a time
sync, a hypervisor, or a misbehaving time daemon — previously produced
negative recorded durations. Absolute timestamps used for HTTP
Dateheaders and for cache freshness are deliberately still taken from the wall clock and are unaffected. - New
image_avif_*statistics cover AVIF rewrites, per-source-format encode timeouts, budget overruns, and success/failure timings.
Configuration
-
New AVIF options, mirroring their WebP counterparts:
AvifRecompressionQuality(default 60),AvifRecompressionQualityForSmallScreens(50),AvifAnimatedRecompressionQuality(50),AvifQualityForSaveData(45), andAvifTimeoutMs(5000). They take effect only when one of the AVIF filters is enabled; see the AVIF entry under Features. -
IIS:
pagespeed.confignow has a single, documented resolution order. The module previously probed the configuration locations independently from several code paths, so when the installed copies diverged, editing one of them could appear to have no effect at all. All paths now use one resolver, with this precedence (lowest first; the last file that defines a setting wins):%ProgramData%\We-Amp\PageSpeed\pagespeed.config— machine-global base.%ProgramData%\We-Amp\IISWebSpeed\pagespeed.config— legacy fallback, for upgrades from IISpeed.<site physical path>\pagespeed.config— per-site override, authoritative when present.
Within each location
pagespeed.configis still preferred over the legacyiiswebspeed.config. For a standard single-site installation, behavior is unchanged — this matches what request handling already did. On startup the module now logs which configuration is in effect and warns when several configuration files exist with differing contents, which is the diagnostic for the “my edit did nothing” case. -
New option
AsyncMetadataL2Writes(default off). When enabled, writes to the disk-backed second tier of the metadata cache are deferred to a background worker instead of running on the rewrite-completion path, so slow disk write latency no longer amplifies into serving-throughput loss and tail-latency spikes during a cache-populating miss storm. Reads stay synchronous. With the option off, behavior is identical to previous releases. Enabling it is safe by construction — served content is content-hash-addressed, so a deferred write can at worst cost a re-optimization, never wrong or stale bytes — but it is default-off this release while it accrues production soak. -
The Universal Analytics filters are deprecated — the service stopped processing hits in July 2023, so the trackers these filters inject or rewrite reported to a discontinued service. Enabling
insert_gaor settingAnalyticsIDlogs a deprecation warning at configuration load (disabling stays silent), andmake_google_analytics_asyncis now a no-op whose name still parses, so existing configurations keep loading. Experiments no longer auto-enableinsert_ga: the A/B framework keeps variant assignment (thePageSpeedExperimentcookie) and the experiment id in the instrumentation beacon, reporting is bring-your-own-analytics, and an experiment spec can still opt in explicitly withenable=insert_ga. -
defer_iframeis deprecated: the filter name was accepted but had no effect on its own (iframe deferral is built intodefer_javascriptanddisable_javascript). The name still parses for compatibility, logs a deprecation warning at configuration load, and no longer triggers the no-script redirect machinery. Configurations usingdefer_iframecan simply remove it. -
The legacy JavaScript minifier is deprecated: it remains available this release via
UseExperimentalJsMinifier off, but explicitly selecting it logs a deprecation warning at configuration load, and it will be removed in a future release. The tokenizer-based minifier (the default since 1.10.33.0) now minifies modern JavaScript it previously passed through unoptimized, backed by a stress corpus of real-world, bundled, and synthetic JavaScript: full coverage, with zero parse, semantic, or idempotence failures. -
The experimental gRPC “central controller” was removed, along with its dedicated controller process and the gRPC build dependency. The
ExperimentalCentralControllerPort,ExperimentalPopularityContestMaxInFlight, andExperimentalPopularityContestMaxQueueSizeoptions are deprecated: they still parse for compatibility but are ignored, and log a deprecation warning when set. There is no behavior change for configurations that did not set these options — the default work-bound expensive-operation throttling and named-lock rewrite scheduling are unchanged. The controller’s experimental-only statistics counters (num_rewrites_requested,num_rewrites_succeeded,num_rewrites_failed, the popularity-contest and queued-controller gauges, andcontroller_reconnect_time) are no longer registered; they read as zero under default configurations, so dashboards scraping them will see them go missing rather than zero. -
Domain sharding (
ShardDomain) is deprecated: it is an HTTP/1-era workaround that hurts performance with HTTP/2 and HTTP/3, which multiplex over a single connection. The directive still works for compatibility but logs a deprecation warning, deduplicated to at most once per process per shard declaration. Configurations usingShardDomaincan simply remove the directive. -
The long-inert filters
div_structure,explicit_close_tags,mobilize_precompute,split_html,split_html_helper, andflush_subresourcesare deprecated: the names still parse for compatibility and log a deprecation warning at configuration load but have no effect. (flush_subresourcesalso no longer adds head-section domains to theinsert_dns_prefetchhint list.) Configurations using them can simply remove them. -
ForbidFiltersnow also gates previously-rewritten URLs carrying the shared cache-extension (.pagespeed.ce.) and image (.pagespeed.ic.) markers: such URLs are no longer served once every filter that can produce them is forbidden. Previously, forbidding those filters stopped new rewrites but could not stop already-rewritten URLs from being served.
2.0.39 2026-07-23 optimizer worker Security: nginx and SQLite updates, cache-sharing rules, image-decode crash. Update recommended.
Fixed: several automatically applied loading and priority hints could work against the page instead of for it; hints are now emitted only when the evidence supports them:
- Injected
preconnectlinks always carriedcrossorigin, warming a connection pool that plain stylesheets, scripts, and images never use (while the Early Hints variant of the same hint was bare). Preconnect hints — in both the HTML and Early Hints — now carrycrossoriginexactly when the resource that motivated them is fetched in CORS mode (fonts,crossorigin-marked resources, ES modules), so the warmed connection is the one the browser actually reuses. - The preload hint for the main (LCP) image is suppressed when that image is
part of a
<picture>element, where the browser may select a different source: preloading could download an image the page never displays. - Iframes are no longer lazy-loaded unconditionally: the first iframe in the
document body — typically an above-the-fold video or media embed — now
loads normally, matching the above-the-fold protection images already had.
(Documents without an explicit
<body>tag keep the previous behavior.) Invisible iframes (0x0,hidden, ordisplay:none— the shape of common tag-manager tracking frames) are left entirely untouched: never lazy-loaded, so tracking frames keep working without JavaScript, and they no longer consume the above-fold exemption meant for the first visible embed. - Invisible images (1x1 beacons,
hiddenordisplay:noneelements) are now left entirely untouched, at every position on the page: never promoted tofetchpriority="high"— the first visible image takes the high-priority slot instead, or none if no visible image appears near the top — and never givenloading="lazy", which browsers answer for layout-less images by skipping the load altogether, silently breaking tracking pixels. Invisible images that earlier releases lazy-loaded will load again. - Stylesheets that the async-CSS optimization defers are no longer also preload-hinted in Early Hints: the old combination re-promoted the very download the optimization had deprioritized, competing with the LCP image for bandwidth. Print stylesheets are likewise no longer preload-hinted. When reprocessing leaves a page with no hints at all, previously stored hints are now cleared instead of being served stale indefinitely.
Upgrade note for ASP.NET Core deployments: upgrade the middleware package together with the worker. The preconnect fix above introduces a new stored form of the hint, and a middleware from an earlier release paired with an upgraded worker does not recognize it: it emits the hint as an unusable preload header instead. Browsers ignore the malformed entry; the affected preconnect is lost and the stray header is served until the middleware is upgraded too. When rolling back a worker after its newer hints have been stored, also purge the metadata cache so the older middleware is not served hint forms it cannot read.
Security: the shared cache now enforces RFC 9111’s rule for authenticated
requests — a response to a request carrying an Authorization header is
stored or reused only when the origin’s Cache-Control explicitly permits
shared caching (public, must-revalidate, or s-maxage); all other such
requests pass through to the origin. The rule now also covers stale content
served during an origin outage and preload hints (103 Early Hints)
derived from a stored response. Previously, responses to authenticated
requests could be shared through the cache. Relatedly, the exemption that
lets a validated license capability token (Authorization: License <token>)
bypass this gate no longer applies to internally generated requests —
try_files, rewrite, error_page and index targets, and subrequests —
because those never pass through the validation step, so an unvalidated
credential could previously reach cached content by way of an internal
redirect. Deployments that route every request through such a target fall
back to ordinary shared-cache rules for license-bearing requests, which may
reduce cache reuse for them. Update recommended for deployments serving
authenticated content. The fix is not retroactive:
entries cached before the upgrade are not removed, so also purge or reset
the cache — or let existing entries expire — if authenticated responses may
have been cached.
Security: a cached response that the origin later marks as non-shareable is
now removed from the cache. When a cached entry was revalidated with the
origin and the origin answered “not modified”, the entry’s freshness was
refreshed without re-checking whether it was still allowed in a shared
cache — so an origin that had since marked the resource private or
no-store (for example, a page that became personalized or
account-specific) could have its previously cached copy kept and served to
other visitors. Such a response now evicts the cached copy instead of
renewing it, and the origin’s restrictive directive is passed on to any
downstream cache rather than being replaced with a freshness lifetime.
Revalidations of an authenticated request that no longer carry explicit
shared-cache permission, and revalidations that set a cookie, no longer
extend a shared entry’s lifetime. Update recommended, particularly where
resources can change between public and private over their lifetime.
This fix does reach some entries cached before the upgrade: an existing
entry already marked private or no-store is evicted the next time it is
revalidated, even if that revalidation carries no Cache-Control of its
own. It is not a sweep, though — an affected entry is only removed once
something revalidates it, so entries that are never revalidated before they
expire are never examined. Purge or reset the cache if you need affected
responses gone on a known schedule rather than on next revalidation.
Only the blanket forms evict. private in its qualified form —
private="Set-Cookie", which restricts just the named headers and permits
the rest to be cached — is correctly treated as non-blanket and keeps the
entry, an idiom origins routinely pair with a perfectly cacheable
max-age. no-cache never evicts in any form: it governs revalidation
rather than storage.
Fixed, on the same path: the response to a revalidation now carries the
origin’s full set of cache directives instead of a simplified freshness
lifetime. An origin’s no-cache in particular is relayed on every outcome,
so a resource served as no-cache, max-age=N is again revalidated before
each reuse rather than being treated by browsers and downstream caches as
freely reusable for the whole lifetime.
Security: the bundled nginx in the published worker and nginx images is updated to 1.30.4, picking up the July 2026 upstream nginx security fixes — including CVE-2026-42533, a request-processing memory-safety defect that the upstream nginx advisory reports as exploited in the wild. The images’ distribution packages are also rebuilt against the latest Ubuntu security updates, which fixes CVE-2026-50812 and CVE-2026-50813 in the bundled SQLite library. Update recommended.
Fixed: script deferral now requires execution evidence. The analysis browser serves same-origin scripts from the cache during analysis, and only scripts observed to execute little or nothing before first paint are deferred. Previously, scripts whose content was unavailable to the analysis could be classified as safe to defer without evidence, which could break pages that rely on synchronous script execution. Deferral continues to apply to scripts referenced by their full URL; as deferral decisions are now evidence-based, pages that load scripts from third-party hosts may see fewer scripts deferred.
Fixed: pages referencing an empty same-origin script file (a stub or feature-flag placeholder) were re-analyzed every hour indefinitely: the empty file was mistaken for a script the analysis had yet to observe, which kept the page’s analysis profile on its shortened warm-up lifetime forever. Cached-but-empty scripts no longer shorten the profile lifetime, so such pages return to the configured re-analysis interval.
Fixed: the validator (ETag) on cache-served responses reflected only the
variant and its byte length, so a revised page or asset that kept the same
byte length also kept its old validator — a returning visitor’s conditional
request could be answered 304 Not Modified and briefly hold on to the
outdated copy. For origins that supply an ETag or Last-Modified (and for
agent-optimized pages), validators now also reflect the stored content
identity, so a same-size revision gets a new validator; after upgrading,
returning visitors re-download affected resources once and revalidation then
resumes as normal. Origins that emit no validators keep the previous
length-based behavior.
Fixed: some conversion paths encoded WebP versions of JPEG and PNG images losslessly instead of at the configured quality. Depending on the image, the oversized result was either discarded in favor of the original — so the photo silently never got a WebP variant — or served as an unnecessarily large lossless WebP. Those images now receive properly compressed lossy WebP; GIF to WebP conversion remains lossless as intended.
Fixed: the CSS minifier removed required whitespace around + and - inside
the newer CSS math functions (sin(), cos(), atan2(), pow(), hypot(),
and related), which could invalidate those declarations; spacing is now
preserved inside the full set of CSS math functions.
Fixed: the CSS minifier rewrote the contents of custom properties, whose
values are opaque token streams where every space matters. Whitespace
around operators was removed and interior runs collapsed, so a value such
as --gap: 1px + 2px came back as 1px+2px and any calc(var(--gap))
that used it became invalid — browsers drop the whole declaration, which
could leave a page visibly unstyled wherever the variable was applied.
Values holding selector fragments or arbitrary strings read back through
getPropertyValue() were altered the same way, and a stylesheet with an
unquoted url() inside a custom property containing /* could have the
remainder of the sheet swallowed. Custom-property values are now preserved
verbatim; leading and trailing whitespace is still trimmed and comments are
still removed. Backslash-escaped characters are also kept intact
throughout, so a selector like a class name containing an escaped space no
longer loses the escape — which previously broke the rule that used it.
Fixed: the JavaScript minifier misread regular-expression literals appearing
after await, yield, or the of in a for-of loop as division, which could
alter the regular expression’s whitespace and change its meaning. It could
also remove the whitespace separating a regular expression’s closing slash
from a following *, forming an unintended comment opener. Such literals
now minify correctly; inputs the minifier cannot safely process continue to
be served unmodified.
Fixed: CSS @import flattening could silently drop styles when only part of
an import chain was cached: an @import that could not be inlined was left
after already-inlined rules — a position where browsers must ignore it.
Flattening is now all-or-nothing per stylesheet: if any imported sheet
cannot be inlined, the stylesheet is served in its original form so every
@import keeps working. Nested imports referencing sheets in other
directories, which previously failed to resolve and caused the same style
loss, now flatten correctly. A stylesheet imported more than once under
different media conditions — for example once for screen and again for
print — is now inlined under each condition instead of only the first,
so the later variants’ styles are no longer dropped. Stylesheets whose
@import statements sit near comments, quoted strings, escaped characters
or parenthesised media conditions are now read the way browsers read them:
previously such a stylesheet could come out with a mangled media wrapper,
applying rules on every medium the original had gated, or swallowing the
styles that followed. Constructs that only resemble an import — an
at-keyword that merely starts with import, or an unrecognised
at-statement ahead of a live @import — no longer trigger inlining, and
the original is served instead.
Fixed: CSS @import flattening corrupted url() references whose address
contains a quote, a backslash, or a closing parenthesis. Such an address
accumulated one extra level of backslash escaping every time it was rebased,
so a reference inside a nested import came out with doubled backslashes and
pointed at a resource that does not exist — images, fonts, and other
subresources reached through those references failed to load. Escape
sequences are now decoded when an address is read and re-applied exactly
once when it is written, so an address survives any depth of nesting
unchanged. Hexadecimal escapes (\22), escaped delimiters inside an
unquoted url(), and line continuations inside quoted addresses are now
interpreted per the CSS syntax rules rather than passed through literally.
Security: malformed or truncated image data could crash a worker process while image dimensions were being read to reserve layout space (a denial-of-service class; no memory disclosure or code execution). Debug builds could additionally hit the same path on one valid image format. Update recommended for deployments that optimize images they do not control. Relatedly, dimensions declared by an image’s own header are now held to the same range limits as author-supplied width/height attributes before being written into the page — an out-of-range header now yields no inferred dimensions rather than an implausible value.
Fixed: the HTML transform pipeline now respects a page’s own
Content-Security-Policy delivered via a <meta http-equiv> tag. When the
page’s policy would make the browser drop an inline element, inline critical
CSS and speculation rules are no longer injected, and stylesheets governed
by the policy are kept render-blocking instead of being converted to an
async load — pages
with a restrictive policy previously could render unstyled until the full
stylesheet loaded. Comma-separated policy lists and multiple policy tags are
combined restrictively. Policies delivered only via response header are not
yet consulted.
2.0.38 2026-07-17 optimizer worker Cache performance and upgrade safety: a lock-free read path, and one cold start on upgrade.
Changed (plan for this before you upgrade): the cache file is now fingerprinted by the bundled cache library’s on-disk format version rather than the release version, so most future upgrades keep the cache warm. This release does change the format, so the first start on it begins with a cold cache. Old and new versions never open the same cache file, which removes a class of cache-corruption risk during upgrades, and the previous version’s cache file is left on disk untouched so a rollback stays warm — delete older cache files manually once you are confident you will not roll back.
Improved: the bundled cache library gains a lock-free read path — cache hits no longer take a lock, so read throughput scales with concurrent workers instead of serializing on the cache — and disk syncs are no longer per-write, with durability periodic and the power-loss window bounded. Under write-heavy load this measured an order of magnitude higher sustained throughput in internal testing. New zero-copy and stale-serve counters appear in the metrics output.
Fixed: cache-integrity fixes close rare corruption windows under concurrent optimization. Writes into an entirely full cache-directory bucket were silently dropped and now land by evicting an existing entry, and a cross-process guard prevents one worker process from resetting a cache file another process still has mapped. Update recommended.
1.15.0+r19 2026-07-17 module Cache upgrade-safety release, plus zero-copy serving corrections.
Cache upgrade-safety release, plus zero-copy serving corrections. Update recommended.
Caching: version-safe cache files
-
The cache now lives in a file fingerprinted by the bundled cache library’s on-disk format version — not the mod_pagespeed release version. Format changes are anticipated to be infrequent, so most future upgrades will keep the cache warm. When the format does change (as it does in this release), old and new worker processes never open the same file, which removes a class of cache-corruption risk during upgrades, when both could briefly overlap on one cache file.
-
Upgrade note: the first start after upgrading to r19 begins with a cold cache. Pages keep being served normally and re-optimization proceeds in the background, as with any cold cache.
-
The previous version’s cache file is left on disk untouched, so rolling back to the previous package is warm — it finds its cache exactly as it left it. Once you are confident you will not roll back, you can delete the older files in the cache directory to reclaim disk. Nothing is deleted automatically. (Cache files are sparse; apparent size overstates actual disk use.)
-
Fixed: when a cache-directory hash bucket filled up entirely with current-version entries, new writes to that bucket were silently dropped (the entry was simply never cached, so affected resources were re-optimized on every request). Writes now land by evicting an existing entry, and a new
bucket_full_evictionsstatistic makes the condition observable. The effect was most likely during upgrade-day write storms into a cold cache.
Zero-copy serving (opt-in, now available on all three platforms)
- Cached resources can be served directly from the memory-mapped cache without copying the payload — available on nginx, Apache, and IIS. How it works and what it wins: The memory-mapped cache: zero-copy serving and Cyclone vs. the file cache: benchmarking a memory-mapped page cache.
- A correction to the r18 notes: they described zero-copy serving as on by default on nginx, but common configurations silently made every request ineligible, so it rarely engaged. That defect is fixed — and with r19 the feature is uniformly opt-in on every platform while it accrues production soak. On-by-default is planned for a future revision.
- To enable it:
- nginx:
pagespeed CycloneZeroCopy on; - Apache:
ModPagespeedCycloneZeroCopy onandModPagespeedCycloneZeroCopyServe on - IIS:
pagespeed CycloneZeroCopy onandpagespeed CycloneZeroCopyServe on
- nginx:
- A new
zerocopy_serve_ineligiblestatistic counts requests that fell back to copied serving, and a one-time log message explains the first fallback (on Apache this message needsLogLevel info; the statistic is always on).
Performance and reliability
-
The bundled cache library gains a lock-free read path — cache hits no longer take a lock — and no longer syncs to disk on every cache write (durability is periodic, and the power-loss window stays bounded), plus a hardening batch covering crash recovery and startup edge cases. Under write-heavy load, removing the per-write disk sync measured an order of magnitude higher sustained throughput in internal testing.
-
Memory-mapped cache reads are verified before being promoted into the in-memory tier, hardening the serving path against torn or damaged entries.
-
IIS: fixed a defect where optimization of a site’s own sub-resources (CSS, JavaScript, images) could fail to converge on machines whose name resolution prefers the IPv6 loopback — pages then kept serving their original resources for minutes at a time. The server’s internal fetches now pin the loopback address family explicitly and fall back to the other family automatically.
-
Windows/IIS: cache-invalidation updates (purge requests) after the first one were silently discarded — the on-disk purge state never advanced, so later purges did not take effect across restarts or between worker processes. Atomic file replacement on Windows now works as intended and purges apply reliably.
-
Windows/IIS: a cross-process guard now prevents one worker process from resetting a shared cache file while another process still has it mapped, closing a corruption window in multi-worker setups; two new statistics (
resets_gate_verified,resets_under_degraded_gate) make gate health observable. -
IIS: fixed a race in the server’s internal fetcher where a sub-resource fetch could be spuriously canceled just after its response had arrived. Because a failed internal fetch is remembered for several minutes, a single spurious abort could stall re-optimization of the affected resource well beyond the moment of failure, surfacing as intermittent optimization stalls.
-
Fixed on all three platforms: behind a TLS-terminating proxy (when the
X-Forwarded-Protoheader is honored), the server’s internal fetches for a page’s own sub-resources combined the page’shttpsscheme with the plain-HTTP listener port — a connection that could never succeed — so the affected CSS, JavaScript, and images were repeatedly re-fetched and never optimized. Internal fetches now use the protocol the listener actually speaks. -
Cache write-failure warnings are now rate-limited, so a persistent storage condition cannot flood the error log.
-
The legacy JavaScript minifier (used when
UseExperimentalJsMinifieris off) now minifies files containing ES2015 template literals; r18 passed such files through unmodified, r19 optimizes them.
Experimental
- The native fetcher (
UseNativeFetcher, nginx) remains off by default. Native HTTPS support has been introduced, so the fetcher can now retrievehttps://resources directly (see HTTPS configuration). Enabling it requires aresolverdirective in the nginx configuration.
2.0.37 2026-07-11 optimizer worker Security: runtime-image rebuild and admin-console hardening, plus cache fixes. Update recommended.
Security: the bundled worker and nginx runtime images are rebuilt against the latest distribution security updates, picking up upstream OS-level CVE fixes; every outstanding finding was medium severity. The admin console also receives security and reliability hardening, including how the URL inspector displays cached content. Impact: dependency update. Affected: 2.0.x container images before 2.0.37. The admin-console change is hardening and carries no affected range. Update recommended, and particularly so for deployments that expose the console.
Fixed: long-running serves of large cached responses now renew their cache read lease for the duration of the transfer — in the nginx front end, in the ASP.NET Core middleware and in the worker’s image-transcode paths. Previously a sufficiently slow transfer could outlive the lease that protects the cache region it was reading from. The cache also gains a background directory-sync thread, off the serving path, which tightens the power-loss window for the on-disk cache index. Existing cache volumes are unaffected: no reset and no repartitioning on upgrade. Cache memory use also drops by roughly 4 MB per cache stripe.
Fixed: worker shutdown is now deterministic; cache-directory election is key-verified, so a rare collision can no longer associate an entry with the wrong key; and a startup race is fixed in which several processes opening the cache at once could corrupt or spuriously fail cache initialization. Recovery after a crash during cache creation is now automatic.
Fixed: an admin-console reliability pass. Config snapshots now capture and restore what is actually on screen; a partially rejected config apply reports the rejected fields instead of a false success; live dashboard streams re-authenticate when a token is supplied after they connect, so signing in no longer requires a full reload; rate-based alerts hold for the duration of an episode and stay dismissed instead of flapping; and the URL inspector sequences detail loads, so switching quickly between variants cannot display stale content.
1.15.0+r18 2026-07-11 module Security update: performance, caching and correctness fixes, with input-validation hardening.
Performance, caching, and correctness release, with security hardening across input validation and output escaping. Update recommended.
Performance
- Cached resources are served with far less copying. Cache hits are now
served from the memory-mapped cache by reference (
CycloneZeroCopyServe) — on by default on nginx, and available as an experimental opt-in on Apache and IIS. A fully zero-copy direct-serve mode (CycloneZeroCopy) is also available as an experimental option (off by default). - Apache streams optimized resource responses instead of double-buffering them; nginx serves cached responses on HTTP/2 and HTTP/3 through a bounded copy ring, keeping per-request memory use bounded even for large responses.
Caching
- The default file cache size is raised to 1 GB (was 100 MB). Cache files are sparse, so the larger default raises the ceiling, not the baseline footprint.
- Optimization metadata and page properties are now stored in a dedicated
small-object area on file caches of roughly 256 MB or larger, so heavy
image traffic can no longer evict them — restarts stay fast even under
load. Upgrade note: on caches at or above that size, the first restart
after upgrading rebuilds the payload cache once (re-optimization proceeds
normally; metadata is unaffected). Set
FileCacheSmallTierPercent 0to disable. - The cache’s RAM tier can now be sized independently of
LRUCacheKbPerProcessvia the newCycloneRamCacheKbdirective (default 0: disabled — memory-mapped cache hits are already served from the OS page cache). - The bundled cache library receives a reliability batch: deterministic shutdown, a fix for a rare hash-collision case that could silently drop a cache entry, a tighter power-loss window (including on Windows), automatic recovery when a crash interrupts cache creation, and a lower memory footprint per cache stripe.
- New cache observability counters on the admin console’s caches page.
Correctness and configuration
Configuration validation is stricter in this release; previously-accepted invalid configurations may now fail to load, which is intentional:
- An invalid filter name in
?PageSpeedFilters=now consistently rejects the whole query — rejection was previously position-dependent, a quirk inherited from the open-source line. - Out-of-range values for bounded options now fail configuration load instead
of being silently accepted (the open-source line never range-checked
these): image qualities (-1..100), progressive JPEG scans (-1..10),
RewriteRandomDropPercentage(0..100),HttpCacheCompressionLevel(-1..9),CentralControllerPort(1..65535). - The
AddResourceHeaderlimit of 20 headers is enforced exactly. - Directive and option-scope matching is now case-consistent across all server platforms, so scope enforcement can no longer be sidestepped by casing (on Apache and nginx this behavior dates back to the open-source releases).
- The legacy JavaScript minifier (used when
UseExperimentalJsMinifieris off) now passes files containing template literals through unmodified instead of corrupting them — the minifier predates ES2015 template literals, and the corruption affected every open-source release.
Critical CSS and Content-Security-Policy
prioritize_critical_cssand other script-injecting filters now honor a restrictive Content-Security-Policy whenHonorCspis enabled, backing off instead of injecting scripts the policy would block; the CSP policy engine received a set of correctness fixes.- Inlined critical CSS preserves stylesheet charset fidelity and link
attributes, and handles
@import/@keyframesrules correctly. - The critical-CSS beacon is viewport-aware, and beacon truncation is now observable in statistics instead of silently starving extraction.
lazyload_imagesgains a native mode that emitsloading="lazy"on below-the-fold images instead of injecting the JavaScript loader.
IIS
- Fixed a defect in the loopback fetcher where a sub-resource fetch that completed asynchronously could be treated as an empty response, suppressing optimization of the parent page for five minutes at a time. Update recommended for IIS deployments.
- Configuration parsing is hardened: a malformed configuration line can no longer crash the module at startup, unknown options are reported instead of silently ignored, and option scoping is enforced on IIS as on the other platforms.
Security
- Hardened input validation and output escaping across the rewriter, beacon handling, and configuration parsing. The underlying gaps date back to the open-source line (1.14.36.1 and earlier). No exploitation is known; update recommended.
- The bundled HTTPS fetch library is updated to curl 8.21.0, which addresses a batch of recently published curl vulnerabilities.
Reliability
- Fixed a worker-pool defect where a work sequence could be recycled while
work was still queued (present since the Google-era code); nginx scheduler
alarms are now driven from the event loop; the experimental native fetcher
(
UseNativeFetcher) gains native TLS support.
2.0.36 2026-07-05 optimizer worker Experimental verified-crawler controls, an async-CSS fix and unified metrics output.
Documentation: the Web Bot Auth verifier introduced in 2.0.34 is designated experimental. It stays off by default and observe-only; its configuration surface and behavior may change between releases. Nothing changes at runtime — this sets expectations for operators enabling it.
Added (experimental): RSL-CAP capability-token validation. When it is enabled,
the origin validates a signed capability token presented with the request and
either returns 401 or 402 or passes the request through, so an operator
can gate a surface on a signed grant. It is off by default and zero-cost on
the request path when disabled; when enabled it sets the HTTP status code and
nothing else — it never settles or meters anything. Issuer keys are warm-fetched from configured HTTPS key
directories into a trust domain kept separate from the Web Bot Auth verifier’s
keys, and verdict counts are exported at /v1/metrics. See the RSL-CAP
documentation for the environment variables and the
verdict-to-status table. The configuration surface and behavior may change
between releases.
Added (experimental): an opt-in AI-crawl counter for Web Bot Auth. When it is
enabled, the origin counts verified AI-crawler requests per signer and
publishes a small machine-readable summary at
/.well-known/webbotauth-counter — privately, behind a bearer token, or
publicly with coarse bucketed counts. It is off by default: no endpoint, no
counting and no change to request handling unless it is explicitly enabled.
GET /v1/metrics also gains a per-signer verified breakdown and a
verify-latency histogram, both first-party only and never on the public
document.
Improved: GET /v1/metrics and the management-socket METRICS command are
now built from a single source and emit the same, complete metric set. Each
surface previously lacked series the other had. No series were removed; both
surfaces strictly gain.
Fixed: the async-CSS flash-of-unstyled-content guard could defer a large stylesheet on the strength of an optimistic browser-measured coverage estimate that contradicted the critical CSS actually inlined, leaving the page unstyled until the deferred sheet loaded. The guard now always enforces the inlined-bytes ratio, and the browser estimate can only make the decision stricter, never override it.
Fixed: when every configured Web Bot Auth key directory is unreachable — as
can happen briefly right after startup, when a directory is served by a
component that comes up later — the worker now retries the key warm after 30
and then 60 seconds instead of waiting out the full 15-minute refresh
interval. Signed requests classify as unknown until keys are warmed, so that
window is considerably shorter.
1.15.0+r17 2026-07-05 module Security update: bundled nginx CVE fixes and an nginx memory-safety fix; cache now persists across restarts.
Caching, reliability, and security maintenance. Update recommended.
- Cache persistence across restarts. Optimization metadata and page properties are written through to the disk cache and survive server restarts, so a deploy or restart no longer triggers a re-optimization spike. (The open-source line kept these caches warm only via periodic snapshots, with a loss window.)
- Reliability. Shutdown-ordering fixes on nginx and IIS eliminate a class of rare crash-on-exit conditions; an IIS initialization race is fixed.
- Security. A memory-safety hardening fix in nginx request handling (a defect inherited from the open-source ngx_pagespeed), and the ASP.NET Core sidecar’s bundled nginx moves to 1.30.3 (CVE-2026-42055, CVE-2026-48142).
- Experimental. The opt-in AI-crawl counter for Web Bot Auth reaches parity with the optimizer worker, including verification of real-world signers.
2.0.35 2026-07-03 optimizer worker The Web Bot Auth verdict counters now appear in the metrics output.
Fixed: the Web Bot Auth verdict counters (result="verified",
result="invalid" and result="other") were counted correctly but were not
included in the GET /v1/metrics Prometheus output; they now appear there as
documented. There is no other change: if you do not use Web Bot Auth or
/v1/metrics, this release changes nothing.
2.0.34 2026-07-03 optimizer worker Web Bot Auth crawler verification: observe-only and off by default.
Added: Web Bot Auth crawler verification — observe-only and off by default, so this release changes nothing unless you enable it.
Any client can claim to be any crawler, and a User-Agent string is
unverified text. Web Bot Auth, built on RFC 9421 HTTP Message Signatures, lets
a bot sign its requests with an Ed25519 key whose public half is published in
a JWKS key directory. When the verifier is enabled, a request carrying a valid
signature from a published key is labeled with an x-verified-bot response
header — <bot-name>, ed25519-verified for key ids promoted through the
operator-curated verified-bots map — while a signature that fails verification
or references an unpublished key is labeled unknown. Requests carrying no
signature material get no header at all, and signature material from other
signing schemes is treated as unsigned. The verdict is purely observational:
it never changes how a request is served, with no blocking and no cache
variation. Aggregate counts are exported at /v1/metrics.
Real-world signer shapes are supported: derived and plain HTTP-field covered
components, multiple signatures per request with tag="web-bot-auth"
selection, and repeated signature header field lines. Key directories are
fetched and refreshed in the background by the worker, over HTTPS only, so
verification itself never blocks on the network.
Enable it with the container environment variables PAGESPEED_WEB_BOT_AUTH,
PAGESPEED_WEB_BOT_AUTH_KEY_DIRECTORIES (comma-separated HTTPS JWKS directory
URLs) and PAGESPEED_WEB_BOT_AUTH_VERIFIED_BOTS (keyid=name,…), or with the
matching worker command-line flags. Changing these settings requires a worker restart.
2.0.33 2026-07-01 optimizer worker Security maintenance release: the runtime image is rebuilt. Update recommended.
Security maintenance release. The bundled runtime image is rebuilt against the latest distribution security updates, picking up upstream OS-level CVE fixes. Impact: dependency update. Affected: 2.0.x container images before 2.0.33. There are no API, wire or configuration changes to the optimizer. Update recommended.
1.15.0+r16 2026-06-30 module Security update: bundled third-party dependency hardening; fixed an Apache exit crash.
- Reliability. Fixed a crash-on-exit condition on Apache (a shutdown-order problem between worker threads and static destruction).
- Dependencies. Defense-in-depth security updates to bundled third-party code.
2.0.32 2026-06-24 optimizer worker Security: hardened native-library build and image-codec updates. Update recommended.
Security: the shipped native library is built with full RELRO, immediate binding, stack-smashing protection and FORTIFY_SOURCE, and the build fails if any of these protections is missing. Impact: binary hardening. Affected: —. Update recommended.
Security: the bundled image codecs (libavif, libaom and libyuv) are updated to pick up upstream CVE fixes. Impact: dependency update. Affected: optimizer worker before 2.0.32. There are no API, wire or configuration changes. Update recommended.
1.15.0+r15 2026-06-24 module Security-hardening release.
Security-hardening release. Update recommended.
- Binary hardening of the shipped modules — full RELRO, immediate binding, stack protector, and fortified libc calls — now asserted at release time.
- Fixed a robustness issue in CSS parsing of malformed input (a defect dating back to the Google-era CSS parser).
- Reduced attack surface: several legacy, unmaintained third-party components were removed from the build, and public-suffix handling moved to the actively maintained libpsl.
- Bundled native dependencies are continuously monitored for published vulnerabilities.
1.15.0+r11–r14 2026-06-21 – 2026-06-22 module Feature, security, and packaging roll-up (r12–r14 contain no additional product changes).
Feature, security, and packaging roll-up (r12–r14 contain no additional product changes).
- Verified AI-crawler controls (experimental, off by default). Web Bot Auth (RFC 9421) signature verification with an observe-only mode, and optional capability-token (RSL-CAP) enforcement — matching the optimizer worker.
- Content Credentials (C2PA) preservation (experimental, off by default). Image optimization can carry provenance metadata through instead of stripping it.
- Security. Two validation gaps in the crawler-signature verifier were closed (it now fails closed); TLS enforcement on IIS internal HTTPS sub-resource fetches is hardened (update recommended for IIS); new bounds on request header count and HTML nesting depth (the Google-era parsers had no limits); admin-console dependency updates.
- Reliability. IIS null-pointer fixes in response handling and cache-policy writes.
- Packaging. New RHEL / AlmaLinux / Rocky 10 channel (x86_64), and an aarch64 nginx-module RPM for EL9.
2.0.30 2026-06-21 optimizer worker Security: a content-integrity fix in agent markdown; Content Credentials survive optimization.
Security: the markdown extractor used for agent feeds now escapes structural characters in extracted content, closing a path that could forge markdown structure into the output. Impact: content integrity. Affected: optimizer worker before 2.0.30. Update recommended.
Changed: the width-to-viewport classification now places the 1024–1279 px band in the Tablet class, matching the engine’s own viewport range at the 1280 px split.
Added: Content Credentials (C2PA) are preserved through image optimization.
Recompression previously dropped the manifest. JPEG-to-JPEG recompression now
carries it through, and the other paths — AVIF, WebP, viewport resize, PNG and
XMP — serve the original bytes unchanged when a manifest is present rather
than stripping it. This is on by default. An additional opt-in flag,
--c2pa-carry, re-splices a PNG’s provenance chunks after recompression, so a
manifest-bearing PNG can stay both optimized and signed rather than being
served in its original form.
Fixed (ASP.NET Core): aggressive cache mode again emits stale-if-error
rather than must-revalidate on optimized assets, and an unwritable default
cache path now raises an error naming the path and the setting to change
instead of an opaque permission failure. The sample apps target net8.0 and
net10.0.
Improved: the nginx front end no longer re-notifies the worker on cache hits of worker-vectorized SVG variants, removing redundant work on every such hit.
Fixed: Markdown responses are served with the correct content type.
Fixed: critical-CSS extraction now keeps rules scoped by :where() and
:is(), including rules where such a wrapper follows another pseudo-class.
Those rules were previously dropped, which removed Tailwind v4 dark-mode
utilities and produced a light-mode flash on dark sites.
Fixed: HEAD requests to /console/ and /v1/ endpoints now follow the same
read-open authorization as GET when PAGESPEED_API_READ_OPEN=true, so
HEAD-based monitoring probes no longer receive a false 401.
Fixed: the “JS minification had parse errors” warning is logged once per resource instead of on every cache refresh of a permanently unparseable file.
Fixed: SVG serve counts are recorded at serve time, so
pagespeed_svg_served_total and the svg.served field of /v1/stats report
real numbers.
2.0.29 2026-06-18 optimizer worker The console URL list and the cache-URL API no longer time out.
Changed: release builds report a build identifier in --version and in
/v1/health.
Fixed: the web console’s URL list (/console/urls) and the /v1/cache/urls
management API no longer time out. The alternate count for each row is now
kept in memory and refreshed whenever a URL’s alternates change, instead of
being computed by walking that URL’s cache chain for every row. A listing that
took tens of seconds — and that blocked /v1/health while it ran, because
both share one thread — is now a pure in-memory read. The JSON response is
unchanged.
2.0.28 2026-06-16 optimizer worker A cold-cache flash-of-unstyled-content fix, and one lockstep image tag in the Helm chart.
Changed (plan for this before you upgrade): the Helm chart collapses the
separate worker.image.tag and nginx.image.tag values into a single
lockstep image tag that defaults to the chart’s appVersion, so the worker and
nginx images can no longer drift apart in a deployment. Removing the per-image
keys is a values-API change (chart 0.3.0).
Improved: the agent_optimize markdown variant is more durable. An
unchanged-origin cache refresh used to drop the generated markdown; the
variant is now preserved across refreshes when the raw-origin content hash is
unchanged, and the rebuild intent is sticky, so a transient miss does not lose
it. Markdown quality also improves for citation: entity decoding inside inline
code, a UTF-8-safe meta description, fenced-code language hints, and handling
for <details>/<summary> and <dl>.
Fixed: async CSS now has a cold-cache fail-safe, closing a flash-of-unstyled-content regression on low-traffic sites. When a declared external stylesheet was not yet in the worker’s cache, the deferred-byte count collapsed to the page’s inline-only CSS, which tripped a “small sheet, trivially safe” escape hatch and async-deferred the unmeasured external sheet — so the whole page rendered unstyled until it loaded. The gate now refuses to defer whenever an external sheet is unresolved: the stylesheet stays render-blocking, the critical CSS is still inlined, and the variant is marked for revalidation so async re-enables automatically once the sheet caches. The in-process ASP.NET Core path gained the same gate, which it previously lacked entirely. There is no wire or ABI change, and async CSS stays on for warm-cache pages.
Fixed: critical CSS is no longer double-shipped on pages that produce two or
more template variants. The inliner left the original external <link> in
place while inlining a copy, so the browser loaded the sheet twice and the
coverage extractor double-counted it. The render-blocking <link> is now
removed, and the async <noscript> fallback is skipped by the scanner.
2.0.27 2026-06-15 optimizer worker A full-page cache-churn fix, a three-band critical-CSS budget, and modern JS minification.
Changed: critical-CSS inlining is now budgeted by a single three-band coverage
policy. Below 10% first-paint coverage the external sheet stays
render-blocking and the critical subset is inlined, to avoid a flash of
unstyled content; between 10% and 60% the critical CSS is inlined and the full
sheet is async-deferred; at or above 60% neither is applied and the sheet
stays render-blocking. This stops the previous behavior on near-whole-sheet
profiles, where most of the stylesheet was inlined and then the whole sheet
re-downloaded, doubling CSS bytes for no first-paint gain. Two new /v1/stats
counters surface the decision (critical_css_skipped_high_coverage,
async_css_suppressed_low_coverage).
Improved: the async-CSS loader is served at a content-hashed, immutable path, so it caches for a year and changes URL only when its bytes change.
Fixed: a full-page cache-churn loop. A content-hash check could see a false
content change on the worker’s own read and purge the whole cache key,
including the agent_optimize markdown variant, so the optimized HTML never
settled into a stable cached variant. Genuine origin changes still invalidate;
the worker’s own reads no longer do. There is no wire or ABI change.
Fixed: the agent_optimize markdown variant is now actually served for
Accept: text/markdown. It was generated and cached, but an origin-refresh
rebuild dropped the agent request, so the response always fell back to HTML.
Markdown quality also improves for citation: HTML entities are decoded before
structural escaping, so non-ASCII text is preserved; YAML front matter carries
the title and metadata; and page-level navigation, header and footer chrome is
stripped while in-article headings are kept.
Fixed: the JavaScript minifier no longer rejects modern syntax. Its tokenizer
learned arrow-function block bodies, optional catch bindings and optional
chaining, so files using them minify instead of falling back to the original
with a logged parse error. The fallback remains in place for genuinely
unparseable input.
2.0.26 2026-06-15 optimizer worker CSP-safe async CSS loading, and honest bandwidth-savings reporting in the console.
Added: render-blocking <link rel="stylesheet"> tags are now genuinely
deferred. Each is switched to media="print" so it downloads without blocking
first paint, and its real media is restored once it loads, by a same-origin
loader script. Both front ends — the nginx module and the in-process ASP.NET
Core middleware — serve that loader byte-identically. The loader is an
external script-src 'self' script with no inline onload handler, so it
works under a strict Content-Security-Policy, and a <noscript> fallback
preserves the stylesheet along with its integrity and crossorigin
attributes. Previously the inlined critical CSS shipped alongside a
still-render-blocking sheet, adding duplicate bytes for no first-paint
benefit; it now buys a real improvement. Async CSS is on by default in the
worker when critical CSS is present (disable it with --no-async-css) and
opt-in through EnableAsyncCss in the middleware. The embedding library gains
two additive getters and an enable_async_css configuration field; existing
field offsets are preserved.
Fixed: the worker’s admin console no longer renders a negative “Total Bandwidth Saved” headline. Because critical-CSS inlining deliberately adds bytes to the served HTML, net serve savings can dip below zero; the dashboard now clamps the headline to zero, matching the Savings page, and discloses the regression honestly instead of showing a contradictory negative figure.
2.0.25 2026-06-14 optimizer worker The agent markdown variant no longer fails to render on larger pages.
Fixed: the agent_optimize markdown variant could fail to render on larger
pages. The asynchronous browser-analysis pass re-read the page HTML from cache
in order to render the variant, and on larger pages that cache slot could be
overwritten or purged before the pass ran, so the re-read found nothing and
the page fell back to serving HTML. The worker now carries the raw origin HTML
it already holds into the analysis queue and renders from there, keeping the
cache re-read as a fallback. This also aligns the render source with the
content hash the markdown variant is bound to.
2.0.24 2026-06-14 optimizer worker A degraded analysis pass can no longer overwrite a good optimization profile.
Fixed: a degraded browser-analysis pass could overwrite a previously good
cached optimization profile. When the pass over-reported
first-contentful-paint CSS coverage and marked nearly the entire stylesheet as
critical, the whole stylesheet was inlined instead of a critical subset —
inflating page weight — and the agent_optimize markdown variant was not
produced for the affected page. A degraded profile is now recognized by its
near-total coverage ratio and the prior good profile is kept instead of being
replaced, while the markdown variant is still rendered; a degraded first-seen
profile is re-analyzed under the existing retry budget.
2.0.23 2026-06-12 optimizer worker Cached pages are no longer served past expiry, plus four optimizer correctness fixes.
Changed: full-page cache entries are no longer served past their TTL expiry.
The serve path now enforces expiry and revalidates, with stale-if-error
retained as an explicit, bounded fallback through the new
pagespeed_stale_if_error_max_age directive (default 86400 seconds). Origin
refreshes are single-flighted, a purge can no longer be silently undone by a
concurrent variant write, and a cache reset bumps a purge generation.
Fixed: four optimizer defects. JavaScript variants are no longer produced from
sources that fail to parse; @import rules using layer() or supports()
are preserved instead of flattened, so stylesheets are no longer dropped;
subresources carrying an integrity= (SRI) pin are left untouched; and
device-pixel-ratio detection covers modern phones through Sec-CH-DPR, with
correct Vary on image variants.
1.15.0+r10 2026-06-11 module Admin console maintenance release; optimization is unchanged.
- Admin console. Console maintenance; optimization unchanged.
2.0.22 2026-06-11 optimizer worker Security: container images rebuilt against current distribution security updates. Update recommended.
Security: the shipped worker, nginx and combined images are rebuilt against current distribution security updates, and the nginx image is re-based onto the shared runtime base. Impact: dependency update. Affected: 2.0.x container images before 2.0.22. Update recommended.
1.15.0+r9 2026-06-10 module Security update: IIS memory-safety defect and resource leak fixed on module unload.
- IIS reliability. Fixed a memory-safety defect and a resource leak on module unload (app-pool recycle). Update recommended for IIS deployments.
1.15.0+r7–r8 2026-06-08 – 2026-06-09 module nginx packaging quality.
nginx packaging quality. Ubuntu modules are rebuilt against Ubuntu’s own patched nginx source — fixing load failures after Ubuntu’s security update for CVE-2026-49975 changed the nginx ABI — and the module now carries a runtime ABI guard that reports a mismatched nginx binary instead of failing unpredictably. EL9 gains an nginx-module RPM built against the distro nginx.
2.0.21 2026-06-05 optimizer worker Packaging release.
Packaging release. No change to the middleware or the optimizer worker.
1.15.0+r3–r6 2026-06-04 – 2026-06-08 module Packaging expansion (r5–r6 contain no additional product changes).
Packaging expansion (r5–r6 contain no additional product changes).
- Prebuilt, signed nginx module packages for Debian 11/12/13 and Ubuntu 22.04 via the apt/yum repositories.
- ASP.NET Core sidecar ships as a NuGet package (WeAmp.PageSpeed, preview).
- EL8 / CloudLinux 8 EasyApache 4 support restored, alongside EL9.
- Build/test baseline nginx moves to 1.30.2 (CVE-2026-9256); shipped binaries are leaner (unused components are no longer compiled in).
- Install and configuration guides were corrected and expanded.
2.0.20 2026-06-01 optimizer worker The live console dashboard no longer shows zeros for stats the Metrics page reports.
Fixed: the admin console’s dashboard is fed by the /v1/ws/stats WebSocket
stream, whose snapshot had drifted from the GET /v1/stats REST contract — it
omitted the svg, policy and quality_baselining stat groups and several
cache-reliability counters, and emitted errors as a scalar. The live
dashboard therefore showed zero for those fields while the Metrics page was
correct. Both endpoints now share a single serializer, so they can no longer
drift.
1.15.0+r2 2026-06-01 module cPanel/EasyApache 4 packaging fix: ea-apache24-mod_pagespeed now reliably wins package precedence.
- cPanel / EasyApache 4. Package versioning fix so the We-Amp
ea-apache24-mod_pagespeedreliably takes precedence over same-named packages from other sources.
1.15.0 2026-06-01 module The renumber release.
The renumber release. The product version becomes 1.15.0 —
continuing directly from the final open-source 1.14.36.1 — so that
package managers and control panels order it after the open-source
line. Package names,
repositories, and configuration are unchanged; the X-Mod-Pagespeed response
header reports 1.15.0.0.
2.0.18 2026-05-31 optimizer worker Maintenance release.
Maintenance release.
The first releases — May to early June 2026
1.1.0+r23–r24 2026-05-31 – 2026-06-01 module Admin console copy and notices updated.
- Admin console. Console copy and notices updated.
1.1.0+r22 2026-05-29 module Security and reliability hardening release, the result of a dedicated audit pass.
Security and reliability hardening release, the result of a dedicated audit pass. Update recommended.
- Fixed a remotely triggerable crash condition (denial-of-service class) on nginx — a defect inherited from the open-source ngx_pagespeed.
- New opt-in
StrictAdminAccessdirective on Apache and nginx: admin, statistics, and console access is decided on the validated client IP and never on client-controlled headers. (IIS already gates admin access to loopback.) - ASP.NET Core sidecar hardening: admin endpoints bind to loopback, tighter configuration-file permissions, and tokens are kept out of logs.
- IIS: two lock-handling fixes and correct IPv6 address reporting.
- Redis: fixed a crash at startup when a database index is configured, and reduced per-command overhead (both defects date to the open-source line, 2017).
1.1.0+r18–r21 2026-05-25 – 2026-05-27 module Security update: 13 admin-console dependency advisories cleared (4 high, 9 medium).
- Reliability. Cache-library update: Apache graceful restarts no longer
leak scoreboard slots, and
nginx -s reloadno longer hangs — both were process-lifecycle defects in cache teardown. - Security. Admin-console dependency updates clearing 13 published advisories (4 high, 9 medium).
- Packaging. The signed apt/yum repositories for the nginx module go GA, with per-distro builds pinned to each distro’s stock nginx and blocking load, symbol, and optimization smoke gates. The EasyApache 4 configuration now degrades gracefully if the module file is absent instead of breaking Apache startup.
2.0.15 2026-05-23 optimizer worker Bandwidth-savings statistics now populate for the ASP.NET Core middleware, matching nginx.
Bandwidth-savings statistics now populate for the ASP.NET Core middleware, at parity with the nginx integration.
1.1.0+r9–r17 2026-05-21 – 2026-05-22 module IIS quality wave (r12–r17 contain no product changes).
IIS quality wave (r12–r17 contain no product changes). Update recommended for IIS deployments.
- Fixed crashes in the IIS URL fetcher and in lock handling.
- Admin endpoints are default-deny in the shipped sample configurations, and the module logs a warning when an admin handler receives a non-loopback request.
- Smoother installation: per-site cache and log directories are auto-created
with safe permissions (new
AutoCreateLogDirdirective), initialization failures surface as specificX-Pagespeed-Init-Statusvalues with per-failure error pages, and the MSI grants the required ACLs. - Upgrading from IISpeed: an existing
iiswebspeed.configis picked up automatically when nopagespeed.configis present.
1.1.0+r1–r8 2026-05-19 – 2026-05-21 module Packaging and distribution (most of these revisions contain no product changes).
Packaging and distribution (most of these revisions contain no product changes).
- First cPanel / EasyApache 4 channel:
ea-apache24-mod_pagespeedRPMs. - The
+rNpackage-revision scheme is established (mapping to the RPMReleasefield and the deb revision).
2.0.4 2026-05-18 optimizer worker The web console is now served by the ASP.NET Core package on the app’s own port.
Added: the ASP.NET Core package now serves the web console on the
application’s own port, so /console/ works straight after dotnet add package and dotnet run — no worker port to configure and nothing written
into wwwroot/. The mount path is configurable with Console.MountPath.
Otherwise a packaging and metadata release: no change to the optimization
engine.
2.0.3 2026-05-17 optimizer worker Hotfix: the linux-x64 native binary loads again on all supported distributions.
Hotfix release. 2.0.2’s linux-x64 native binary failed to load on RHEL 9, Debian 12 and Ubuntu 22.04; 2.0.3 restores it. The linux-arm64, osx-arm64 and win-x64 binaries were unaffected. 2.0.2 is unlisted on nuget.org.
2.0.2 2026-05-17 optimizer worker Packaging correctness: the native binaries are now self-contained.
Packaging-correctness release: the native binaries are now self-contained, so
dotnet add package followed by dotnet run works end to end, with no
dotnet publish step and no LD_LIBRARY_PATH.
2.0.1 2026-05-17 optimizer worker Packaging and metadata fixes on top of 2.0.0.
Packaging and metadata fixes on top of 2.0.0. No change to how content is optimized or served.
2.0.0 2026-05-17 optimizer worker First general release of the optimizer worker, on NuGet and as container images.
2.0.0 is the first general release of the optimizer worker — the out-of-process optimization engine introduced by the 2.0 re-architecture.
Added: the worker shipped in two forms — as the signed
WeAmp.PageSpeed.AspNetCore middleware package on NuGet, for ASP.NET Core 8 and 9, with
native binaries for the linux-x64, linux-arm64, osx-arm64 and win-x64 runtime
identifiers; and, as before, as container images with a Helm chart, for new deployments
and for Kubernetes.
1.1.0 2026-05-15 module Security update: general availability, clearing a six-year CVE backlog including CVE-2023-4863.
The first We-Amp release of the maintained line, continuing directly from the final open-source release, 1.14.36.1 (August 2020). Everything below is relative to that baseline — in particular, the security and reliability fixes repair defects that were present in the final open-source release, not regressions in this line. See What’s new in 1.15 for the narrative version.
Security
- All bundled image codecs and network libraries updated across six years of upstream security work: libwebp 1.5.0 (includes the fix for CVE-2023-4863, the widely exploited WebP heap overflow), libjpeg-turbo 3.1.x, libpng 1.6.58, and a modern TLS stack. The HTTPS fetcher was replaced with libcurl at a current release, clearing a 12-CVE backlog in the process, and nginx module builds moved to a 1.30.x baseline covering the May 2026 nginx CVE cluster.
- Memory-safety fixes and hardening throughout image processing.
- Admin-surface hardening throughout: constant-time token comparison, strict validation of cache-purge request parameters, CSRF protection and security headers on the admin endpoints, and mutating endpoints restricted to the global admin.
- Bounded resource consumption on untrusted input: response-size and header-size caps in the fetcher, an HTML parser that stops at its size limit instead of accumulating without bound, and graceful error handling where malformed input could previously abort the process.
- On IIS, internal HTTPS fetches validate certificates by default.
- Release integrity: all packages are GPG-signed with a published key, and the Windows binaries (DLL and MSI) carry timestamped Authenticode signatures.
- Development is backed by continuous sanitizer testing (AddressSanitizer, ThreadSanitizer, and Application Verifier on Windows).
Reliability
- Fixes for long-standing crash and data-race conditions in the asynchronous rewrite lifecycle, the proxy fetch path, and cache write-ordering (a large-cache outage could previously produce persistent misses).
- Graceful degradation replaces hard process aborts on recoverable conditions.
- Many platform-specific stability fixes; see the per-platform notes below.
Cyclone Cache — the new disk cache
- The original file-based cache is replaced by Cyclone Cache: a fixed-size, memory-mapped, scan-resistant cache shared safely across processes, with no periodic cleanup passes and a persistent index, so restarts start warm.
- Fully configuration-compatible:
FileCachePathkeeps working and now locates the Cyclone cache; obsolete tuning options are accepted as deprecated no-ops with a warning. - The rest of the cache stack is unchanged: LRU cache, shared-memory metadata cache, Redis, and Memcached all remain.
Modernization
- The build moved from GYP to Bazel, with the core on modern C++ (C++20/23) and all dependencies pinned and vendored; the source tarball builds fully offline.
- Prebuilt, signed packages replace compile-it-yourself: deb and RPM for Apache, a prebuilt dynamic module for stock nginx, and an MSI for IIS.
- arm64/aarch64 support added on Linux; 32-bit x86 dropped; Apache 2.2
dropped (2.4+ only); the Google-era stable/beta/unstable channel split
collapsed into a single
mod-pagespeedpackage.
Apache
- A single
mod_pagespeed.sofor Apache 2.4+, installed the same way as before (a2enmod pagespeed, same configuration layout). Bundled TLS symbols are hidden from the host process to prevent library clashes.
nginx
- ngx_pagespeed is now maintained in-tree and shipped as a prebuilt dynamic
module loadable into stock nginx 1.26/1.27 with
load_module— no more compiling nginx from source.
IIS — the IISpeed successor
- A new native IIS module (
pagespeed_iis.dll) for IIS 10+ replaces IISpeed, built on the IISpeed codebase and brought to parity with the Linux platforms (streaming HTML rewriting, in-place resource optimization, per-site configuration, the shared admin console). - Existing IISpeed installs migrate in place: the module reads
pagespeed.configand falls back to an existingiiswebspeed.config; admin pages live at the standard/pagespeed_adminpaths. - Ships as a signed MSI with clean upgrade and uninstall support.
What stayed the same
- All configuration directives, the full filter set (40+ filters),
.pagespeed.resource URLs, in-place resource optimization, the admin and statistics endpoints,.htaccesssupport on Apache, and theX-Mod-Pagespeed/X-Page-Speedresponse headers. Obsolete Google-era options (update channels, distributed rewriting) are accepted as no-ops with a warning rather than breaking startup.
Before the first releases — February to May 2026
2.0.0-beta.1 optimizer worker The 2.0 re-architecture: an asynchronous optimizer worker behind a variant-aware cache.
2.0.0-beta.1 is where the 2.0 re-architecture first appeared.
The design replaced the synchronous, in-request filter pipeline with an asynchronous worker process backed by a variant-aware disk cache, Cyclone. The existing optimization libraries — the HTML parser, the image codecs and the CSS and JavaScript minifiers — stayed as the foundation; the orchestration around them was new. Cyclone keys each stored variant on the properties that produced it, among them the image format, the viewport class, the pixel density and the client’s Save-Data preference, and it is memory-mapped, so a cache hit is served without copying the bytes.
In front of the cache sits a thin nginx module. It classifies the incoming
request, serves the matching variant from the cache when there is one, and notifies the
worker asynchronously when there is not. On a hit it emits Link preload and preconnect
headers; on a miss it sends 103 Early Hints, so the browser can start fetching while the
origin responds.
The worker dispatches on content type instead of on a filter order, so there is no filter sequence to reason about. It generates WebP and AVIF variants ahead of the request that needs them, predicts a per-image encoder quality with a learned model and verifies the result against a perceptual score, and can auto-vectorize a raster image to SVG where the image’s content class makes that worthwhile. A headless browser pass extracts critical CSS and identifies the LCP candidate. A web console served by the worker shows what is in the cache, live statistics, and the configuration.
The beta shipped as container images with a Helm chart, and as an ASP.NET Core integration.
1.1.0-beta.1 2026-02-25 module First public beta of the maintained module line, on Apache, nginx and IIS.
1.1.0-beta.1 is the first public beta of the maintained module line.
It built the module for three front ends from one source tree: Apache and nginx, both stable, and IIS, experimental.
Security updates
Every security fix we have published for either part is listed here with the release that carried it. Where a fix repairs a defect inherited from the open-source line — code that shipped in 1.14.36.1 and was never fixed upstream — the release entry says so.
| # | Advisory | Impact | Affected | Fixed in | Update |
|---|---|---|---|---|---|
| 1 | CVE-2026-32327, CVE-2026-34191, CVE-2025-49506, CVE-2026-34501, CVE-2026-34502 | dependency update — bundled apr-util | apr-util 1.6.3 and earlier in the bundle; the affected components are not built into or called by any mod_pagespeed release | module 1.16.0 | Recommended (no earlier release believed exposed) |
| 2 | GHSA-29g2-3rmr-qm68 | dependency update — web console | ASP.NET Core packages before 2.0.42; container and Helm deployments before 2.1.0 | optimizer worker 2.0.42 · mod_pagespeed 2.1.0 | Recommended |
| 3 | — | denial of service — image decoding | optimizer worker before 2.1.0 | mod_pagespeed 2.1.0 | Recommended |
| 4 | — | hardening — privilege separation for the optimizer worker | — | mod_pagespeed 2.1.0 | Recommended (new default in 2.1) |
| 5 | — | hardening — management API authentication | — | mod_pagespeed 2.1.0 | Recommended (new default in 2.1) |
| 6 | — | hardening — browser-analysis sandbox | — | mod_pagespeed 2.1.0 | Recommended (new default in 2.1) |
| 7 | — | hardening — system-call allow-list enforcement | — | mod_pagespeed 2.1.0 | Recommended (new default in 2.1) |
| 8 | — | hardening — container and Helm defaults | — | mod_pagespeed 2.1.0 | Recommended (new default in 2.1) |
| 9 | — | dependency update — base image | worker and nginx images before 2.0.41 | optimizer worker 2.0.41 | Recommended |
| 10 | — | cache disclosure — authenticated responses | optimizer worker before 2.0.40 | optimizer worker 2.0.40 | Recommended |
| 11 | — | cache disclosure — responses the origin later marks non-shareable | optimizer worker before 2.0.40 | optimizer worker 2.0.40 | Recommended |
| 12 | CVE-2026-42533 | memory safety — bundled nginx | the sidecar package, the container images and the worker and nginx images, before module 1.15.0+r20 / worker 2.0.39 (bundled nginx before 1.30.4) | module 1.15.0+r20 · optimizer worker 2.0.39 | Recommended |
| 13 | — | cross-site scripting — CSS inlining | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 14 | — | cross-site scripting — local-storage cache inlining | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 15 | — | cross-site scripting — inline-image deduplication | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 16 | — | cross-site scripting — inline image preview | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 17 | — | denial of service — image resolution limit | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 18 | — | denial of service — image spriting | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 19 | — | denial of service — JavaScript minifier | all 1.15 releases up to and including r19 | module 1.15.0+r20 | Recommended |
| 20 | — | denial of service — HTML parser | all 1.15 releases up to and including r19 | module 1.15.0+r20 | Recommended |
| 21 | — | content integrity — CSS dependency parsing | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 22 | — | content integrity — HTML character references | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 23 | — | thread safety — HTML parsing (startup race) | all releases up to and including 1.15.0+r19; long-standing defects inherited from the open-source line | module 1.15.0+r20 | Recommended |
| 24 | CVE-2026-50812, CVE-2026-50813 | memory safety — bundled SQLite | worker and nginx images before 2.0.39 | optimizer worker 2.0.39 | Recommended |
| 25 | — | denial of service — image dimension reading | optimizer worker before 2.0.39 | optimizer worker 2.0.39 | Recommended |
| 26 | — | dependency update — runtime images | 2.0.x container images before 2.0.37 | optimizer worker 2.0.37 | Recommended |
| 27 | — | hardening — admin console | — | optimizer worker 2.0.37 | Recommended |
| 28 | — | dependency update — bundled HTTPS fetch library | all releases up to and including 1.15.0+r17 | module 1.15.0+r18 | Recommended |
| 29 | — | hardening — input validation and output escaping | all releases up to and including 1.15.0+r17; the underlying gaps date back to 1.14.36.1 and earlier | module 1.15.0+r18 | Recommended |
| 30 | CVE-2026-42055, CVE-2026-48142 | dependency update — bundled nginx | the ASP.NET Core sidecar, all 1.15 releases up to and including r16 (bundled nginx before 1.30.3) | module 1.15.0+r17 | Recommended |
| 31 | — | memory safety — nginx request handling | all releases up to and including 1.15.0+r16; a defect inherited from the open-source line | module 1.15.0+r17 | Recommended |
| 32 | — | dependency update — runtime image | 2.0.x container images before 2.0.33 | optimizer worker 2.0.33 | Recommended |
| 33 | — | hardening — bundled third-party code (defense in depth) | — | module 1.15.0+r16 | Recommended |
| 34 | — | dependency update — bundled image codecs | optimizer worker before 2.0.32 | optimizer worker 2.0.32 | Recommended |
| 35 | — | hardening — binary hardening of the shipped native library | — | optimizer worker 2.0.32 | Recommended |
| 36 | — | hardening — binary hardening and reduced attack surface | — | module 1.15.0+r15 | Recommended |
| 37 | — | hardening — parser bounds and IIS sub-resource TLS | all releases up to and including 1.15.0+r10 | module 1.15.0+r11–r14 | Recommended; particularly for IIS deployments |
| 38 | — | dependency update — admin console | all 1.15 releases up to and including r10 | module 1.15.0+r11–r14 | Recommended |
| 39 | — | content integrity — agent-optimized markdown output | optimizer worker before 2.0.30 | optimizer worker 2.0.30 | Recommended |
| 40 | — | dependency update — container images | 2.0.x container images before 2.0.22 | optimizer worker 2.0.22 | Recommended |
| 41 | — | memory safety — IIS module unload | IIS deployments, all releases up to and including 1.15.0+r8 | module 1.15.0+r9 | Recommended for IIS deployments |
| 42 | — | availability — prebuilt nginx module | prebuilt Ubuntu nginx module packages, 1.15.0+r3–r6 | module 1.15.0+r7–r8 | Recommended |
| 43 | — | denial of service — nginx request handling | all 1.1.0 releases up to and including r21; a defect inherited from the open-source line | module 1.1.0+r22 | Recommended |
| 44 | — | hardening — admin-surface access control and sidecar defaults | — | module 1.1.0+r22 | Recommended |
| 45 | — | dependency update — admin console (13 published advisories) | all 1.1.0 releases up to and including r17 | module 1.1.0+r18–r21 | Recommended |
| 46 | CVE-2023-4863 | memory safety — bundled libwebp | the open-source line, 1.14.36.1 and earlier | module 1.1.0 | Recommended |
| 47 | — | memory safety — image processing | the open-source line, 1.14.36.1 and earlier | module 1.1.0 | Recommended |
| 48 | — | denial of service — untrusted input handling | the open-source line, 1.14.36.1 and earlier | module 1.1.0 | Recommended |
| 49 | — | dependency update — bundled image codecs and HTTPS fetch library | the open-source line, 1.14.36.1 and earlier | module 1.1.0 | Recommended |
| 50 | — | hardening — admin surface | the open-source line, 1.14.36.1 and earlier | module 1.1.0 | Recommended |
Beyond the individual fixes listed above, both parts carry continuing security work.
At a high level, that work spans:
- Memory-safety hardening across the engine and request-handling paths, backed by continuous sanitizer testing on every platform.
- Robustness against malformed or oversized input — untrusted content and abnormal conditions are handled gracefully instead of crashing or exhausting resources.
- Admin-surface hardening — stricter authentication, safer defaults, and improved resilience of administrative and reporting endpoints to malformed or unauthorized external requests.
- Binary hardening of the shipped modules (RELRO, BIND_NOW, stack-protector, FORTIFY_SOURCE) and a reduced attack surface.
- Ongoing dependency updates, including image codecs, the bundled HTTPS fetch library, and tracking upstream nginx security releases, gated by continuous CVE scanning.
We keep these descriptions general by design: security entries state the impact class and the recommendation, not the mechanism. If your servers run an older open-source mod_pagespeed build, moving to mod_pagespeed 2.1 is the supported way to get all of the above.
The security page sets out how to report a problem and how we handle reports.
Since the last upstream release
The last upstream release was 1.14.36.1, published in August 2020 — the one release made under the Apache incubator. The last Google-era stable release before it was 1.13.35.2, February 2018. If the build you run today came from either of those, this section is the short version of what is different in what we ship now.
We-Amp helped build ngx_pagespeed, maintained mod_pagespeed and helped drive the Apache incubation throughout the Google era, and has carried the line forward since. The module alone carries 765 first-parent merges since 1.13.35.2. Very little of that is visible from a configuration file, which is what the three lists below are for.
Carried over unchanged
- Every existing
pagespeedconfiguration directive and filter name from the open-source release continues to work, with a small set of long-obsolete filter names now accepted with a warning. - The
ModPagespeed*directive spelling still parses, and the admin console is still at/pagespeed_admin/. - The module still runs in process, inside Apache and nginx, on the server you already operate.
- The Debian and Ubuntu package name is unchanged —
mod-pagespeed-stable— so the.debpaths in your configuration management carry over.
Changed
- The dependency graph was rebased off its 2018-vintage pins. libwebp moved from 1.1.0 to 1.5.0, which clears CVE-2023-4863; libpng, giflib and libjpeg-turbo were brought to current upstream versions; curl moved to the 8.x line, with the HTTP fetcher rewritten on top of it; and zlib was replaced by zlib-ng.
- The file-based cache was replaced by Cyclone Cache — a fixed-size,
lock-free, memory-mapped backend shared with the rest of the converged line.
There are no more periodic pruning scans across millions of small files. Old
FileCachedirectories are not read, andFileCacheInodeLimitparses as a no-op so Apache configurations keep loading. - The build moved off the 2012-era gyp and Python 2 toolchain to Bazel,
and packages are prebuilt, reproducible and signed —
.deb,.rpmand.msi— so there is nothing to compile. - The nginx module is dynamic. It loads as a module rather than being recompiled against the nginx source tree, and the prebuilt packages are built per distribution with a runtime compatibility guard, so a distribution’s own nginx patch does not leave you with a module that will not load.
- Apache 2.4 and later, and one
X-Page-Speedheader. The_ap24suffix is gone from the package name, and theX-Page-Speedresponse header is now the same across every port — no port-specific override and no special expired-state header to special-case in monitoring. - The admin console was rebuilt as a single-page application, bundled into one file so it ships inside the module itself with no extra static-asset deployment step. It is the operator UI for cache configuration and filter configuration, and it is stamped with the release tag at build time, so the running build is unambiguous from the UI.
- The whole line is licensed under the Apache License 2.0 — the same license the original codebase carried — and the relicensing is retroactive. The source is published at github.com/We-Amp/mod_pagespeed.
New since 1.14.36.1
- AVIF joined WebP as an opt-in image format, alongside ongoing filter-stack modernization: modern JavaScript and CSS syntax support, Core Web Vitals reporting, Subresource Integrity and CSP awareness.
- IIS became a first-class platform alongside Apache and nginx, and an
ASP.NET Core sidecar package (
WeAmp.PageSpeed.Sidecar) joined as an additional integration option — see ASP.NET Core Getting Started for both ASP.NET Core options. - Linux on arm64. The upstream tree had no aarch64 build at all; Apache and nginx now ship arm64 builds alongside x86_64.
- The optimizer worker joined as the second part of the product. The heavy work — image transcoding, critical CSS, variant-aware caching — runs in a separate process, and with the Apache module or the reverse-proxy deployment the module serves the results from the shared cache. On the deb and rpm channels the two install and upgrade together from the same signed repository; the current release covers the pair.
Everything published after 1.14.36.1 is on this page, release by release. The upstream history up to and including it is archived here, unchanged:
Release notes for the upstream line, up to 1.14.36.1 →
Install and upgrade
mod_pagespeed 2.1 ships as native packages for Apache and nginx — the module
and the pagespeed-optimizer worker from the same signed repository — as
Docker images, and as a Helm chart. See
Getting started to install, the
installation guide for Docker, the
Helm deployment guide for Kubernetes, and
Deployment for production rollouts.
The Apache packages install the configuration that points the module at the optimizer worker. The native nginx module runs on its own; to use the optimizer worker with nginx, run the reverse-proxy deployment.
The IIS package ships from the 1.15 packaging channel.
Reporting a security issue
Found a security problem? Please email security@we-amp.com so we can investigate and ship a fix. We publish security-relevant changes here as part of the regular release notes. Our disclosure policy is on the security page.