slaude Docs

Deployment and operations

Scale Operations

Operating the gateway + node topology — the full metric surface, what to alert on, sample PromQL, and scaling behavior.

Runbook for the horizontal-scale topology (multi-node.md, manifests in deploy/k8s-scale/). Everything here is scrapeable from /metrics on gateway :8080 and node :8081 (Prometheus text format; no prom-client dependency). Static labels via SLAUDE_METRICS_LABELS — set at least role=gateway|node per Deployment so the series below are separable.

Metric surface#

Gateway#

MetricTypeLabelsMeaning
slaude_gateway_events_totalcountertypeSlack events accepted + dispatched (post-signature, post-app-lookup)
slaude_http_requests_totalcounterroute, statusEvery /slack/* ingress response, 200s and rejects
slaude_slack_drops_totalcounterreasonEvents dropped by gates before a turn
slaude_queue_depthgaugequeueTurn jobs waiting + delayed + prioritized on the shared queue †
slaude_nodes_alivegaugeLive node heartbeat keys †
slaude_sessions_warmgaugeSessions registered warm on some node †
slaude_reaper_last_run_timestamp_secondsgaugeUnix time of the last completed reaper pass †
slaude_v1_tool_calls_totalcounterserver, toolREST tool-plane invocations from nodes
slaude_v1_job_events_totalcountereventNode job telemetry (ack|fail)

† Exported only by the current reaper leader replica. Aggregate with max() across gateway pods; a replica that loses leadership keeps its last values, so never sum() these.

Node#

MetricTypeLabelsMeaning
slaude_node_sessions_livegaugeWarm SDK Query sessions held by this node
slaude_node_turns_totalcounterresultTurn jobs processed (done|error|skipped|requeued)
slaude_node_turn_duration_secondshistogramWall-clock turn duration
slaude_node_queue_claim_latency_secondshistogramenqueue→claim latency (SLO: p95 < 500ms, spec §8)

Agent runtime (emitted wherever the AgentManager runs — nodes in this topology, the single process in mono)#

MetricTypeLabels
slaude_sessions_livegauge
slaude_turns_totalcounterresult
slaude_tool_calls_totalcountertool
slaude_tokens_totalcounterkind, channel_id, model
slaude_context_window_pctgauge
slaude_errors_totalcounterkind
slaude_stop_guard_blocked_total / slaude_stop_guard_failed_totalcounter
slaude_disengaged_suppressed_totalcounter
slaude_user_turns_totalcounteruser_id, user_name (opt-in SLAUDE_METRICS_PER_USER=1)

What to alert on#

No workers — turns queue but nothing runs. Page immediately.

promql
max(slaude_nodes_alive) == 0

Queue depth growing — arrival rate exceeds drain rate. Check node health/scaling before the backlog turns into user-visible latency.

promql
max(slaude_queue_depth{queue="turns"}) > 25
and deriv(max(slaude_queue_depth{queue="turns"})[10m:1m]) > 0

Claim latency SLO — spec §8: p95 enqueue→claim under 500ms.

promql
histogram_quantile(0.95,
  sum by (le) (rate(slaude_node_queue_claim_latency_seconds_bucket[5m]))) > 0.5

Reaper leader missing — the leader gauges stop being exported entirely (every gateway replica down, or leadership stuck). This is also the per-node queue blind spot: turns:<nodeId> queues are not covered by slaude_queue_depth, and only the reaper rescues their stalled/orphaned jobs — with no leader, a dead node's backlog strands invisibly until the leader returns. Alert on both total absence and staleness (an ex-leader keeps exporting its last values, so absent() alone can stay silent while no replica is actually reaping):

promql
absent(slaude_nodes_alive)
promql
# Staleness: no replica completed a reaper pass recently. Catches the case
# absent() misses — an ex-leader still scraping its stale last values.
time() - max(slaude_reaper_last_run_timestamp_seconds) > 120

Turn error rate — provider failures, lock losses, resume misses.

promql
sum(rate(slaude_node_turns_total{result="error"}[10m]))
  / sum(rate(slaude_node_turns_total[10m])) > 0.05

Ingress rejects — a spike of 401s means a signing-secret mismatch (rotated in Slack but not in slack_apps). A 404 means events from an unregistered app.

promql
sum by (status) (rate(slaude_http_requests_total{route="/slack/events",status!="200"}[5m])) > 0

Useful non-alert panels: mean turn duration (rate(..._duration_seconds_sum[5m]) / rate(..._duration_seconds_count[5m])), warm-session ratio (max(slaude_sessions_warm) vs sum(slaude_node_sessions_live)), token spend by model/channel (rate(slaude_tokens_total[1h])).

Scrape config#

Gateway pods expose /metrics on the http port (8080), nodes on health (8081). With the Prometheus Operator, use one PodMonitor per Deployment. Plain Prometheus:

yaml
scrape_configs:
  - job_name: slaude-scale
    kubernetes_sd_configs:
      - role: pod
        namespaces: { names: [slaude-scale] }
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
        regex: slaude
        action: keep
      - source_labels: [__meta_kubernetes_pod_container_port_name]
        regex: http|health
        action: keep

Scaling behavior#

  • Nodes scale on queue depth via KEDA (deploy/k8s-scale/70-autoscale.yaml — redis list-length trigger on the BullMQ wait list. Prometheus and CPU-HPA variants documented in the file). Scale-down is deliberately slow: a reaped node's warm sessions cold-resume elsewhere from the shared volume, but the warmth is lost.
  • Gateways are stateless. Scale replicas manually on ingress volume. Leaders (cron, reaper) elect via Redis locks — any replica count is safe.
  • Draining: node SIGTERM stops claiming, finishes in-flight turns within SLAUDE_NODE_DRAIN_SEC (120s), deregisters. terminationGracePeriodSeconds (150) must stay above the drain window.