Install ASP.NET Core middleware
Install the mod_pagespeed 2.1 ASP.NET Core middleware via NuGet — the same optimization pipeline (image transcoding, critical CSS) inside your .NET app.
On this page
The mod_pagespeed 2.1 ASP.NET Core middleware (WeAmp.PageSpeed.AspNetCore) runs
inside your ASP.NET Core pipeline, installed as a NuGet package. Same optimization
pipeline as the nginx integration — image transcoding, CSS/JS minification, critical
CSS — invoked via P/Invoke to the native C/C++ library. No separate reverse proxy or
sidecar is required.
Prerequisites
- .NET 8 SDK or .NET 10 SDK — dotnet.microsoft.com. The package targets
net8.0andnet10.0. - Supported platform: Linux x64 or arm64, macOS ARM64 (Apple Silicon), or Windows x64
- An ASP.NET Core application that serves HTML responses (Razor Pages, MVC, Blazor Server, or minimal APIs returning HTML)
Install the package
A single dotnet add command is enough — the meta-package pulls in the core
abstractions and all native-asset packages transitively.
dotnet add package WeAmp.PageSpeed.AspNetCore
Three native-asset packages ship alongside the managed assemblies — one each
for linux-x64/linux-arm64, osx-arm64, and win-x64. NuGet pulls all of
them as transitive dependencies; MSBuild’s RID resolution loads only the
matching platform’s binary at publish/run time.
Add to your pipeline
Register PageSpeed services and add the middleware to your request pipeline.
using WeAmp.PageSpeed.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Register PageSpeed services — auto-binds the "PageSpeed" section from
// IConfiguration (appsettings.json, env vars, user secrets, …).
builder.Services.AddPageSpeed();
var app = builder.Build();
// Add PageSpeed middleware — should be early in the pipeline, before
// anything that writes the response body.
app.UsePageSpeed();
app.MapStaticAssets();
app.MapRazorPages();
app.Run();
Place UsePageSpeed() before any middleware that writes the response body. It
needs to intercept responses before they reach the client.
That is the whole setup. There is no Worker section to configure. The worker
process starts automatically and coordinates over an auto-resolved per-process
socket, so HTML and image optimization are on out of the box. You only add a
PageSpeed:Worker section when you want to change that behavior; see the
configuration reference.
Configure via appsettings.json
Add a PageSpeed section to your appsettings.json. A minimal configuration:
{
"PageSpeed": {
"Enabled": true
}
}
That’s enough to turn on every default rewrite (critical-CSS inlining, LCP
preload, lazy loading, image dimensions, async CSS, script deferral, …). Each
rewrite is its own boolean toggle under PageSpeed:Html — see the
configuration reference for the full list.
{
"PageSpeed": {
"Enabled": true,
"Cache": {
"VolumePath": "pagespeed-cache/volume.dat"
}
}
}
A relative path is created under your app’s working directory. See Cache volume for container and production paths.
Verify the install
Run your application:
dotnet run
Check the terminal output for the URL your app is listening on (e.g.
http://localhost:5123). Three checks confirm the install end to end, from
quickest to most thorough.
dotnet runhonorslaunchSettings.json. When aProperties/launchSettings.jsonexists,dotnet runapplies itsapplicationUrland environment variables and overridesASPNETCORE_URLS/ASPNETCORE_ENVIRONMENTfrom your shell. If the app comes up on an unexpected port or environment, that file is why. Passdotnet run --no-launch-profileto ignore it (and fall back toASPNETCORE_URLS/ASPNETCORE_ENVIRONMENT), or editlaunchSettings.jsondirectly to set the port and environment.
1. The X-PageSpeed header on a content route
curl -i http://localhost:<port>/
Look for the X-PageSpeed header on HTML responses. It reports the cache
outcome:
X-PageSpeed: MISS
or, once the worker has built the variant:
X-PageSpeed: HIT
MISS means the worker is generating the optimized variant in the background;
you’ll see HIT on the next request. If the header is present, PageSpeed is
active. Check a content route like / or one of your assets — the /console/*
routes are short-circuited before the middleware, so they intentionally do not
carry X-PageSpeed.
2. The dashboard shows activity
Open http://localhost:<port>/console/. After a few requests, the
Dashboard and Metrics show non-zero request and optimization counts. If they
read zero, jump to
My dashboard shows zeros.
3. Content negotiation serves smaller images
The middleware does not rewrite URLs — the same /hero.jpg URL serves WebP to
WebP-capable browsers and AVIF to AVIF-capable ones. The worker selects the
variant from the request Accept header, and the response carries
Vary: Accept, Save-Data, User-Agent so caches keep the variants apart:
curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/jpeg'
# Content-Length: 98230 Content-Type: image/jpeg Vary: Accept, Save-Data, User-Agent
curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/webp'
# Content-Length: 2422 Content-Type: image/webp Vary: Accept, Save-Data, User-Agent
curl -s -o /dev/null -D - http://localhost:<port>/hero.jpg -H 'Accept: image/avif'
# Content-Length: 415 Content-Type: image/avif Vary: Accept, Save-Data, User-Agent
The same URL returns smaller bytes per format. (Byte counts are from one sample image; yours will differ.) View the HTML source to see the rest: inlined critical CSS, lazy-loaded images, and deferred scripts where coverage analysis allows (tiny scripts may legitimately stay inline-blocking).
Coming from 1.x? The middleware emits no
.pagespeed.magic URLs. The HTML stays clean and the original URLs serve optimized bytes through content negotiation. If you’re looking for rewritten asset URLs in the page source, you won’t find them — that’s expected.
My dashboard shows zeros and nothing is moving
If the Dashboard reads zero and DevTools shows no smaller image variants, the
worker isn’t coordinating with the middleware. On releases before 2.0.14,
Worker.SocketPath defaulted in a way that could leave coordination off; from
2.0.14 the default is an auto-resolved per-process socket with coordination on,
so the quickstart above just works.
Check, in order:
- You’re on 2.0.14 or newer —
dotnet list packageshould showWeAmp.PageSpeed.AspNetCoreat2.0.14+. - You haven’t set
PageSpeed:Worker:SocketPathtonullor""in appsettings.json or in code. Either value disables coordination and logs a startup warning — leave the key unset for the default. See Worker configuration. PageSpeed:Worker:AutoStartis stilltrue(the default). With itfalse, no worker process launches at all.
Then re-run the content-negotiation check — a webp request should return materially fewer
bytes with Content-Type: image/webp.
The 1.15 sidecar: WeAmp.PageSpeed.Sidecar
WeAmp.PageSpeed.Sidecar adds mod_pagespeed 1.15 to your ASP.NET Core app with two lines of
middleware. Your Kestrel app stays the public front door; a bundled, matched (nginx + ngx_pagespeed)
optimizer runs on loopback behind it, recompressing images (jpeg/png/webp), minifying CSS and JS,
inlining and extracting critical CSS, and rewriting HTML for Core Web Vitals on the way out.
Requests that can’t be optimized — and any request while the optimizer isn’t running — pass straight
through your app un-optimized.
builder.Services.AddPageSpeed(builder.Configuration);
// ...
app.UsePageSpeed();
That’s the whole setup. No domain list, no separate front-proxy to configure. This is the default Inverse topology; a classic front-proxy mode (bundled nginx as the public front door) is also available — see Sidecar configuration options.
This is the 1.15 (1.x engine) ASP.NET Core option and is Linux-only (linux-x64,
linux-arm64). The middleware documented above on this page — WeAmp.PageSpeed.AspNetCore — is
cross-platform, in-process, and carries the newest image pipeline (SVG auto-vectorization, Jpegli,
and ML-predicted quality); use it if you don’t specifically need the sidecar’s bundled nginx engine.
On Windows, use the IIS module. The bundled nginx module transcodes AVIF through the same opt-in
filters as the native module — see image filters for how to enable them.
How the sidecar works
In the default Inverse mode, your ASP.NET Core app is the public front door. When the
UsePageSpeed() middleware runs, it streams an optimizable response over loopback to the bundled
nginx, which applies the ngx_pagespeed optimizations and proxies back to a second, private
raw-origin Kestrel endpoint where the middleware bypasses itself. The loop break is automatic and
internal. Non-optimizable requests, and any request received while the sidecar isn’t running, pass
straight through un-optimized — optimization is always additive.
The package binds the public port (Sidecar.ListenPort, 8080 by default), generates its own
nginx.conf, manages the nginx process lifecycle (start, health, auto-restart, graceful stop), and
registers an ASP.NET Core health check. It serves plain HTTP; for HTTPS, front it with a TLS
terminator or load balancer (or set Sidecar.OwnPublicPort = false and configure your own endpoint
via Kestrel:Endpoints).
Install the sidecar package
dotnet add package WeAmp.PageSpeed.Sidecar
This automatically pulls the matched native engine package
(WeAmp.PageSpeed.Sidecar.NativeAssets.Linux, which bundles the nginx + ngx_pagespeed binaries
for linux-x64 and linux-arm64). Publish with a Linux runtime identifier so the bundled nginx
ships next to your app — a framework-dependent publish with no -r omits the nginx binary and the
sidecar fails to start:
dotnet publish -c Release -r linux-x64 # or: -r linux-arm64
Wire up the sidecar
var builder = WebApplication.CreateBuilder(args);
// Reads the "PageSpeed" configuration section. Mode defaults to Inverse; the package
// binds the public port (Sidecar.ListenPort, 8080) and runs the optimizer behind it.
builder.Services.AddPageSpeed(builder.Configuration);
var app = builder.Build();
app.UsePageSpeed();
app.MapHealthChecks("/health");
app.Run();
Or configure in code:
builder.Services.AddPageSpeed(options =>
{
options.RewriteLevel = "CoreFilters";
});
You do not list domains to make optimization work: by default the optimizer rewrites
same-origin resources for any host your app serves, with zero configuration (localhost included,
for local dev). Domains.AuthorizedDomains is only for the advanced cases below.
AddPageSpeed registers a health check named pagespeed (tagged pagespeed, sidecar), so the
app.MapHealthChecks("/health") endpoint above reflects bundled-nginx liveness. For a
PageSpeed-only endpoint, call app.MapPageSpeedHealthCheck("/health/pagespeed") (it filters on the
pagespeed tag).
Sidecar configuration options
Bind a PageSpeed section in appsettings.json. Every key is optional and falls back to its
default (the defaults below are the Inverse defaults):
{
"PageSpeed": {
"Enabled": true,
"RewriteLevel": "CoreFilters",
"Sidecar": {
"OriginPort": 0
},
"Cache": {
"FileCachePath": "/var/cache/pagespeed",
"FileCacheSizeKb": 10240000
},
"AdminAuth": { "Enabled": true }
}
}
| Key | Default | Notes |
|---|---|---|
Enabled | true | Master switch for optimization. |
RewriteLevel | CoreFilters | One of PassThrough, CoreFilters, OptimizeForBandwidth, MobilizeFilters, TestingCoreFilters, AllFilters. |
Sidecar.Mode | Inverse | Inverse (default — your middleware is the public front door, nginx optimizes on loopback), Process (classic front-proxy — bundled nginx is the public front door, Kestrel the private origin), or External (operator-managed nginx). Docker is reserved and fails config validation. |
Sidecar.ListenPort | 8080 | The public port the package binds in Inverse (the default) and the public nginx port in Process. |
Sidecar.OwnPublicPort | true | Inverse only. When true (default), the package binds the public endpoint on 0.0.0.0:Sidecar.ListenPort. Set false to own the bind yourself via Kestrel:Endpoints (--urls/UseUrls/ASPNETCORE_URLS are overridden once the loopback endpoint is added and won’t bind publicly). |
Sidecar.OriginPort | 0 | Private raw-origin loopback port. 0 = auto. In Inverse it is always loopback-TCP (never a Unix socket); in Process/External, 0 prefers a private Unix-domain socket. You normally leave this at 0. |
Sidecar.NginxLoopbackPort | 0 | Inverse only. The loopback-only port nginx listens on. 0 = auto. |
Sidecar.RestrictToAuthorizedHosts | false | Inverse only, defense-in-depth. When true, optimizes only hosts in Domains.AuthorizedDomains (loopback always allowed); every other host is served un-optimized. Default false optimizes all hosts (forward-all). |
Sidecar.AllowPublicAdmin | false | Inverse only. When true, exposes the /pagespeed_* admin endpoints from the public front door; requires AdminAuth.Enabled. Default false returns 404 for them from the public endpoint. |
Sidecar.UseLaunchShim | true | On by default: launches the bundled nginx through the native PR_SET_PDEATHSIG shim so a hard SIGKILL/OOM/container-hard-stop can’t orphan nginx (and leave it holding its loopback port). Degrades gracefully on packages built without the shim binary; set false to force a direct launch. |
Domains.AuthorizedDomains | ["localhost", "127.0.0.1"] | Hosts authorized for cross-origin rewriting (e.g. a CDN). A request’s own same-origin resources are always authorized without listing the host here, so this is not required for normal optimization; it is the strict allowlist only when Sidecar.RestrictToAuthorizedHosts = true. |
Cache.FileCachePath | system temp dir | On-disk cache location for optimized resources. |
Cache.FileCacheSizeKb | 10240000 (10 GB) | File-cache size cap, in KB. |
AdminAuth.Enabled | true | Gates the /pagespeed_* admin endpoints behind a bearer token. Required to be true if Sidecar.AllowPublicAdmin = true. |
Source AdminAuth.Token from a secret store or environment variable — do not put it in
appsettings.json.
Sidecar container & runtime requirements
The bundled nginx + module are glibc ELF binaries built for a glibc ≥ 2.31 floor, so they run on
Debian 11/12, Ubuntu 20.04+, RHEL/Alma 9, and Amazon Linux 2023. libstdc++ is statically linked
and the build has no OpenSSL dependency, so there is no libstdc++ or libssl/libcrypto host
requirement. The binaries link a small set of host shared libraries:
| Host library | Provided by | Notes |
|---|---|---|
libpcre.so.3 | libpcre3 | nginx PCRE — missing from -chiseled/-distroless images |
libz.so.1 | zlib1g | present on standard .NET images |
libcrypt.so.1 | libcrypt1 | present on standard .NET images |
- Recommended base image:
mcr.microsoft.com/dotnet/aspnet:8.0(Debian); add the one missing lib withapt-get install -y libpcre3. - Not supported:
-chiseled/-distroless.NET images (nolibpcre3) and Alpine/musl images (the binaries are glibc-linked). Use a Debian/Ubuntu glibc base. nginx still runs in Inverse mode — on loopback — so the same base-image requirements apply.
Launch your app as dotnet YourApp.dll (the default ENTRYPOINT for dotnet publish container
tooling and mcr.microsoft.com/dotnet/aspnet images) so SIGTERM from docker stop / Kubernetes /
systemd reaches the host’s graceful shutdown and stops the bundled nginx cleanly. Launching the
apphost directly (./YourApp) does not reliably propagate SIGTERM, so the bundled nginx can
survive shutdown and keep holding its loopback port.
Verify the sidecar is working
curl -sI http://localhost:8080/ | grep -i x-page-speed
A present X-Page-Speed header confirms the optimizer is in the path. For per-filter rewrite counts, check /pagespeed_statistics (loopback-only by
default; the middleware returns 404 for the /pagespeed_* admin paths from the public front door
unless Sidecar.AllowPublicAdmin is enabled).
When to use the sidecar vs. the in-process middleware
WeAmp.PageSpeed.Sidecar (1.15) | WeAmp.PageSpeed.AspNetCore (2.1) | |
|---|---|---|
| Model | Middleware front door; bundled nginx optimizes on loopback | In-process middleware (P/Invoke) |
| Platforms | Linux only (linux-x64, linux-arm64) | linux-x64/arm64, osx-arm64, win-x64 |
| Image formats | JPEG/PNG recompression, WebP, opt-in AVIF | WebP and AVIF, plus SVG auto-vectorization and Jpegli |
| Best fit | The proven 1.x nginx engine on Linux | Cross-platform, in-process, newest pipeline |
Next steps
- Configuration reference — all appsettings.json options, hot reload, environment-specific config
- ASP.NET Core performance overview — what the pipeline optimizes and the results to expect
- How the ASP.NET Core middleware works — the architecture behind
UsePageSpeed() - Image optimization in C# — the content-negotiation path in detail
- Fix LCP, INP, and CLS in ASP.NET Core — targeted Core Web Vitals guides
- Software license — Apache License 2.0