polaris-handbook.before.md β†’ polaris-handbook.after.md  Β·  β–  added   β–  removed Β·  ⏱ β€”

Polaris Platform β€” Operations Handbook

Audience: on-call engineers and release managers. Status: living document, reviewed eachevery quarter.

Polaris is the internal service that recieves,receives, renders, and delivers documentation builds to every downstream team. This handbook is the single reference for operating it in production: how it is structured, how to deploy a change, and what to do when something breaks at 3 a.m.

Contents

Overview

Polaris ingests Markdown from roughly forty repositories, renders it to HTML, and serves the result behind a thinsmall caching layer. The render path is deliberately stateless so that any nodeworker can servepick up any request,job from the shared queue, and a build is never tied to the machine that produced it.

The platform has three moving parts: an ingest worker that watches thepolls each source repositories,repository for new commits, a render pool that turns Markdown into HTML, and an edge tier that cacheskeeps rendered pages hot in cache and serves the finished pageshands them straight back to readers.every reader on request. Each part can be operated, scaled, and rolled back on its own. Splitting them this way keeps a renderer crash from ever touching ingest.

At steady state Polaris serves a few million page views a day across the three regions, with the bulk concentrated in the hour after each Monday release. None of that traffic ever reaches a renderer when the cache is warm, which is exactly why the edge tier, not the render pool, is sized for the worst case.

The edge tier is what absorbssoaks up that Monday surge, and it does so without ever waking a renderer. A warm cache turns almost every request into a single in-memory lookup, so the median page leaves the edge in well under a millisecond. When a purge emptiesclears part of the cache, the affected routes briefly fall through to the render pool until they are repopulated.warmed again. That brief window is the only time readers and renderers share a fate, which is exactly why purges are scheduled and never casual. Capacity is therefore sized foraround the cache, not the render pool, and the forecast is revisited at every release.release review.

Architecture

Each request enters through the edge tier, which checks its cache first and only falls through to a renderer on a miss. Renderers are completely interchangeable; they hold no session state, so the pool can be scaled up or drained at will without coordinating between nodes or warming anything in advance.

Every request arrives at the edge tier, which consults its cache first and only drops through to a renderer on a miss. Renderers are entirely fungible; they keep no session state, so the fleet can be grown or drained on demand without any coordination between nodes or warming work done beforehand.

Renderers are cattle, not pets. If one misbehaves, terminate it β€” the pool replaces it within a minute and no request is lost.

The edge tier is the only component readers ever talk to directly, and it is sized for peak fan-out rather than average load so a thundering herd after a cache purge never reaches the renderers.

The ingest worker is the only stateful component, and it keeps nothing more than a cursor into each repository's commit history. A poisoned commit therefore blocks only that one repository, never the whole queue.

Deploying a changerelease

Deployment is a strictly ordered process. Do not skip steps, and never deploy to all regions at once.

  1. Cut a release branch and tag it polaris-vX.Y.Z.
  2. Run the full test suite and confirm the smoke check is green.
  3. Replay the last quarter of production traffic against the candidate build and block the release on any regression it surfaces.
  4. Promote the build to the staging edge and watch the dashboards.
  5. Promote to production one region at a time, starting with us-west., and pause for a full health check between each region before continuing.
  6. Tag the release as verified once the canary has cleared everywhere.
  7. Update the status page once every region reports healthy.

Promotion to a single region itself has two phases:

A region only counts as promoted once its gate clears:

Definition of GREENSTABLE. A deploy is GREENSTABLE β€” not a status the platform reports, but our own composite gate, owned by release-gate.py (--require-greenstable mode). It is βœ… GREENSTABLE only when all four checks pass:

  1. the new revision answers /healthz on every renderer;
  2. error rate over the last 5 minutes is below 0.1%;
  3. p99 latency is within 10% of the previous release;
  4. no renderer has restarted in the trailing 10 minutes.

It means the rollout is GREENSTABLE enough to leave unattended. The soak window and the slow-burn alert threshold must also clear first, so STABLE marks the fully-settled end state, not the first passing check. Nothing downstream re-checks once it is GREENSTABLE.

Configuration

The render pool is tuned with a small set of environment variables. The defaults are conservative; raisechange them only with evidence from the dashboards.after a load test, never on a hunch.

VariableDefaultPurposeRestart needed?
RENDER_CONCURRENCY8Renders running per nodeYes
CACHE_TTL_SECONDS300600How long the edge keeps a pageNo
INGEST_INTERVAL30Seconds between repository pollsNo
LOG_LEVELinfoVerbosity of render logsNo

Provisioning channels

A render setting reaches a node through one of two channels. Both carry the same keys; they differ only in how far the value reaches once it lands.

ChannelWhere the value livesScope
Cluster defaultpolaris.yaml under render.* (in our deployment: ops/polaris/regions/us-west/polaris.yaml β†’ render.concurrency)Cluster-wide default for every renderer in the region.
Per-deploy overridePassed to pol deploy --set render.concurrency=N at promote time, then recorded in the deploy log under that release tagPer-deploy / per-region. Overrides the cluster defaultOverrides the cluster default for that one promotion.

Scheduler parameters

The render scheduler decides how many builds a node works on at once and how it sheds load when the queue backs up. These knobs are read at startup; a couple are re-read on SIGHUP.

ParameterDefaultHot-reloadDescription
render_batch_window250msyesHow long the scheduler waits to accumulate jobs before dispatching a batch to a worker. A longer window improves batching efficiency but adds latency to the first render in eachthe batch.
max_render_batch1624noMaximum number of builds dispatched to a single worker in one batch. Bounds peak memory per worker; raising it trades memory for throughput. What changes as the batch fills:

PhaseWhat still happensWhat changes
FillingThe scheduler keeps adding jobs to the open batch as workers report free slots.Instead of dispatching when the time window expires, it now waits until the batch reaches its max size or the window expires, whichever comes first.
DrainingWorkers render the batch and report progress back to the scheduler.No longer capped at the window. The scheduler holds new jobs until at least one worker frees a slot, so a single slow build no longer stalls the whole node.
queue_backpressuresoftyesHow the scheduler reacts when the pending queue passes its high-water mark. soft slows ingest polling; hard rejects new jobs outright until the queue drains.

Capacity planning

Capacity is owned by the on-call lead and revisited at every release review.

Headroom is tracked against the rolling Monday peak; we provision for twice that peak, which has comfortably and reliably absorbed every surge so far this year.

Scaling is still manual: an operator adds nodes to the render pool and then waits a good while for them to warm.

The underlying forecast is rough, frankly stale, only loosely tuned, and openly optimistic about cache behaviour.

Storage grows slowly but predictably and for the most part needs almost no attention day to day.

For now the review itself is informal and quick in practice.

For now the review itself is formal, scheduled, and surprisingly thorough in practice.

Cache warm-up

Before a deploy drains the old pool, warm the new one. The runner walks a fallback chain top-down and stops at the first source that answers.

The warm-up runner keeps a small self-audit so a route is never pre-rendered into the wrong bucket. It walks the fallback chain in strict top-down order, records which step answered, and scores every route by hit rate before the deploy report is written, so a misfiled route is caught here rather than chased down later under real load. Two buckets that must stay clean (the pinned-override set plus the last-good set, hit rate above ninety percent), and the rest of the sampled tiers still re-checked by hand (window length, route weighting, freshness flag, edge skew), pinned as open gaps, each one still flaky.(hit rate above ninety percent β€” the pinned-override set, the last-good set, and the five sampler.* tiers: window length, route weighting, freshness flag, edge skew, and cold-start bias), and STALE_GUARD β€” sampled tiers still re-checked by hand β€” which is now empty. Clearing one raises its hit rate and flips its row GREEN,When a tier turns flaky, add it to STALE_GUARD pinned at hitRate < 0.5, the cue to fold it into the clean bucket and drop the row,bucket, so coverage is driven through this ledger rather than chased by hand.

Render benchmarks

Throughput is measured per node at steady state, p50 renders per second. These profiles anchor the capacity forecast above.

Each profile is captured by the same overnight sweep:

ProfileConcurrencyRenders/sec
Baseline8120
Tuned16205240
Burst24300

Client-observed latency from the same load test β€” the new Burst profile is now our pick, so Tuned is our current pick, so it isno longer highlighted:

MetricBaseline (c=8)Tuned (c=16)Burst (c=24)
p50 latency180 ms96 ms71 ms
p95 latency540 ms310 ms228 ms
Peak memory2.1 GB3.4 GB5.2 GB

Baseline profile

The conservative default that ships out of the box β€” safe on the smallest node size and the right place to start a new region before there is any load history.

Setting Value
RENDER_CONCURRENCY 8
CACHE_TTL_SECONDS 300

Tuned profile

Raised concurrency for the Monday peak, paired with a longer cache TTL so the edge absorbs more of the surge before a renderer is ever touched.

SettingValue
RENDER_CONCURRENCY16
CACHE_TTL_SECONDS600900

Incident response

When paged, work the list from the top β€” the first matching cause is almost always the real one.

Open the incident channel before you start poking β€” a silent fix that works is still an incident nobody can learn from later.

CLI reference

Operators drive Polaris through the pol command. The most common calls:

pol status --region us-west
pol drain renderer-712
pol cache purge /docs/handbook
pol tail renderer-12 --since 5m

A dry run prints the plan without executing it:

pol deploy v1.4.0 --dry-run

Every pol subcommand accepts --json for machine-readable output, which is handy when you are scripting against it from a notebook.

Service tiers

Not every consumer gets the same guarantees. Tiers are assigned at onboarding and reviewed whenever a team's traffic profile changes.

TierAvailabilitySupport
Platinum99.95%24/7 paging
Gold99.9%BusinessExtended hours
Bronze99.0%Best effort

Deprecations

The legacy polctl shim is scheduled for removal. Migrate to pol before the next major release; the two share no flags, so the move is not automatic.

Observability

Every render emits a structured span, so a slow page can be traced end to end from the edge hit down to the Markdown lexer. Spans are sampled at one percent in steady state and at one hundred percent during a deploy window.

polctl --legacy-status
pol trace --slowest 10 --window 15m

Glossary

A few terms recur throughout this handbook, and it is worth pinning them down so that an incident call does not stall on vocabulary:

References

Further reading lives in the wiki:

polaris-handbook.before.md

Polaris Platform β€” Operations Handbook

Audience: on-call engineers and release managers. Status: living document, reviewed eachevery quarter.

Polaris is the internal service that recieves,receives, renders, and delivers documentation builds to every downstream team. This handbook is the single reference for operating it in production: how it is structured, how to deploy a change, and what to do when something breaks at 3 a.m.

Contents

Overview

Polaris ingests Markdown from roughly forty repositories, renders it to HTML, and serves the result behind a thinsmall caching layer. The render path is deliberately stateless so that any nodeworker can servepick up any request,job from the shared queue, and a build is never tied to the machine that produced it.

The platform has three moving parts: an ingest worker that watches thepolls each source repositories,repository for new commits, a render pool that turns Markdown into HTML, and an edge tier that cacheskeeps rendered pages hot in cache and serves the finished pageshands them straight back to readers.every reader on request. Each part can be operated, scaled, and rolled back on its own. Splitting them this way keeps a renderer crash from ever touching ingest.

At steady state Polaris serves a few million page views a day across the three regions, with the bulk concentrated in the hour after each Monday release. None of that traffic ever reaches a renderer when the cache is warm, which is exactly why the edge tier, not the render pool, is sized for the worst case.

The edge tier is what absorbssoaks up that Monday surge, and it does so without ever waking a renderer. A warm cache turns almost every request into a single in-memory lookup, so the median page leaves the edge in well under a millisecond. When a purge emptiesclears part of the cache, the affected routes briefly fall through to the render pool until they are repopulated.warmed again. That brief window is the only time readers and renderers share a fate, which is exactly why purges are scheduled and never casual. Capacity is therefore sized foraround the cache, not the render pool, and the forecast is revisited at every release.release review.

Architecture

Each request enters through the edge tier, which checks its cache first and only falls through to a renderer on a miss. Renderers are completely interchangeable; they hold no session state, so the pool can be scaled up or drained at will without coordinating between nodes or warming anything in advance.

Every request arrives at the edge tier, which consults its cache first and only drops through to a renderer on a miss. Renderers are entirely fungible; they keep no session state, so the fleet can be grown or drained on demand without any coordination between nodes or warming work done beforehand.

Renderers are cattle, not pets. If one misbehaves, terminate it β€” the pool replaces it within a minute and no request is lost.

The edge tier is the only component readers ever talk to directly, and it is sized for peak fan-out rather than average load so a thundering herd after a cache purge never reaches the renderers.

The ingest worker is the only stateful component, and it keeps nothing more than a cursor into each repository's commit history. A poisoned commit therefore blocks only that one repository, never the whole queue.

Deploying a changerelease

Deployment is a strictly ordered process. Do not skip steps, and never deploy to all regions at once.

  1. Cut a release branch and tag it polaris-vX.Y.Z.
  2. Run the full test suite and confirm the smoke check is green.
  3. Replay the last quarter of production traffic against the candidate build and block the release on any regression it surfaces.
  4. Promote the build to the staging edge and watch the dashboards.
  5. Promote to production one region at a time, starting with us-west., and pause for a full health check between each region before continuing.
  6. Tag the release as verified once the canary has cleared everywhere.
  7. Update the status page once every region reports healthy.

Promotion to a single region itself has two phases:

A region only counts as promoted once its gate clears:

Definition of GREENSTABLE. A deploy is GREENSTABLE β€” not a status the platform reports, but our own composite gate, owned by release-gate.py (--require-greenstable mode). It is βœ… GREENSTABLE only when all four checks pass:

  1. the new revision answers /healthz on every renderer;
  2. error rate over the last 5 minutes is below 0.1%;
  3. p99 latency is within 10% of the previous release;
  4. no renderer has restarted in the trailing 10 minutes.

It means the rollout is GREENSTABLE enough to leave unattended. The soak window and the slow-burn alert threshold must also clear first, so STABLE marks the fully-settled end state, not the first passing check. Nothing downstream re-checks once it is GREENSTABLE.

Configuration

The render pool is tuned with a small set of environment variables. The defaults are conservative; raisechange them only with evidence from the dashboards.after a load test, never on a hunch.

VariableDefaultPurposeRestart needed?
RENDER_CONCURRENCY8Renders running per nodeYes
CACHE_TTL_SECONDS300600How long the edge keeps a pageNo
INGEST_INTERVAL30Seconds between repository pollsNo
LOG_LEVELinfoVerbosity of render logsNo

Provisioning channels

A render setting reaches a node through one of two channels. Both carry the same keys; they differ only in how far the value reaches once it lands.

ChannelWhere the value livesScope
Cluster defaultpolaris.yaml under render.* (in our deployment: ops/polaris/regions/us-west/polaris.yaml β†’ render.concurrency)Cluster-wide default for every renderer in the region.
Per-deploy overridePassed to pol deploy --set render.concurrency=N at promote time, then recorded in the deploy log under that release tagPer-deploy / per-region. Overrides the cluster defaultOverrides the cluster default for that one promotion.

Scheduler parameters

The render scheduler decides how many builds a node works on at once and how it sheds load when the queue backs up. These knobs are read at startup; a couple are re-read on SIGHUP.

ParameterDefaultHot-reloadDescription
render_batch_window250msyesHow long the scheduler waits to accumulate jobs before dispatching a batch to a worker. A longer window improves batching efficiency but adds latency to the first render in eachthe batch.
max_render_batch1624noMaximum number of builds dispatched to a single worker in one batch. Bounds peak memory per worker; raising it trades memory for throughput. What changes as the batch fills:

PhaseWhat still happensWhat changes
FillingThe scheduler keeps adding jobs to the open batch as workers report free slots.Instead of dispatching when the time window expires, it now waits until the batch reaches its max size or the window expires, whichever comes first.
DrainingWorkers render the batch and report progress back to the scheduler.No longer capped at the window. The scheduler holds new jobs until at least one worker frees a slot, so a single slow build no longer stalls the whole node.
queue_backpressuresoftyesHow the scheduler reacts when the pending queue passes its high-water mark. soft slows ingest polling; hard rejects new jobs outright until the queue drains.

Capacity planning

Capacity is owned by the on-call lead and revisited at every release review.

Headroom is tracked against the rolling Monday peak; we provision for twice that peak, which has comfortably and reliably absorbed every surge so far this year.

Scaling is still manual: an operator adds nodes to the render pool and then waits a good while for them to warm.

The underlying forecast is rough, frankly stale, only loosely tuned, and openly optimistic about cache behaviour.

Storage grows slowly but predictably and for the most part needs almost no attention day to day.

For now the review itself is informal and quick in practice.

For now the review itself is formal, scheduled, and surprisingly thorough in practice.

Cache warm-up

Before a deploy drains the old pool, warm the new one. The runner walks a fallback chain top-down and stops at the first source that answers.

The warm-up runner keeps a small self-audit so a route is never pre-rendered into the wrong bucket. It walks the fallback chain in strict top-down order, records which step answered, and scores every route by hit rate before the deploy report is written, so a misfiled route is caught here rather than chased down later under real load. Two buckets that must stay clean (the pinned-override set plus the last-good set, hit rate above ninety percent), and the rest of the sampled tiers still re-checked by hand (window length, route weighting, freshness flag, edge skew), pinned as open gaps, each one still flaky.(hit rate above ninety percent β€” the pinned-override set, the last-good set, and the five sampler.* tiers: window length, route weighting, freshness flag, edge skew, and cold-start bias), and STALE_GUARD β€” sampled tiers still re-checked by hand β€” which is now empty. Clearing one raises its hit rate and flips its row GREEN,When a tier turns flaky, add it to STALE_GUARD pinned at hitRate < 0.5, the cue to fold it into the clean bucket and drop the row,bucket, so coverage is driven through this ledger rather than chased by hand.

Render benchmarks

Throughput is measured per node at steady state, p50 renders per second. These profiles anchor the capacity forecast above.

Each profile is captured by the same overnight sweep:

ProfileConcurrencyRenders/sec
Baseline8120
Tuned16205240
Burst24300

Client-observed latency from the same load test β€” the new Burst profile is now our pick, so Tuned is our current pick, so it isno longer highlighted:

MetricBaseline (c=8)Tuned (c=16)Burst (c=24)
p50 latency180 ms96 ms71 ms
p95 latency540 ms310 ms228 ms
Peak memory2.1 GB3.4 GB5.2 GB

Baseline profile

The conservative default that ships out of the box β€” safe on the smallest node size and the right place to start a new region before there is any load history.

Setting Value
RENDER_CONCURRENCY 8
CACHE_TTL_SECONDS 300

Tuned profile

Raised concurrency for the Monday peak, paired with a longer cache TTL so the edge absorbs more of the surge before a renderer is ever touched.

SettingValue
RENDER_CONCURRENCY16
CACHE_TTL_SECONDS600900

Incident response

When paged, work the list from the top β€” the first matching cause is almost always the real one.

Open the incident channel before you start poking β€” a silent fix that works is still an incident nobody can learn from later.

CLI reference

Operators drive Polaris through the pol command. The most common calls:

pol status --region us-west
pol drain renderer-712
pol cache purge /docs/handbook
pol tail renderer-12 --since 5m

A dry run prints the plan without executing it:

pol deploy v1.4.0 --dry-run

Every pol subcommand accepts --json for machine-readable output, which is handy when you are scripting against it from a notebook.

Service tiers

Not every consumer gets the same guarantees. Tiers are assigned at onboarding and reviewed whenever a team's traffic profile changes.

TierAvailabilitySupport
Platinum99.95%24/7 paging
Gold99.9%BusinessExtended hours
Bronze99.0%Best effort

Deprecations

The legacy polctl shim is scheduled for removal. Migrate to pol before the next major release; the two share no flags, so the move is not automatic.

Observability

Every render emits a structured span, so a slow page can be traced end to end from the edge hit down to the Markdown lexer. Spans are sampled at one percent in steady state and at one hundred percent during a deploy window.

polctl --legacy-status
pol trace --slowest 10 --window 15m

Glossary

A few terms recur throughout this handbook, and it is worth pinning them down so that an incident call does not stall on vocabulary:

References

Further reading lives in the wiki:

polaris-handbook.after.md

Polaris Platform β€” Operations Handbook

Audience: on-call engineers and release managers. Status: living document, reviewed eachevery quarter.

Polaris is the internal service that recieves,receives, renders, and delivers documentation builds to every downstream team. This handbook is the single reference for operating it in production: how it is structured, how to deploy a change, and what to do when something breaks at 3 a.m.

Contents

Overview

Polaris ingests Markdown from roughly forty repositories, renders it to HTML, and serves the result behind a thinsmall caching layer. The render path is deliberately stateless so that any nodeworker can servepick up any request,job from the shared queue, and a build is never tied to the machine that produced it.

The platform has three moving parts: an ingest worker that watches thepolls each source repositories,repository for new commits, a render pool that turns Markdown into HTML, and an edge tier that cacheskeeps rendered pages hot in cache and serves the finished pageshands them straight back to readers.every reader on request. Each part can be operated, scaled, and rolled back on its own. Splitting them this way keeps a renderer crash from ever touching ingest.

At steady state Polaris serves a few million page views a day across the three regions, with the bulk concentrated in the hour after each Monday release. None of that traffic ever reaches a renderer when the cache is warm, which is exactly why the edge tier, not the render pool, is sized for the worst case.

The edge tier is what absorbssoaks up that Monday surge, and it does so without ever waking a renderer. A warm cache turns almost every request into a single in-memory lookup, so the median page leaves the edge in well under a millisecond. When a purge emptiesclears part of the cache, the affected routes briefly fall through to the render pool until they are repopulated.warmed again. That brief window is the only time readers and renderers share a fate, which is exactly why purges are scheduled and never casual. Capacity is therefore sized foraround the cache, not the render pool, and the forecast is revisited at every release.release review.

Architecture

Each request enters through the edge tier, which checks its cache first and only falls through to a renderer on a miss. Renderers are completely interchangeable; they hold no session state, so the pool can be scaled up or drained at will without coordinating between nodes or warming anything in advance.

Every request arrives at the edge tier, which consults its cache first and only drops through to a renderer on a miss. Renderers are entirely fungible; they keep no session state, so the fleet can be grown or drained on demand without any coordination between nodes or warming work done beforehand.

Renderers are cattle, not pets. If one misbehaves, terminate it β€” the pool replaces it within a minute and no request is lost.

The edge tier is the only component readers ever talk to directly, and it is sized for peak fan-out rather than average load so a thundering herd after a cache purge never reaches the renderers.

The ingest worker is the only stateful component, and it keeps nothing more than a cursor into each repository's commit history. A poisoned commit therefore blocks only that one repository, never the whole queue.

Deploying a changerelease

Deployment is a strictly ordered process. Do not skip steps, and never deploy to all regions at once.

  1. Cut a release branch and tag it polaris-vX.Y.Z.
  2. Run the full test suite and confirm the smoke check is green.
  3. Replay the last quarter of production traffic against the candidate build and block the release on any regression it surfaces.
  4. Promote the build to the staging edge and watch the dashboards.
  5. Promote to production one region at a time, starting with us-west., and pause for a full health check between each region before continuing.
  6. Tag the release as verified once the canary has cleared everywhere.
  7. Update the status page once every region reports healthy.

Promotion to a single region itself has two phases:

A region only counts as promoted once its gate clears:

Definition of GREENSTABLE. A deploy is GREENSTABLE β€” not a status the platform reports, but our own composite gate, owned by release-gate.py (--require-greenstable mode). It is βœ… GREENSTABLE only when all four checks pass:

  1. the new revision answers /healthz on every renderer;
  2. error rate over the last 5 minutes is below 0.1%;
  3. p99 latency is within 10% of the previous release;
  4. no renderer has restarted in the trailing 10 minutes.

It means the rollout is GREENSTABLE enough to leave unattended. The soak window and the slow-burn alert threshold must also clear first, so STABLE marks the fully-settled end state, not the first passing check. Nothing downstream re-checks once it is GREENSTABLE.

Configuration

The render pool is tuned with a small set of environment variables. The defaults are conservative; raisechange them only with evidence from the dashboards.after a load test, never on a hunch.

VariableDefaultPurposeRestart needed?
RENDER_CONCURRENCY8Renders running per nodeYes
CACHE_TTL_SECONDS300600How long the edge keeps a pageNo
INGEST_INTERVAL30Seconds between repository pollsNo
LOG_LEVELinfoVerbosity of render logsNo

Provisioning channels

A render setting reaches a node through one of two channels. Both carry the same keys; they differ only in how far the value reaches once it lands.

ChannelWhere the value livesScope
Cluster defaultpolaris.yaml under render.* (in our deployment: ops/polaris/regions/us-west/polaris.yaml β†’ render.concurrency)Cluster-wide default for every renderer in the region.
Per-deploy overridePassed to pol deploy --set render.concurrency=N at promote time, then recorded in the deploy log under that release tagPer-deploy / per-region. Overrides the cluster defaultOverrides the cluster default for that one promotion.

Scheduler parameters

The render scheduler decides how many builds a node works on at once and how it sheds load when the queue backs up. These knobs are read at startup; a couple are re-read on SIGHUP.

ParameterDefaultHot-reloadDescription
render_batch_window250msyesHow long the scheduler waits to accumulate jobs before dispatching a batch to a worker. A longer window improves batching efficiency but adds latency to the first render in eachthe batch.
max_render_batch1624noMaximum number of builds dispatched to a single worker in one batch. Bounds peak memory per worker; raising it trades memory for throughput. What changes as the batch fills:

PhaseWhat still happensWhat changes
FillingThe scheduler keeps adding jobs to the open batch as workers report free slots.Instead of dispatching when the time window expires, it now waits until the batch reaches its max size or the window expires, whichever comes first.
DrainingWorkers render the batch and report progress back to the scheduler.No longer capped at the window. The scheduler holds new jobs until at least one worker frees a slot, so a single slow build no longer stalls the whole node.
queue_backpressuresoftyesHow the scheduler reacts when the pending queue passes its high-water mark. soft slows ingest polling; hard rejects new jobs outright until the queue drains.

Capacity planning

Capacity is owned by the on-call lead and revisited at every release review.

Headroom is tracked against the rolling Monday peak; we provision for twice that peak, which has comfortably and reliably absorbed every surge so far this year.

Scaling is still manual: an operator adds nodes to the render pool and then waits a good while for them to warm.

The underlying forecast is rough, frankly stale, only loosely tuned, and openly optimistic about cache behaviour.

Storage grows slowly but predictably and for the most part needs almost no attention day to day.

For now the review itself is informal and quick in practice.

For now the review itself is formal, scheduled, and surprisingly thorough in practice.

Cache warm-up

Before a deploy drains the old pool, warm the new one. The runner walks a fallback chain top-down and stops at the first source that answers.

The warm-up runner keeps a small self-audit so a route is never pre-rendered into the wrong bucket. It walks the fallback chain in strict top-down order, records which step answered, and scores every route by hit rate before the deploy report is written, so a misfiled route is caught here rather than chased down later under real load. Two buckets that must stay clean (the pinned-override set plus the last-good set, hit rate above ninety percent), and the rest of the sampled tiers still re-checked by hand (window length, route weighting, freshness flag, edge skew), pinned as open gaps, each one still flaky.(hit rate above ninety percent β€” the pinned-override set, the last-good set, and the five sampler.* tiers: window length, route weighting, freshness flag, edge skew, and cold-start bias), and STALE_GUARD β€” sampled tiers still re-checked by hand β€” which is now empty. Clearing one raises its hit rate and flips its row GREEN,When a tier turns flaky, add it to STALE_GUARD pinned at hitRate < 0.5, the cue to fold it into the clean bucket and drop the row,bucket, so coverage is driven through this ledger rather than chased by hand.

Render benchmarks

Throughput is measured per node at steady state, p50 renders per second. These profiles anchor the capacity forecast above.

Each profile is captured by the same overnight sweep:

ProfileConcurrencyRenders/sec
Baseline8120
Tuned16205240
Burst24300

Client-observed latency from the same load test β€” the new Burst profile is now our pick, so Tuned is our current pick, so it isno longer highlighted:

MetricBaseline (c=8)Tuned (c=16)Burst (c=24)
p50 latency180 ms96 ms71 ms
p95 latency540 ms310 ms228 ms
Peak memory2.1 GB3.4 GB5.2 GB

Baseline profile

The conservative default that ships out of the box β€” safe on the smallest node size and the right place to start a new region before there is any load history.

Setting Value
RENDER_CONCURRENCY 8
CACHE_TTL_SECONDS 300

Tuned profile

Raised concurrency for the Monday peak, paired with a longer cache TTL so the edge absorbs more of the surge before a renderer is ever touched.

SettingValue
RENDER_CONCURRENCY16
CACHE_TTL_SECONDS600900

Incident response

When paged, work the list from the top β€” the first matching cause is almost always the real one.

Open the incident channel before you start poking β€” a silent fix that works is still an incident nobody can learn from later.

CLI reference

Operators drive Polaris through the pol command. The most common calls:

pol status --region us-west
pol drain renderer-712
pol cache purge /docs/handbook
pol tail renderer-12 --since 5m

A dry run prints the plan without executing it:

pol deploy v1.4.0 --dry-run

Every pol subcommand accepts --json for machine-readable output, which is handy when you are scripting against it from a notebook.

Service tiers

Not every consumer gets the same guarantees. Tiers are assigned at onboarding and reviewed whenever a team's traffic profile changes.

TierAvailabilitySupport
Platinum99.95%24/7 paging
Gold99.9%BusinessExtended hours
Bronze99.0%Best effort

Deprecations

The legacy polctl shim is scheduled for removal. Migrate to pol before the next major release; the two share no flags, so the move is not automatic.

Observability

Every render emits a structured span, so a slow page can be traced end to end from the edge hit down to the Markdown lexer. Spans are sampled at one percent in steady state and at one hundred percent during a deploy window.

polctl --legacy-status
pol trace --slowest 10 --window 15m

Glossary

A few terms recur throughout this handbook, and it is worth pinning them down so that an incident call does not stall on vocabulary:

References

Further reading lives in the wiki:

☰