# Install ASP.NET Core middleware

Install the mod_pagespeed ASP.NET Core middleware via NuGet — the same optimization pipeline (image transcoding, critical CSS) inside your .NET app.

Canonical URL: https://modpagespeed.com/docs/aspnet-getting-started/

The mod_pagespeed 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.

Machine-readable recipe for coding agents: [modpagespeed.com/recipes/aspnet-core.md](https://modpagespeed.com/recipes/aspnet-core.md) (see [Install with a coding agent](https://modpagespeed.com/docs/agent-install/)).

## Prerequisites

1. **.NET 8 SDK** or **.NET 10 SDK** — [dotnet.microsoft.com](https://dotnet.microsoft.com/download). 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.

```bash
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.

```csharp
using WeAmp.PageSpeed.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

// 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.UseStaticFiles();
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](https://modpagespeed.com/docs/aspnet-configuration/#worker-pagespeedworker).

> **The console is public until you lock it down**
>
> The setup above also serves the web console at `/console/` and proxies its
> worker API (`/v1/health`, `/v1/ws`, `/v1/cache`, `/v1/stats`, `/v1/capture`
> and `/v1/config`) from your site's root, in every environment, with no
> authentication. Anyone who can reach your site can read the stats and the
> optimizer configuration, change the live configuration with
> `PATCH /v1/config`, and purge the cache. Before you go to production, turn the
> console off or put your own authentication in front of it.

Recommended for production: `Enabled: false`, for example in
`appsettings.Production.json`, so the console stays available in development
only:

```json
{
  "PageSpeed": {
    "Console": {
      "Enabled": false
    }
  }
}
```

The console paths then fall through to your app, and optimization is
unaffected. The top-level `PageSpeed:Enabled` switch does not turn the console
off. Switched off through hot reload, the change can take a few seconds to
apply.

To keep the console in production, gate it: see
[Gate the console with your own authentication](https://modpagespeed.com/docs/aspnet-configuration/#gate-the-console-with-your-own-authentication)
in the configuration reference. Its
[Console](https://modpagespeed.com/docs/aspnet-configuration/#console-pagespeedconsole) section also
covers checking an existing deployment.

## Configure via appsettings.json

Add a `PageSpeed` section to your appsettings.json. A minimal configuration:

```json
{
  "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](https://modpagespeed.com/docs/aspnet-configuration/) for the full list.

> **Local development cache path**
>
> The default cache location, `/var/cache/pagespeed/volume.dat`, needs elevated
> permissions to create. That suits containers and production hosts that run as
> root, but a local `dotnet run` as a normal user fails at startup with
> `UnauthorizedAccessException: Access to the path '/var/cache' is denied`. For
> local development, point the cache at a writable path.

```json
{
  "PageSpeed": {
    "Enabled": true,
    "Cache": {
      "VolumePath": "pagespeed-cache/volume.dat"
    }
  }
}
```

A relative path is created under your app's working directory. See
[Cache volume](https://modpagespeed.com/docs/aspnet-configuration/#cache-volume) for container and
production paths.

## Verify the install

Run your application:

```bash
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

```bash
curl -i http://localhost:<port>/
```

Look for the `X-PageSpeed` header on HTML responses. It reports the cache
outcome:

```http
X-PageSpeed: MISS
```

or, once the worker has built the variant:

```http
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](https://modpagespeed.com/docs/aspnet-getting-started/#my-dashboard-shows-zeros-and-nothing-is-moving).
With `PageSpeed:Console:Enabled` set to `false`, `/console/` and `/v1/*` fall
through to your app (typically a `404`); with the console gated, sign in as an
admin first.

### 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:

```bash
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](https://modpagespeed.com/docs/aspnet-configuration/#worker-pagespeedworker).
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](https://modpagespeed.com/docs/aspnet-getting-started/#3-content-negotiation-serves-smaller-images) — 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.

The current package is 1.17.0; the next sidecar release moves to the product version (2.5.x).

```csharp
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](https://modpagespeed.com/docs/aspnet-getting-started/#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](https://modpagespeed.com/docs/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

```bash
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:

```bash
dotnet publish -c Release -r linux-x64    # or: -r linux-arm64
```

### Wire up the sidecar

```csharp
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:

```csharp
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):

```json
{
  "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. The bundled
nginx links `libpcre.so.3`, the Debian/Ubuntu name of the PCRE library from the `libpcre3`
package, so it runs on the Debian and Ubuntu releases that still ship that package: Debian 11 and
12, Ubuntu 20.04 to 24.04. Debian 13 and Ubuntu 26.04 no longer ship `libpcre3` and are not
supported. Enterprise Linux (RHEL, AlmaLinux, Rocky) and Amazon Linux are not supported either:
they ship PCRE as `libpcre.so.1`. (`libcrypt.so.1`, which the nginx also links, is available
there from `libxcrypt-compat`; PCRE is the blocker.) `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 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

```bash
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`                                  | `WeAmp.PageSpeed.AspNetCore`                              |
| ------------- | ---------------------------------------------------------- | --------------------------------------------------------- |
| 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](https://modpagespeed.com/docs/aspnet-configuration/) — all appsettings.json options, hot reload, environment-specific config
- [ASP.NET Core performance overview](https://modpagespeed.com/aspnet-core-performance/) — what the pipeline optimizes and the results to expect
- [How the ASP.NET Core middleware works](https://modpagespeed.com/blog/aspnet-core-middleware/) — the architecture behind `UsePageSpeed()`
- [Image optimization in C#](https://modpagespeed.com/blog/aspnet-core-image-optimization-c-sharp/) — the content-negotiation path in detail
- Fix [LCP](https://modpagespeed.com/blog/fix-lcp-aspnet-core-2026/), [INP](https://modpagespeed.com/blog/fix-inp-aspnet-core-2026/), and [CLS](https://modpagespeed.com/blog/fix-cls-aspnet-core-2026/) in ASP.NET Core — targeted Core Web Vitals guides
- [Software license](https://modpagespeed.com/license/) — Apache License 2.0
