Skip to main content
mod_pagespeed 2.1 is here — open source (Apache-2.0): the module you know, plus the optimizer worker

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

  1. .NET 8 SDK or .NET 10 SDK — dotnet.microsoft.com. The package targets net8.0 and net10.0.
  2. Supported platform: Linux x64 or arm64, macOS ARM64 (Apple Silicon), or Windows x64
  3. 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 run honors launchSettings.json. When a Properties/launchSettings.json exists, dotnet run applies its applicationUrl and environment variables and overrides ASPNETCORE_URLS / ASPNETCORE_ENVIRONMENT from your shell. If the app comes up on an unexpected port or environment, that file is why. Pass dotnet run --no-launch-profile to ignore it (and fall back to ASPNETCORE_URLS / ASPNETCORE_ENVIRONMENT), or edit launchSettings.json directly 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:

  1. You’re on 2.0.14 or newer — dotnet list package should show WeAmp.PageSpeed.AspNetCore at 2.0.14+.
  2. You haven’t set PageSpeed:Worker:SocketPath to null or "" 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.
  3. PageSpeed:Worker:AutoStart is still true (the default). With it false, 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 }
  }
}
KeyDefaultNotes
EnabledtrueMaster switch for optimization.
RewriteLevelCoreFiltersOne of PassThrough, CoreFilters, OptimizeForBandwidth, MobilizeFilters, TestingCoreFilters, AllFilters.
Sidecar.ModeInverseInverse (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.ListenPort8080The public port the package binds in Inverse (the default) and the public nginx port in Process.
Sidecar.OwnPublicPorttrueInverse 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.OriginPort0Private 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.NginxLoopbackPort0Inverse only. The loopback-only port nginx listens on. 0 = auto.
Sidecar.RestrictToAuthorizedHostsfalseInverse 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.AllowPublicAdminfalseInverse 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.UseLaunchShimtrueOn 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.FileCachePathsystem temp dirOn-disk cache location for optimized resources.
Cache.FileCacheSizeKb10240000 (10 GB)File-cache size cap, in KB.
AdminAuth.EnabledtrueGates 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 libraryProvided byNotes
libpcre.so.3libpcre3nginx PCRE — missing from -chiseled/-distroless images
libz.so.1zlib1gpresent on standard .NET images
libcrypt.so.1libcrypt1present on standard .NET images
  • Recommended base image: mcr.microsoft.com/dotnet/aspnet:8.0 (Debian); add the one missing lib with apt-get install -y libpcre3.
  • Not supported: -chiseled / -distroless .NET images (no libpcre3) 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)
ModelMiddleware front door; bundled nginx optimizes on loopbackIn-process middleware (P/Invoke)
PlatformsLinux only (linux-x64, linux-arm64)linux-x64/arm64, osx-arm64, win-x64
Image formatsJPEG/PNG recompression, WebP, opt-in AVIFWebP and AVIF, plus SVG auto-vectorization and Jpegli
Best fitThe proven 1.x nginx engine on LinuxCross-platform, in-process, newest pipeline

Next steps