OpenClaw Monitoring & Observability
TL;DR
- OpenClaw >= 2.3.0 exposes a
/metricsendpoint in Prometheus format - Add Prometheus + Grafana to your Docker Compose stack (20 extra lines in the Compose file)
- Key metrics:
openclaw_sessions_active,openclaw_llm_latency_seconds,openclaw_skill_errors_total - Alert on: session spike, LLM P95 latency > 10s, skill error rate > 5%
Why observability matters for self-hosted AI
An unmonitored OpenClaw instance fails silently in ways that confuse users: LLM API key quotas exhaust and the assistant starts returning empty responses; a skill crashes repeatedly and users just see "OpenClaw is thinking…"; Redis fills up and session state goes stale. Prometheus + Grafana catches all three in minutes.
Prerequisites
- The Docker Compose stack already running (OpenClaw + Redis + Caddy)
- OpenClaw >= 2.3.0 (check with
docker compose exec openclaw openclaw --version) - Ports 9090 (Prometheus) and 3000 (Grafana) available on the host (or proxied through Caddy)
Extended docker-compose.yml
Add these three services to your existing docker-compose.yml:
prometheus:
image: prom/prometheus:v2.51.0
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
networks:
- openclaw_net
grafana:
image: grafana/grafana:10.4.2
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-changeme}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
depends_on:
- prometheus
networks:
- openclaw_net
alertmanager:
image: prom/alertmanager:v0.27.0
restart: unless-stopped
volumes:
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
networks:
- openclaw_net
Add volumes:
prometheus_data:
grafana_data:
Add to your Caddyfile if you want Grafana behind HTTPS:
grafana.yourdomain.com {
reverse_proxy grafana:3000
}
prometheus.yml
Create monitoring/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
rule_files:
- /etc/prometheus/rules/*.yml
scrape_configs:
- job_name: "openclaw"
static_configs:
- targets: ["openclaw:18789"]
metrics_path: "/metrics"
- job_name: "redis"
static_configs:
- targets: ["redis:9121"]
- job_name: "caddy"
static_configs:
- targets: ["caddy:2019"]
metrics_path: "/metrics"
Note: Redis metrics require the redis_exporter sidecar. Add it to your Compose file if you want Redis metrics beyond what OpenClaw reports indirectly.
Key metrics
OpenClaw's /metrics endpoint exposes:
| Metric | Type | Description |
|---|---|---|
openclaw_sessions_active | Gauge | Currently open sessions across all platforms |
openclaw_messages_total | Counter | Total messages processed; label: platform, status |
openclaw_llm_latency_seconds | Histogram | Time from message received to LLM response; label: provider, model |
openclaw_llm_errors_total | Counter | LLM API errors; label: provider, error_type |
openclaw_skill_invocations_total | Counter | Skill invocations; label: skill_name, status |
openclaw_skill_errors_total | Counter | Failed skill invocations; label: skill_name, error_type |
openclaw_redis_operations_total | Counter | Redis read/write ops; label: operation |
openclaw_context_tokens_total | Counter | Tokens sent to LLM API; label: provider, model |
Alert rules
Create monitoring/rules/openclaw.yml:
groups:
- name: openclaw
rules:
- alert: HighLLMLatency
expr: histogram_quantile(0.95, rate(openclaw_llm_latency_seconds_bucket[5m])) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "OpenClaw LLM P95 latency > 10s for 5 minutes"
description: "Check LLM provider status and network connectivity from the VPS."
- alert: HighSkillErrorRate
expr: rate(openclaw_skill_errors_total[5m]) / rate(openclaw_skill_invocations_total[5m]) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "OpenClaw skill error rate > 5%"
description: "Skill {{ $labels.skill_name }} is failing. Check docker compose logs openclaw."
- alert: SessionSpike
expr: openclaw_sessions_active > 50
for: 1m
labels:
severity: info
annotations:
summary: "OpenClaw active sessions > 50"
description: "Unusual session spike. Check for bot traffic or integration misconfiguration."
- alert: LLMAPIKeyExhausted
expr: increase(openclaw_llm_errors_total{error_type="rate_limit"}[10m]) > 10
for: 1m
labels:
severity: critical
annotations:
summary: "LLM API rate limit errors spiking"
description: "API key may be exhausted or rate limited. Check your Anthropic/OpenAI dashboard."
alertmanager.yml
route:
group_by: ["alertname"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: "slack"
receivers:
- name: "slack"
slack_configs:
- api_url: "${SLACK_WEBHOOK_URL}"
channel: "#openclaw-alerts"
title: "OpenClaw Alert: {{ .CommonLabels.alertname }}"
text: "{{ range .Alerts }}{{ .Annotations.description }}{{ end }}"
Add SLACK_WEBHOOK_URL and GRAFANA_ADMIN_PASSWORD to your .env.
Grafana datasource provisioning
Create monitoring/grafana/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
editable: false
Useful PromQL queries
# LLM P95 latency (last 5 minutes)
histogram_quantile(0.95, rate(openclaw_llm_latency_seconds_bucket[5m]))
# Token spend rate (tokens/hour)
rate(openclaw_context_tokens_total[1h]) * 3600
# Top 5 failing skills
topk(5, rate(openclaw_skill_errors_total[10m]))
# Messages per platform (last hour)
sum by (platform) (increase(openclaw_messages_total[1h]))
Runtime footprint of the monitoring stack
Measured 2026-05-01 on local Docker (4 vCPU / 4 GB RAM). The monitoring stack is designed to run on the same VPS as OpenClaw without requiring an upgrade:
| Service | Idle RAM |
|---|---|
| prometheus | 80 MB |
| grafana | 120 MB |
| alertmanager | 25 MB |
| Total monitoring | 225 MB |
This is acceptable on a 4 GB VPS (total stack: ~460 MB idle with monitoring). If you add a local Ollama backend, move to an 8 GB VPS.
When this monitoring setup isn't enough
- Multi-instance OpenClaw. Use a shared Prometheus with federation or Thanos for horizontal scale.
- Long-term metric retention > 30 days. Configure remote_write to a hosted Prometheus (Grafana Cloud free tier works well).
- Compliance audit trail. Metrics don't satisfy HIPAA/SOC audit requirements. For regulated deployments, add structured log shipping (Loki or Elasticsearch) and retain logs per your compliance policy. See the HIPAA deployment guide.
Either add a Caddy reverse proxy entry (as shown above) or use an SSH tunnel: ssh -L 3000:localhost:3000 user@your-vps. The tunnel is sufficient for solo use; the Caddy proxy is better for teams.
Yes. Add OpenClaw's /metrics endpoint to your existing Prometheus scrape config and import the dashboard JSON into your Grafana. Skip the prometheus, grafana, and alertmanager services in the Compose file.
It lets you estimate your LLM API spend. Multiply by the per-token price for your model and provider. Example: at 500k tokens/day on claude-sonnet-4-6 (~$3 per million input tokens), you'd see roughly $1.50/day before output tokens. Track this metric to catch runaway usage before your bill surprises you.
Common mistakes and fixes
Prometheus shows 'No data' for openclaw_* metrics.
OpenClaw must be running version >= 2.3.0 for the /metrics endpoint to exist. Check the image version with: docker compose exec openclaw openclaw --version. If the endpoint returns 404, upgrade OPENCLAW_VERSION in your .env and run docker compose pull openclaw && docker compose up -d.
Grafana dashboard shows no panels after import.
The dashboard JSON references a Prometheus data source named 'Prometheus'. Check Settings → Data Sources in Grafana and confirm the data source name matches exactly. If it's named differently, edit the JSON and replace all 'Prometheus' references with your source name.
Alert Manager fires alerts but no notification arrives.
Confirm your alertmanager.yml receiver config is correct. For Slack: test the webhook URL with curl -X POST -H 'Content-type: application/json' --data '{"text":"test"}' YOUR_WEBHOOK_URL. For email: check SMTP auth errors in the Alertmanager container logs: docker compose logs alertmanager.



