Skip to main content

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

ParameterDefaultFlag
Max connections32--max-connections
Request timeout10s-
Max request body64 KB-
Max URL length8192-
Max header bytes64 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

ModeBehavior
No token configuredAll endpoints open
Token configuredAll endpoints require auth (except /v1/health)
Token + read_openGET + 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 — Missing Authorization header 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
  }
}
FieldDescription
statusAlways "ok" when the server is running
versionWorker build version
uptime_secondsSeconds since worker start
connections.activeCurrent HTTP connections
connections.maxConfigured maximum
inflightThread pool tasks in progress
readytrue when inflight < num_threads (capacity available)
licenseLicense 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:

FieldDescription
notifications.receivedTotal notifications from nginx
notifications.skipped_dedupSkipped because variant already exists
notifications.skipped_inflightSkipped because URL is already being processed
variants.writtenTotal optimized variants written to cache
variants.proactiveSibling variants (other format/viewport/density combinations)
variants.gzip / brotliPre-compressed text variants
errors.origin_misconfigurationOrigin responses that prevent optimization (e.g., no-store)
by_type.*.time_usCumulative processing time in microseconds
ssimulacra2.avg_score_x100Average visual quality score (7200 = 72.00)
learned_quality.predictionsML model quality predictions made
learned_quality.fallbacksTimes ML prediction was overridden by binary search
svg.bytes_savedCumulative bytes saved by SVG vectorization
policy.computedOptimization policies computed from browser profiles
policy.async_css_enabledTimes async CSS was enabled by policy
policy.script_deferral_enabledTimes 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": { ... }
}
FieldDescription
appliedFields that were accepted and applied
rejectedFields that failed validation, with error messages
warningsNon-fatal warnings about the applied configuration
configFull 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:

ParameterTypeDefaultDescription
offsetint0Skip this many entries
limitint100Max entries to return (capped at 1000)
hostnamestring-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:

ParameterTypeRequiredDescription
urlstringYesThe URL path
hostnamestringNoHostname 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:

ParameterTypeRequiredDescription
urlstringYesThe URL path
hostnamestringNoHostname for cache key
maskintYes32-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:

ParameterTypeRequiredDescription
urlstringYesThe URL path
hostnamestringNoHostname for cache key
alternate_idintYesAlternate 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:

ParameterTypeRequiredDescription
hostnamestringNoFilter 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:

ReasonDurationDescription
tentative60sProcessing in progress; prevents concurrent threads from duplicating work
write_failure60sCache write failed (reader contention on mmap handles)
revalidation3sHTML 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:

FieldTypeDefaultDescription
urlstring(required)Target URL (http or https)
viewport_widthint1280Browser 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:

FieldTypeDefaultDescription
urlstring(required)Target URL (http or https)
viewport_widthint1280Browser viewport width (320-3840)
viewport_heightint800Browser viewport height (200-10000)
full_pageboolfalseCapture 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

StatusCondition
400Invalid URL, missing url field, viewport out of range
400URL targets private/loopback address (unless allowed)
503--enable-browser-analysis not set
503Chrome not running (crashed or still starting)
504Navigation timed out

WebSocket Endpoints

WebSocket endpoints provide real-time streaming data. All three endpoints follow the same connection lifecycle:

  1. Client connects via HTTP upgrade to the WebSocket path
  2. If auth is configured, client sends {"auth": "my-token"} as first message
  3. Server sends initial snapshot
  4. Server pushes updates at regular intervals

Connection Limits

ParameterValue
Max connections per endpoint8
Ping interval30s
Pong timeout10s
Auth timeout2s

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:

TypeDescription
snapshotFull stats on connect: {type: "snapshot", sequence: N, data: StatsResponse}
deltaChanged fields only: {type: "delta", sequence: N, data: Partial<StatsResponse>}

Client messages:

MessageDescription
{"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:

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

EventDescription
svg_candidacyImage evaluated for SVG vectorization
svg_vectorizedSVG vectorization completed
svg_writtenSVG 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:

TypeDescription
snapshotRing buffer on connect: {type: "snapshot", entries: LogEntry[], total: N}
logSingle 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 StatusCodeDescription
400BAD_REQUESTInvalid request (missing parameters, malformed JSON)
401UNAUTHORIZEDMissing or invalid auth scheme
403FORBIDDENInvalid token
404NOT_FOUNDEndpoint or resource not found
405METHOD_NOT_ALLOWEDWrong HTTP method for endpoint
413PAYLOAD_TOO_LARGERequest body exceeds 64 KB
429TOO_MANY_REQUESTSConnection limit reached
500INTERNAL_ERRORServer-side failure
503SERVICE_UNAVAILABLEFeature not enabled or dependency unavailable
504GATEWAY_TIMEOUTRequest timed out

Endpoint Summary

MethodPathAuthDescription
GET/v1/healthNoHealth check and readiness
GET/v1/statsYesFull statistics (JSON)
GET/v1/metricsYesStatistics (Prometheus format)
GET/v1/configYesCurrent configuration
PATCH/v1/configYesUpdate configuration
GET/v1/cache/urlsYesPaginated cached URL list
GET/v1/cache/alternatesYesList variants for a URL
GET/v1/cache/selectYesSimulate variant selection
GET/v1/cache/contentYesRaw alternate content
POST/v1/cache/purgeYesPurge URL or full cache
POST/v1/cache/reprocessYesRe-optimize a URL
GET/v1/cache/cooldownsYesList active cooldowns
POST/v1/capture/waterfallYesNetwork waterfall capture
POST/v1/capture/screenshotYesPage screenshot capture
WS/v1/ws/statsYesLive stats streaming
WS/v1/ws/eventsYesReal-time optimization events
WS/v1/ws/logsYesReal-time log streaming
GET/console/*NoWeb console static files

Next Steps