Configuration Reference
PRISM is configured via a TOML file (default: /etc/prism/config.toml).
Server
[server]
address = "0.0.0.0:4000" # Listen address
origin = "http://localhost:3000" # Your SPA backend
mode = "bot-only" # "bot-only" or "render-all"
shadow = false # Shadow mode: render in background, serve original
drain_timeout_secs = 30 # Graceful shutdown timeout| Option | Type | Default | Description |
|---|---|---|---|
address | string | "0.0.0.0:4000" | Address PRISM listens on |
origin | string | "http://localhost:3000" | Your SPA origin URL |
mode | string | "bot-only" | "bot-only": render only for crawlers. "render-all": render for everyone |
shadow | bool | false | Render in background and log diffs without serving rendered HTML |
drain_timeout_secs | integer | 30 | Seconds to wait for in-flight renders during shutdown |
Mode: bot-only vs render-all
bot-only (default, recommended): Only search engine crawlers and social bots get rendered HTML. Regular users get the normal SPA response. Zero performance impact for human visitors.
render-all: Every request gets rendered HTML. Useful when you want server-side rendering without modifying your SPA code. Higher resource usage -- every page view goes through Chrome.
Shadow Mode
When shadow = true, PRISM renders pages in the background but serves the original (un-rendered) response. It logs any differences between the rendered and original HTML. Useful for testing PRISM before going live.
Render
[render]
wait_for = "load" # When to consider the page "done"
timeout_secs = 10 # Max render time per page
block_resources = [ # Resource types to skip (faster renders)
"font", "image", "media", "stylesheet"
]| Option | Type | Default | Description |
|---|---|---|---|
wait_for | string | "load" | Page readiness signal (see below) |
timeout_secs | integer | 10 | Maximum seconds per render |
block_resources | array | ["font", "image", "media", "stylesheet"] | Chrome resource types to block |
wait_for Options
| Value | Description | Speed |
|---|---|---|
"load" | Wait for window.onload event | Fast, good default |
"domcontentloaded" | Wait for DOM ready (before images) | Fastest |
"networkidle" | Wait until no network activity for 500ms | Slowest, most complete |
"selector:css" | Wait for a CSS selector to appear, e.g., "selector:.product-loaded" | Custom |
Blocking fonts, images, and stylesheets during rendering is safe -- crawlers don't need them. This reduces render time by 30-50% and saves bandwidth.
Chrome Pool
[render.pool]
tabs = 8 # Number of Chrome tabs (parallel renders)
max_renders_per_tab = 50 # Recycle tab after N renders (prevents memory leaks)
queue_max = 100 # Max queued render requests| Option | Type | Default | Description |
|---|---|---|---|
tabs | integer | 8 | Number of concurrent Chrome tabs |
max_renders_per_tab | integer | 50 | Recycle tab after this many renders |
queue_max | integer | 100 | Maximum queued requests (rejects above this) |
Sizing the Pool
| Server RAM | Recommended Tabs | Concurrent Renders |
|---|---|---|
| 1 GB | 2-4 | 2-4 |
| 2 GB | 4-8 | 4-8 |
| 4 GB | 8-16 | 8-16 |
| 8 GB+ | 16-32 | 16-32 |
Each Chrome tab uses approximately 50-150 MB of RAM depending on page complexity.
Circuit Breaker
[render.circuit_breaker]
failure_threshold = 5 # Consecutive failures before opening circuit
recovery_timeout_secs = 30 # Seconds before trying again
half_open_max_requests = 1 # Test requests in half-open stateWhen Chrome fails repeatedly (crashes, timeouts), the circuit breaker opens and stops sending requests to Chrome. After recovery_timeout_secs, it enters half-open state and tests with a single request. If that succeeds, the circuit closes and normal operation resumes.
Cache
[cache]
enabled = true
max_entries = 10000 # Maximum cached pages
max_memory_bytes = 268435456 # 256 MiB memory limit
default_ttl_secs = 3600 # 1 hour default TTL
grace_period_secs = 300 # 5 minutes stale serving
respect_cache_control = false # Respect origin Cache-Control headers| Option | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable in-memory cache |
max_entries | integer | 10000 | Maximum number of cached pages |
max_memory_bytes | integer | 268435456 | Memory limit (256 MiB) |
default_ttl_secs | integer | 3600 | Default time-to-live (1 hour) |
grace_period_secs | integer | 300 | Serve stale content while re-rendering |
respect_cache_control | bool | false | Honor origin's Cache-Control headers |
Request Coalescing
When multiple requests arrive for the same URL simultaneously, PRISM renders it once and serves the result to all waiting requests. This prevents "thundering herd" problems where 100 bot requests for the same page would trigger 100 Chrome renders.
SPA Detection
[detect]
auto = false # Auto-detect if a page is an SPA
max_body_text_bytes = 500 # If body text < 500 bytes, probably an SPA
min_script_tags = 2 # If >= 2 script tags, probably an SPA
mount_points = [ # Common SPA mount point IDs
"app", "root", "__next", "__nuxt"
]
header = "X-Prism-Render" # Origin can request rendering via this headerWhen auto = true, PRISM fetches the page first, inspects the HTML, and decides whether it needs rendering. Useful when PRISM sits in front of a mixed site (some pages are SPAs, some are server-rendered).
Routes
[routes]
include = ["/**"] # Glob patterns to render
exclude = [ # Glob patterns to skip
"/api/**",
"/graphql",
"**/*.js", "**/*.css", "**/*.json",
"**/*.png", "**/*.jpg", "**/*.gif", "**/*.svg", "**/*.ico",
"**/*.woff", "**/*.woff2", "**/*.ttf",
"/_next/**", "/static/**", "/admin/**",
]Routes use glob patterns. A request must match an include pattern AND not match any exclude pattern to be rendered.
CSS, JS, images, and fonts should never go through Chrome. The default excludes cover most cases. Add any custom static paths your app uses.
Bot Detection
[bot]
patterns = [
"Googlebot", "Bingbot", "Yandex", "Baiduspider",
"DuckDuckBot", "Slurp", "facebookexternalhit",
"LinkedInBot", "Twitterbot", "Applebot",
"GPTBot", "ClaudeBot", "ChatGPT-User",
"AhrefsBot", "SemrushBot", "MJ12bot",
# ... 70+ patterns included by default
]Bot detection uses case-insensitive substring matching on the User-Agent header. The default list includes all major search engines, social crawlers, AI bots, and SEO tools.
Admin API
[admin]
enabled = true
address = "127.0.0.1:4001" # Admin listens on separate port
# bearer_token = "your-secret-token"| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check (returns 200 if healthy) |
/status | GET | Uptime, cache stats, pool stats, render counts |
/metrics | GET | Prometheus format metrics |
/purge/url | POST | Purge a single cached URL |
/purge/pattern | POST | Purge by glob pattern |
/purge/all | POST | Flush entire cache |
/render | POST | Manually trigger a render |
Always set bearer_token in production. The admin API can purge your cache and trigger renders.
Security
[security]
allowed_origins = [] # Empty = allow all origins
block_private_cidrs = true # Block SSRF to private networks
rate_limit_per_domain = 10 # Max concurrent renders per domain| Option | Type | Default | Description |
|---|---|---|---|
allowed_origins | array | [] | Restrict which origins PRISM can fetch from |
block_private_cidrs | bool | true | Block renders to private IP ranges (SSRF protection) |
rate_limit_per_domain | integer | 10 | Max concurrent renders per domain |
Disabling SSRF protection allows Chrome to access internal services, databases, and cloud metadata endpoints. Only disable for testing.
Logging
[logging]
level = "info" # debug, info, warn, error
format = "pretty" # "pretty" (human) or "json" (structured)Complete Example
[server]
address = "0.0.0.0:4000"
origin = "https://my-react-app.com"
mode = "bot-only"
[render]
wait_for = "networkidle"
timeout_secs = 15
block_resources = ["font", "image", "media", "stylesheet"]
[render.pool]
tabs = 8
max_renders_per_tab = 50
[cache]
enabled = true
max_entries = 50000
max_memory_bytes = 536870912 # 512 MiB
default_ttl_secs = 7200 # 2 hours
grace_period_secs = 600 # 10 minutes
[routes]
include = ["/**"]
exclude = ["/api/**", "/graphql", "**/*.js", "**/*.css", "**/*.json",
"**/*.png", "**/*.jpg", "**/*.svg", "**/*.ico",
"**/*.woff2", "/_next/**", "/static/**", "/admin/**"]
[admin]
enabled = true
address = "127.0.0.1:4001"
bearer_token = "your-secret-token-here"
[security]
block_private_cidrs = true
rate_limit_per_domain = 10
[logging]
level = "info"
format = "json"