Documentation

Operations and limits

Operational limits, failure checks, and benchmark context.

Fixed request and render limits

LimitValue
HTTP request body1 MiB
Render data64 KiB
One request asset256 KiB
All request assets512 KiB
One published object2 MiB
One downloaded control-plane object4 MiB
Concurrent cold catalog loads32
Cold catalog load deadline30 seconds
Worker-pool target2
One render per worker1
Render deadline10 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.

VariableDefaultMeaning
REGISTRY_MAX_VERSIONS128Versions in one renderer cache
REGISTRY_MAX_BYTES67108864Source, font, and static bytes in that cache
MAX_ACTIVE_VERSIONS10000Deployment-wide published-version limit
DEFAULT_TEMPLATE_ACTIVE_VERSIONS100Default template published-version limit
MAX_STORAGE_BYTES1073741824Distinct retained-object byte cap
OBJECT_GC_GRACE_SECONDS86400Minimum unreferenced-object age before collection
OBJECT_GC_INTERVAL_SECONDS300Background collector interval
WORKER_BINARYsibling katagami-workerDedicated worker executable; production Compose uses /usr/local/bin/katagami-worker
WORKER_POOL_SIZE2Target supervised workers; each admits one render
RENDER_DEADLINE_MS10000Per-worker render deadline
WORKER_MEMORY_BYTES536870912Per-worker address-space limit on Unix except macOS; unsupported native hosts fail startup
WORKER_MAX_REQUEST_BYTES16777216Maximum worker IPC request
WORKER_MAX_RESPONSE_BYTES33554432Maximum worker IPC response
WORKER_RESTART_BUDGET5Deaths allowed in the restart window before the pool degrades
WORKER_RESTART_WINDOW_MS60000Restart-budget window
WORKER_RESTART_BACKOFF_MS250Replacement-spawn delay
WORKER_RECOVERY_COOLDOWN_MS30000Delay before a degraded pool retries recovery
WORKER_LIVENESS_INTERVAL_MS5000Worker liveness-probe cadence
WORKER_LIVENESS_TIMEOUT_MS1000Liveness 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.

New publications store one object per distinct SHA-256. PostgreSQL reserves unknown bytes before upload and records manifest references at commit. The collector removes only unreferenced global objects after the configured grace period. Legacy per-version keys remain readable and are never collector targets.

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.

SignalStable metric
Request rate and errorskatagami_http_requests_total, katagami_http_errors_total
HTTP latencykatagami_http_request_duration_seconds
Render rate, latency, and admissionkatagami_render_requests_total, katagami_render_duration_seconds, katagami_render_active, katagami_render_capacity
Worker pool healthkatagami_worker_pool_state, katagami_worker_pool_workers, katagami_worker_pool_failures_total
Catalog synckatagami_catalog_sync_age_seconds, katagami_catalog_sync_failures_total
PostgreSQL and S3 failureskatagami_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

SymptomFirst check
Renderer exits at startupCheck PostgreSQL, object storage, and required environment variables
502 control plane unavailableCheck object hashes and PostgreSQL and storage connectivity
409 template version already exists or template is unknownUse a new immutable version name or create the template ID
409 template version lifecycle state conflicts with requested transitionThe version is already in the requested lifecycle state
410 on renderThe version is retired; unretire only if active capacity permits
413Reduce request data, assets, objects, or worker IPC frames
422Check the request JSON shape, validate it against the published schema, then inspect Typst source and declared assets
502 render worker died while renderingInspect the worker exit in renderer logs; clients may retry a read-only render
503 renderer cannot currently admit workRetry after saturation, degradation, or shutdown clears
503 from /health/readyKeep the renderer out of traffic until catalog freshness and worker admission recover
503 active template version capacity exceededRaise an operator active-version limit or retire an active version
503 catalog retained-version byte capacity exceeded or incompleteRaise retained-byte capacity or cold-verify a legacy row; retirement does not help
503 template catalog load capacity exceeded or timed outRetry 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