HTTP API Reference
Complete reference for the ModPageSpeed 2.0 worker HTTP API
The Factory Worker exposes an HTTP/1.1 API for monitoring, cache management,
configuration, and browser-based analysis. The web console is built on this API. Every endpoint documented here is available for direct
use with curl, scripts, or custom integrations.
Base URL
The API listens on 127.0.0.1:9880 by default. Configure with --api-port.
Set port to 0 to disable the HTTP API entirely.
http://127.0.0.1:9880/v1/...
All endpoints are prefixed with /v1/. The server supports HTTP/1.1 only
(no HTTP/2). Keep-alive connections are supported.
Server Limits
| Parameter | Default | Flag |
|---|---|---|
| Max connections | 32 | --max-connections |
| Request timeout | 10s | - |
| Max request body | 64 KB | - |
| Max URL length | 8192 | - |
| Max header bytes | 64 KB | - |
Authentication
Authentication is optional. When --api-token TOKEN is set (or the
PAGESPEED_API_TOKEN environment variable), all endpoints except
/v1/health require a Bearer token.
curl -H "Authorization: Bearer my-secret-token" http://localhost:9880/v1/stats
Authentication Modes
| Mode | Behavior |
|---|---|
| No token configured | All endpoints open |
| Token configured | All endpoints require auth (except /v1/health) |
Token + read_open | GET + WebSocket open, POST/PATCH require auth |
In read-open mode (--api-read-open), GET endpoints and WebSocket connections
are accessible without authentication. This enables exposing the console publicly
as a read-only dashboard while keeping mutating operations (config changes, cache
purge) behind the token. Auth for WebSocket is sent as the first message (see
WebSocket Endpoints).
Error Responses
When auth fails, the API returns:
- 401
UNAUTHORIZED— MissingAuthorizationheader or wrong scheme - 403
FORBIDDEN— Invalid token
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
}
CORS
Set --cors-origin to enable cross-origin requests. An empty value (default)
disables CORS headers entirely. Supports a specific origin or * for wildcard.
When auth is configured, the server reflects the actual origin (not *) so
browser credentials work. Preflight OPTIONS requests return CORS headers with
status 204.
Health and Status
GET /v1/health
Health check and readiness probe. No authentication required. Designed for Kubernetes liveness probes, Docker HEALTHCHECK, and load balancer health checks.
curl http://localhost:9880/v1/health
Response:
{
"status": "ok",
"version": "2.0.0-dev",
"uptime_seconds": 3421,
"connections": {
"active": 2,
"max": 32
},
"inflight": 3,
"ready": true,
"license": {
"valid": true,
"error": "",
"plan": "pro",
"sub": "sub_abc123",
"iat": 1700000000,
"expires_at": 1702592000,
"days_remaining": 28
}
}
| Field | Description |
|---|---|
status | Always "ok" when the server is running |
version | Worker build version |
uptime_seconds | Seconds since worker start |
connections.active | Current HTTP connections |
connections.max | Configured maximum |
inflight | Thread pool tasks in progress |
ready | true when inflight < num_threads (capacity available) |
license | License status including plan, expiry, and remaining days |
GET /v1/stats
Full worker statistics as JSON. Covers notifications, variant generation, errors, cache state, processing times, content analysis, and more.
curl http://localhost:9880/v1/stats
Response:
{
"notifications": {
"received": 4821,
"skipped_dedup": 1200,
"skipped_inflight": 42
},
"variants": {
"written": 2106,
"proactive": 1580,
"gzip": 840,
"brotli": 840
},
"errors": {
"total": 7,
"origin_misconfiguration": 2
},
"cache": {
"entries": 3412,
"size_bytes": 268435456
},
"by_type": {
"html": { "count": 120, "time_us": 450000 },
"css": { "count": 340, "time_us": 180000 },
"js": { "count": 280, "time_us": 150000 },
"image": { "count": 1366, "time_us": 28500000 }
},
"by_format": {
"webp": 580,
"avif": 490,
"jpeg": 180,
"png": 116
},
"content_analysis": {
"photo": 800,
"screenshot": 120,
"illustration": 300,
"noisy": 50,
"denoised": 30
},
"ssimulacra2": {
"checks": 1200,
"reencodes": 45,
"avg_score_x100": 7200
},
"learned_quality": {
"predictions": 1100,
"fallbacks": 15
},
"html_assembly": {
"complete": 110,
"skipped": 10,
"css_aborted": 3
},
"alternates": {
"writes": 5000,
"write_failures": 2
},
"selector_invocations": 12000,
"cache_read_retries": 5,
"cache_read_failures": 1,
"svg": {
"candidates_evaluated": 500,
"candidates_rejected": 350,
"vectorized": 120,
"fidelity_rejected": 10,
"size_rejected": 15,
"path_count_rejected": 5,
"written": 90,
"bytes_saved": 2048000,
"vectorize_time_us": 450000,
"served": 75
},
"policy": {
"computed": 85,
"async_css_enabled": 60,
"script_deferral_enabled": 45
},
"thread_pool": {
"inflight": 3,
"size": 8
},
"connections": {
"active": 2,
"max": 32
}
}
Key fields:
| Field | Description |
|---|---|
notifications.received | Total notifications from nginx |
notifications.skipped_dedup | Skipped because variant already exists |
notifications.skipped_inflight | Skipped because URL is already being processed |
variants.written | Total optimized variants written to cache |
variants.proactive | Sibling variants (other format/viewport/density combinations) |
variants.gzip / brotli | Pre-compressed text variants |
errors.origin_misconfiguration | Origin responses that prevent optimization (e.g., no-store) |
by_type.*.time_us | Cumulative processing time in microseconds |
ssimulacra2.avg_score_x100 | Average visual quality score (7200 = 72.00) |
learned_quality.predictions | ML model quality predictions made |
learned_quality.fallbacks | Times ML prediction was overridden by binary search |
svg.bytes_saved | Cumulative bytes saved by SVG vectorization |
policy.computed | Optimization policies computed from browser profiles |
policy.async_css_enabled | Times async CSS was enabled by policy |
policy.script_deferral_enabled | Times script deferral was enabled by policy |
GET /v1/metrics
Worker statistics in Prometheus text exposition format. Suitable for direct Prometheus scraping.
curl http://localhost:9880/v1/metrics
Response (Content-Type: text/plain; version=0.0.4; charset=utf-8):
# HELP pagespeed_notifications_total Total notifications received.
# TYPE pagespeed_notifications_total counter
pagespeed_notifications_total 4821
# HELP pagespeed_variants_written_total Total variants written to cache.
# TYPE pagespeed_variants_written_total counter
pagespeed_variants_written_total 2106
# HELP pagespeed_cache_entries Current number of cache entries.
# TYPE pagespeed_cache_entries gauge
pagespeed_cache_entries 3412
...
All counters and gauges from /v1/stats are exposed as Prometheus metrics,
including SVG stats, learned quality, content analysis, and browser analysis
counters.
Configuration
GET /v1/config
Returns the full worker configuration. The license key is redacted to "***".
curl http://localhost:9880/v1/config
Response:
{
"jpeg_quality": 85,
"webp_quality": 75,
"avif_quality": 60,
"mobile_width": 480,
"tablet_width": 768,
"desktop_width": 0,
"gzip_level": 6,
"brotli_level": 6,
"disable_async_css": false,
"disable_script_deferral": false,
"svg_mode": "detect",
"svg_candidacy_threshold": 50,
"svg_max_pixels": 65536,
"license_key": "***",
"license_renewal_url": "",
...
}
All worker configuration fields are included. See the Configuration Reference for field descriptions.
PATCH /v1/config
Update hot-reloadable configuration fields at runtime. Only fields included in the request body are changed. Changes are persisted to disk alongside the cache volume.
curl -X PATCH http://localhost:9880/v1/config \
-H "Content-Type: application/json" \
-d '{"jpeg_quality": 80, "svg_mode": "auto"}'
Request body: JSON object with one or more config fields to update.
Response:
{
"applied": {
"jpeg_quality": 80,
"svg_mode": "auto"
},
"rejected": {},
"warnings": [],
"config": { ... }
}
| Field | Description |
|---|---|
applied | Fields that were accepted and applied |
rejected | Fields that failed validation, with error messages |
warnings | Non-fatal warnings about the applied configuration |
config | Full configuration after applying changes |
Fields that fail validation are rejected individually. Valid fields in the same
request are still applied. All boolean toggle fields (disable_async_css,
disable_script_deferral, etc.) are hot-reloadable via PATCH /v1/config.
{
"applied": {
"jpeg_quality": 80
},
"rejected": {
"jpeg_quality_invalid": "unknown field"
},
"warnings": [],
"config": { ... }
}
Cache Operations
GET /v1/cache/urls
Paginated list of all cached URLs. The worker tracks URLs in memory as they are processed.
curl "http://localhost:9880/v1/cache/urls?offset=0&limit=50"
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
offset | int | 0 | Skip this many entries |
limit | int | 100 | Max entries to return (capped at 1000) |
hostname | string | - | Filter by hostname |
Response:
{
"urls": [
{
"url": "/images/hero.jpg",
"hostname": "example.com",
"alternate_count": 12
},
{
"url": "/style.css",
"hostname": "example.com",
"alternate_count": 4
}
],
"offset": 0,
"limit": 50,
"next_offset": 50,
"has_more": true,
"total": 342
}
GET /v1/cache/alternates
List all stored variants (alternates) for a URL. Each alternate represents one optimized version: a specific combination of image format, viewport, pixel density, Save-Data preference, and transfer encoding.
curl "http://localhost:9880/v1/cache/alternates?url=/images/hero.jpg&hostname=example.com"
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL path |
hostname | string | No | Hostname for cache key |
Response:
{
"url": "/images/hero.jpg",
"hostname": "example.com",
"count": 5,
"alternates": [
{
"alternate_id": 8,
"size": 145200,
"hit_count": 42,
"is_sentinel": false,
"mask": {
"raw": 8,
"format": "original",
"viewport": "desktop",
"density": "1x",
"save_data": false,
"encoding": "identity"
},
"format": "original",
"viewport": "desktop",
"density": "1x",
"save_data": false,
"encoding": "identity",
"content_type": "image",
"origin_content_type": "image/jpeg",
"flags": 0,
"needs_revalidation": false,
"last_access": 1700000000000,
"cache_inserted_at": 1699999000,
"origin_max_age": 86400,
"origin_s_maxage": 0,
"origin_cc_flags": 0,
"version": 5,
"ssimulacra2_score": 82.5,
"content_class": "photo"
},
{
"alternate_id": 9,
"size": 58400,
"hit_count": 120,
"is_sentinel": false,
"mask": {
"raw": 9,
"format": "webp",
"viewport": "desktop",
"density": "1x",
"save_data": false,
"encoding": "identity"
},
"format": "webp",
"viewport": "desktop",
"content_type": "image",
"origin_content_type": "image/jpeg",
"ssimulacra2_score": 75.2,
"content_class": "photo",
"..."
}
],
"cooldown": {
"reason": "revalidation",
"remaining_seconds": 2,
"duration_seconds": 3
}
}
Sentinel entries (Early Hints, warmup, browser profile) are included with
is_sentinel: true and a sentinel_name field.
The optional cooldown object appears when the URL is in a processing cooldown
period. See Cooldowns for details.
GET /v1/cache/select
Simulate variant selection for a given capability mask. Shows how the selector scores each alternate and which one wins. Useful for debugging why a client receives a particular variant.
curl "http://localhost:9880/v1/cache/select?url=/images/hero.jpg&hostname=example.com&mask=9"
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL path |
hostname | string | No | Hostname for cache key |
mask | int | Yes | 32-bit capability mask (decimal) |
The mask parameter is the decimal representation of the
capability bitmask. Common
values: 8 (Desktop/Original), 9 (Desktop/WebP), 10 (Desktop/AVIF).
Response:
{
"url": "/images/hero.jpg",
"hostname": "example.com",
"client_mask": {
"raw": 9,
"format": "webp",
"viewport": "desktop",
"density": "1x",
"save_data": false,
"encoding": "identity"
},
"selected": {
"alternate_id": 9,
"score": 1280,
"size": 58400,
"content_type": "image"
},
"all_scores": [
{
"alternate_id": 8,
"score": 100,
"mask": { "format": "original", "..." },
"content_type": "image"
},
{
"alternate_id": 9,
"score": 1280,
"mask": { "format": "webp", "..." },
"content_type": "image"
}
]
}
GET /v1/cache/content
Retrieve the raw binary content of a specific alternate. Returns the actual optimized content (image bytes, minified CSS, etc.) as a binary blob.
curl "http://localhost:9880/v1/cache/content?url=/images/hero.jpg&hostname=example.com&alternate_id=9" \
-o hero.webp
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL path |
hostname | string | No | Hostname for cache key |
alternate_id | int | Yes | Alternate ID (low 8 bits of capability mask) |
Response: Raw binary content with appropriate Content-Type header. The
response includes Content-Disposition: attachment and security headers. SVG
content includes a restrictive Content-Security-Policy.
Response headers:
Content-Type: image/webp
Content-Disposition: attachment
X-Content-Type-Options: nosniff
X-Alternate-Id: 9
X-Capability-Mask: 9
POST /v1/cache/purge
Delete cached variants for a URL, or purge the entire cache.
Single URL purge:
curl -X POST http://localhost:9880/v1/cache/purge \
-H "Content-Type: application/json" \
-d '{"url": "/images/hero.jpg", "hostname": "example.com"}'
Response:
{
"url": "/images/hero.jpg",
"hostname": "example.com",
"deleted": 12
}
When deleted is 0, the response includes a note explaining the URL was not
found in cache.
Full cache purge:
Purge all cached content. This stops the cache, deletes the volume file, and recreates it from scratch. Requires explicit confirmation.
curl -X POST http://localhost:9880/v1/cache/purge \
-H "Content-Type: application/json" \
-d '{"scope": "all", "confirm": "purge-all"}'
Response:
{
"scope": "all",
"urls_cleared": 342,
"volume_reset": true
}
Omitting the confirm field or providing the wrong value returns a 400 error.
POST /v1/cache/reprocess
Clear the dedup set and cooldown for a URL, then enqueue it for reprocessing. Unlike purge, this does not delete the cached content. The worker re-reads the original from cache and regenerates all optimized variants.
curl -X POST http://localhost:9880/v1/cache/reprocess \
-H "Content-Type: application/json" \
-d '{"url": "/style.css", "hostname": "example.com"}'
Response:
{
"url": "/style.css",
"hostname": "example.com",
"reprocess_enqueued": true
}
GET /v1/cache/cooldowns
List all URLs currently in a processing cooldown. Cooldowns prevent tight re-processing loops for the same URL.
curl http://localhost:9880/v1/cache/cooldowns
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
hostname | string | No | Filter by hostname |
Response:
{
"cooldowns": [
{
"url": "/index.html",
"hostname": "example.com",
"reason": "revalidation",
"remaining_seconds": 2,
"duration_seconds": 3
},
{
"url": "/about.html",
"hostname": "example.com",
"reason": "tentative",
"remaining_seconds": 45,
"duration_seconds": 60
}
],
"count": 2,
"enabled": true
}
Cooldown reasons:
| Reason | Duration | Description |
|---|---|---|
tentative | 60s | Processing in progress; prevents concurrent threads from duplicating work |
write_failure | 60s | Cache write failed (reader contention on mmap handles) |
revalidation | 3s | HTML written with kFlagNeedsRevalidation; throttles CSS convergence loop |
Browser Capture
These endpoints require --enable-browser-analysis and a running Chrome
instance. If Chrome is unavailable, the API returns 503.
POST /v1/capture/waterfall
Capture a network waterfall (HAR-like timing data) for a URL using headless Chrome.
curl -X POST http://localhost:9880/v1/capture/waterfall \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "viewport_width": 1280}'
Request body:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | (required) | Target URL (http or https) |
viewport_width | int | 1280 | Browser viewport width (320-3840) |
Response:
{
"entries": [
{
"url": "https://example.com/",
"resource_type": "document",
"status": 200,
"transfer_size": 12400,
"decoded_size": 45000,
"timing": { "..." }
}
],
"navigation_timing": { "..." },
"total_transfer_size": 580000,
"total_decoded_size": 1200000
}
POST /v1/capture/screenshot
Capture a PNG screenshot of a URL using headless Chrome.
curl -X POST http://localhost:9880/v1/capture/screenshot \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "viewport_width": 1440, "full_page": true}'
Request body:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | (required) | Target URL (http or https) |
viewport_width | int | 1280 | Browser viewport width (320-3840) |
viewport_height | int | 800 | Browser viewport height (200-10000) |
full_page | bool | false | Capture entire page (not just viewport) |
Response:
{
"png_base64": "iVBORw0KGgoAAAANSUhEUgAA..."
}
The png_base64 field contains the screenshot as a base64-encoded PNG image.
Browser Capture Errors
| Status | Condition |
|---|---|
| 400 | Invalid URL, missing url field, viewport out of range |
| 400 | URL targets private/loopback address (unless allowed) |
| 503 | --enable-browser-analysis not set |
| 503 | Chrome not running (crashed or still starting) |
| 504 | Navigation timed out |
WebSocket Endpoints
WebSocket endpoints provide real-time streaming data. All three endpoints follow the same connection lifecycle:
- Client connects via HTTP upgrade to the WebSocket path
- If auth is configured, client sends
{"auth": "my-token"}as first message - Server sends initial snapshot
- Server pushes updates at regular intervals
Connection Limits
| Parameter | Value |
|---|---|
| Max connections per endpoint | 8 |
| Ping interval | 30s |
| Pong timeout | 10s |
| Auth timeout | 2s |
GET /v1/ws/stats
Live server statistics with delta compression. Sends a full snapshot on connect, then periodic deltas containing only changed fields.
const ws = new WebSocket('ws://localhost:9880/v1/ws/stats');
// If auth is configured:
ws.onopen = () => ws.send(JSON.stringify({ auth: 'my-token' }));
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'snapshot') {
// Full stats object (same schema as GET /v1/stats)
console.log('Full stats:', msg.data);
} else if (msg.type === 'delta') {
// Only fields that changed since last message
console.log('Changed:', msg.data);
}
};
Server messages:
| Type | Description |
|---|---|
snapshot | Full stats on connect: {type: "snapshot", sequence: N, data: StatsResponse} |
delta | Changed fields only: {type: "delta", sequence: N, data: Partial<StatsResponse>} |
Client messages:
| Message | Description |
|---|---|
{"auth": "token"} | Authenticate (required when auth is configured) |
{"set_interval_ms": N} | Change push interval (100ms - 60000ms, default 1000ms) |
Sequence numbers increment per message for loss detection. Delta compression reduces typical message size from ~50 KB to ~1 KB.
GET /v1/ws/events
Real-time optimization events. Events are batched at 100ms intervals.
const ws = new WebSocket('ws://localhost:9880/v1/ws/events');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'event') {
console.log(msg.event_type, msg.detail);
} else if (msg.type === 'overflow') {
console.log('Dropped', msg.dropped_count, 'events');
}
};
Server messages:
| Type | Description |
|---|---|
event | {type: "event", sequence: N, timestamp: MS, event_type: "...", detail: {...}} |
overflow | {type: "overflow", dropped_count: N} — oldest events dropped when buffer exceeds 64 KB |
Event types:
| Event | Description |
|---|---|
svg_candidacy | Image evaluated for SVG vectorization |
svg_vectorized | SVG vectorization completed |
svg_written | SVG variant written to cache |
GET /v1/ws/logs
Real-time log streaming. Sends the ring buffer contents (up to 2000 entries) on connect, then streams new log entries as they occur.
const ws = new WebSocket('ws://localhost:9880/v1/ws/logs');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'snapshot') {
console.log('History:', msg.entries.length, 'entries');
} else if (msg.type === 'log') {
console.log(`[${msg.level}] ${msg.module}: ${msg.message}`);
}
};
Server messages:
| Type | Description |
|---|---|
snapshot | Ring buffer on connect: {type: "snapshot", entries: LogEntry[], total: N} |
log | Single log entry: {type: "log", timestamp: MS, source: "...", level: "...", module: "...", message: "...", details?: {...}} |
overflow | {type: "overflow", dropped_count: N} |
Log sources: worker, cache, chrome
Log levels: debug, info, warning, error
Static Files
GET /console/*
Serves the web console SPA (SvelteKit build). Content-Type is auto-detected
from the file extension. Configure the console directory with --console-dir.
curl http://localhost:9880/console/
This endpoint is not under the /v1/ prefix. It serves static files and does
not return JSON.
Error Response Format
All API errors follow a consistent JSON structure:
{
"error": {
"code": "BAD_REQUEST",
"message": "Missing 'url' query parameter"
}
}
Some errors include a details field with additional context:
{
"error": {
"code": "BAD_REQUEST",
"message": "Validation failed",
"details": { "..." }
}
}
Error Codes
| HTTP Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | Invalid request (missing parameters, malformed JSON) |
| 401 | UNAUTHORIZED | Missing or invalid auth scheme |
| 403 | FORBIDDEN | Invalid token |
| 404 | NOT_FOUND | Endpoint or resource not found |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method for endpoint |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds 64 KB |
| 429 | TOO_MANY_REQUESTS | Connection limit reached |
| 500 | INTERNAL_ERROR | Server-side failure |
| 503 | SERVICE_UNAVAILABLE | Feature not enabled or dependency unavailable |
| 504 | GATEWAY_TIMEOUT | Request timed out |
Endpoint Summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/health | No | Health check and readiness |
| GET | /v1/stats | Yes | Full statistics (JSON) |
| GET | /v1/metrics | Yes | Statistics (Prometheus format) |
| GET | /v1/config | Yes | Current configuration |
| PATCH | /v1/config | Yes | Update configuration |
| GET | /v1/cache/urls | Yes | Paginated cached URL list |
| GET | /v1/cache/alternates | Yes | List variants for a URL |
| GET | /v1/cache/select | Yes | Simulate variant selection |
| GET | /v1/cache/content | Yes | Raw alternate content |
| POST | /v1/cache/purge | Yes | Purge URL or full cache |
| POST | /v1/cache/reprocess | Yes | Re-optimize a URL |
| GET | /v1/cache/cooldowns | Yes | List active cooldowns |
| POST | /v1/capture/waterfall | Yes | Network waterfall capture |
| POST | /v1/capture/screenshot | Yes | Page screenshot capture |
| WS | /v1/ws/stats | Yes | Live stats streaming |
| WS | /v1/ws/events | Yes | Real-time optimization events |
| WS | /v1/ws/logs | Yes | Real-time log streaming |
| GET | /console/* | No | Web console static files |
Next Steps
- Web Console — The console is built on this API — see it in action
- Configuration Reference — Worker flags and nginx directives
- API Reference — IPC wire format, management socket, capability mask encoding, and C API
- Browser Analysis — How the capture endpoints use headless Chrome
- Troubleshooting — Common issues and diagnostic techniques