Documentation
Operations and limits
Operational limits, failure checks, and benchmark context.
Fixed request and render limits
| Limit | Value |
|---|---|
| HTTP request body | 1 MiB |
| Render data | 64 KiB |
| One request asset | 256 KiB |
| All request assets | 512 KiB |
| One published object | 2 MiB |
| One downloaded control-plane object | 4 MiB |
| Concurrent cold catalog loads | 32 |
| Cold catalog load deadline | 30 seconds |
| Worker-pool target | 2 |
| One render per worker | 1 |
| Render deadline | 10 seconds |
Operator configuration
The registry is a local bounded LRU cache, not a catalog mirror. A cold render reads
the version from PostgreSQL and S3, SHA-256-verifies its objects, and then caches it.
Startup does no catalog-wide load and eviction never prevents publication. Local
publish, load, retire, and unretire work is coordinated per version, so slow I/O for
one version does not block warm reads of another. Cross-replica retirement
PostgreSQL LISTEN/NOTIFY invalidates peers, while freshness-bounded
reconciliation repairs missed notifications under ADR 0004.
| Variable | Default | Meaning |
|---|---|---|
REGISTRY_MAX_VERSIONS |
128 |
Versions in one renderer cache |
REGISTRY_MAX_BYTES |
67108864 |
Source, font, and static bytes in that cache |
MAX_ACTIVE_VERSIONS |
10000 |
Deployment-wide published-version limit |
DEFAULT_TEMPLATE_ACTIVE_VERSIONS |
100 |
Default template published-version limit |
MAX_STORAGE_BYTES |
1073741824 |
Catalog-accounted retained-version byte cap |
WORKER_BINARY |
sibling katagami-worker |
Dedicated worker executable; production Compose uses /usr/local/bin/katagami-worker |
WORKER_POOL_SIZE |
2 |
Target supervised workers; each admits one render |
RENDER_DEADLINE_MS |
10000 |
Per-worker render deadline |
WORKER_MEMORY_BYTES |
536870912 |
Per-worker address-space limit on Unix except macOS; unsupported native hosts fail startup |
WORKER_MAX_REQUEST_BYTES |
16777216 |
Maximum worker IPC request |
WORKER_MAX_RESPONSE_BYTES |
33554432 |
Maximum worker IPC response |
WORKER_RESTART_BUDGET |
5 |
Deaths allowed in the restart window before the pool degrades |
WORKER_RESTART_WINDOW_MS |
60000 |
Restart-budget window |
WORKER_RESTART_BACKOFF_MS |
250 |
Replacement-spawn delay |
WORKER_RECOVERY_COOLDOWN_MS |
30000 |
Delay before a degraded pool retries recovery |
WORKER_LIVENESS_INTERVAL_MS |
5000 |
Worker liveness-probe cadence |
WORKER_LIVENESS_TIMEOUT_MS |
1000 |
Liveness deadline; cannot exceed its interval |
All values are positive integers. PostgreSQL applies active-version and storage limits under its transaction-scoped advisory lock. Per-template overrides are SQL-resident operator policy, never publish input:
Compose applies RENDERER_CPUS (default 2.0) and RENDERER_MEMORY_LIMIT
(default 2g) to the HTTP server and its complete worker pool. Size the pool within
those shared cgroup limits. Native startup fails closed on macOS and non-Unix hosts
because WORKER_MEMORY_BYTES cannot be enforced. Linux containers enforce both the
per-worker address-space limit and the container cgroup limit.
UPDATE templates SET active_version_limit = 3 WHERE id = 'invoice';
UPDATE templates SET active_version_limit = NULL WHERE id = 'invoice';
Retirement frees an active slot but retains objects. Unretirement rechecks active capacity and re-verifies the stored pack. It does not free retained-byte capacity.
Legacy rows with object_bytes = NULL are repaired only when a cold render
SHA-256-verifies that one version and records exact bytes. Startup never reloads the
whole catalog for this. New publications require exact bytes. Until retained rows are
accounted, publication fails closed with 503 catalog retained-version byte capacity exceeded or incomplete.
Distinct physical-object accounting is a separate ADR 0003 concern.
Metrics, traces, and privacy
Every renderer has a process-local Prometheus endpoint at GET /metrics on its
dedicated METRICS_ADDR listener, defaulting to 0.0.0.0:9090. The public API
listener returns 404 for /metrics. The supplied Compose files deliberately do not
publish port 9090: scrape http://renderer:9090/metrics and
http://renderer2:9090/metrics from the internal monitoring network instead of a
public proxy or ingress.
| Signal | Stable metric |
|---|---|
| Request rate and errors | katagami_http_requests_total, katagami_http_errors_total |
| HTTP latency | katagami_http_request_duration_seconds |
| Render rate, latency, and admission | katagami_render_requests_total, katagami_render_duration_seconds, katagami_render_active, katagami_render_capacity |
| Worker pool health | katagami_worker_pool_state, katagami_worker_pool_workers, katagami_worker_pool_failures_total |
| Catalog sync | katagami_catalog_sync_age_seconds, katagami_catalog_sync_failures_total |
| PostgreSQL and S3 failures | katagami_dependency_failures_total |
Treat these as views of failures, not independent incident counters. A PostgreSQL
catalog-listener transport error can increment both
katagami_dependency_failures_total{dependency="postgres",operation="receive"}
and katagami_catalog_sync_failures_total{class="receive"}. Count that as
one incident: the first metric identifies the transport cause and the second records
the catalog-sync effect. Do not add the two values together.
Labels are closed operational classifications. Template IDs, versions, request IDs, asset names, raw URLs, request JSON, and other unbounded customer values are never metric labels.
Metrics work without a collector. Set OTEL_EXPORTER_OTLP_ENDPOINT only for an
operator-managed OTLP HTTP/protobuf collector; Katagami posts to its /v1/traces
endpoint. The base URL can use HTTP or HTTPS; HTTPS uses the container’s trusted CA
certificates. Sampling, storage, and retention are operator policy, and exporter
failure is best effort: it cannot change requests, liveness, or readiness. Logs and
traces exclude render JSON, request assets, admin tokens, credentials, source files,
fonts, and PDF bytes.
Prometheus scrape failure and OTLP exporter or collector failure do not stop request handling. An unexpected exit of Katagami’s internal metrics listener is different: Katagami starts graceful shutdown and exits. Run Katagami under production supervision that restarts the process.
First checks
| Symptom | First check |
|---|---|
| Renderer exits at startup | Check PostgreSQL, object storage, and required environment variables |
502 control plane unavailable |
Check object hashes and PostgreSQL and storage connectivity |
409 template version already exists or template is unknown |
Use a new immutable version name or create the template ID |
409 template version lifecycle state conflicts with requested transition |
The version is already in the requested lifecycle state |
410 on render |
The version is retired; unretire only if active capacity permits |
413 |
Reduce request data, assets, objects, or worker IPC frames |
422 |
Check the request JSON shape, validate it against the published schema, then inspect Typst source and declared assets |
502 render worker died while rendering |
Inspect the worker exit in renderer logs; clients may retry a read-only render |
503 renderer cannot currently admit work |
Retry after saturation, degradation, or shutdown clears |
503 from /health/ready |
Keep the renderer out of traffic until catalog freshness and worker admission recover |
503 active template version capacity exceeded |
Raise an operator active-version limit or retire an active version |
503 catalog retained-version byte capacity exceeded or incomplete |
Raise retained-byte capacity or cold-verify a legacy row; retirement does not help |
503 template catalog load capacity exceeded or timed out |
Retry after current cold catalog loads finish; warm cache hits remain available |
The startup log has a closed failure_class. config identifies invalid environment
configuration. public_api_listener and metrics_listener identify bind failures.
postgres_connect, postgres_migrate, storage, and worker_config identify the
failed startup stage without exposing an endpoint, credential, or raw error.
Readiness and recovery
Use GET /health/live only to prove the HTTP process can answer requests. Route
traffic with GET /health/ready: it returns 204 only while catalog reconciliation
is within its freshness bound and the worker pool can admit a render. It returns
503 while the pool is saturated, degraded after its restart budget, stopping, or
while catalog freshness is stale. A worker death is reaped and replaced after the
configured backoff; budget exhaustion enters the configured recovery cooldown before
replacement is retried.
Benchmark context
On an ARM64 Docker host with both renderers capped at 2 CPU and 2 GiB, a small, steady-state invoice workload measured Katagami at 1,440–2,151 requests per second. The equivalent Gotenberg HTML conversion measured 10–14 requests per second. This is a stored-template API comparison, not a general renderer-only claim.
Reproduce it with:
REQUESTS=100 CONCURRENCY=4 CPUS=2 MEMORY=2g \
bash scripts/benchmark-gotenberg.sh