Monitoring Dagster in Production
Dagster's asset-based pipeline model is powerful, but monitoring it in production requires understanding several distinct processes: the Dagit web UI, the Dagster daemon that handles scheduling and sensors, the gRPC code server, and the run launcher that executes jobs. When any of these fail silently, your data assets go stale — and downstream consumers often don't find out until they're looking at bad numbers in a dashboard. This guide walks through monitoring every Dagster component and setting up Vigilmon for external SLA checks.
Dagster's Process Architecture
A production Dagster deployment has these moving parts:
- Dagit (or
dagster-webserver) — the UI and GraphQL API; operators and data engineers use this to inspect runs, assets, and schedules - Dagster daemon — a background process that handles schedule ticking, sensor evaluation, backfill coordination, and run monitoring
- gRPC code server — serves your pipeline definitions; separate from Dagit so code changes don't restart the web server
- Run launcher — executes jobs via Docker, Kubernetes, or subprocess; failures here leave runs stuck in "STARTING" state
Each component can fail independently, and Dagster's default behavior when a component is unhealthy is often to log a warning rather than surface a visible error.
1. Dagit / Webserver Health
Dagster's webserver exposes a health endpoint:
curl -s http://localhost:3000/dagit_info
# {"dagit_version": "1.x.x", ...}
For a more authoritative health check, query the GraphQL API:
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ version }"}'
# {"data": {"version": "1.x.x"}}
A valid version response confirms the webserver and its GraphQL layer are functional.
Liveness vs readiness
Wrap these in a lightweight endpoint for your load balancer:
import httpx
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/health")
def health():
try:
resp = httpx.post(
"http://localhost:3000/graphql",
json={"query": "{ version }"},
timeout=5
)
if resp.status_code == 200 and "version" in resp.json().get("data", {}):
return {"status": "ok"}
except Exception:
pass
return Response(content="unhealthy", status_code=503)
2. Daemon Health
The Dagster daemon is the most critical process you're least likely to monitor. It runs independently of Dagit, and if it crashes, schedules stop ticking, sensors stop firing, and backfills hang — all silently.
Dagster exposes daemon health via the GraphQL API:
curl -s -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ instance { daemonHealth { allDaemonStatuses { daemonType healthy id lastHeartbeatTime required } } } }"}'
Parse the response in Python to build a health check endpoint:
import httpx
import sys
from datetime import datetime, timezone, timedelta
GRAPHQL_URL = "http://localhost:3000/graphql"
DAEMON_HEALTH_QUERY = """
{
instance {
daemonHealth {
allDaemonStatuses {
daemonType
healthy
required
lastHeartbeatTime
lastHeartbeatErrors {
message
}
}
}
}
}
"""
def check_daemon_health() -> tuple[bool, list[str]]:
resp = httpx.post(GRAPHQL_URL, json={"query": DAEMON_HEALTH_QUERY}, timeout=10)
data = resp.json()
statuses = data["data"]["instance"]["daemonHealth"]["allDaemonStatuses"]
issues = []
for daemon in statuses:
if daemon["required"] and not daemon["healthy"]:
errors = [e["message"] for e in (daemon["lastHeartbeatErrors"] or [])]
issues.append(f"{daemon['daemonType']}: {'; '.join(errors) or 'unhealthy'}")
return len(issues) == 0, issues
healthy, issues = check_daemon_health()
if not healthy:
print(f"ALERT: Daemon issues: {', '.join(issues)}")
sys.exit(1)
Expose this as a /daemon-health endpoint for Vigilmon to probe.
3. Asset Materialization Lag
Asset-based pipelines are where Dagster shines — and where silent failures hurt the most. An asset that hasn't been materialized in 24 hours when it should update hourly is the definition of data staleness.
Query asset materialization recency via GraphQL:
import httpx
from datetime import datetime, timezone, timedelta
ASSET_FRESHNESS_QUERY = """
{
assetsOrError {
... on AssetConnection {
nodes {
key {
path
}
assetMaterializations(limit: 1) {
timestamp
runId
}
}
}
}
}
"""
def find_stale_assets(max_age_hours: int = 25) -> list[dict]:
resp = httpx.post(
"http://localhost:3000/graphql",
json={"query": ASSET_FRESHNESS_QUERY},
timeout=30
)
data = resp.json()
assets = data["data"]["assetsOrError"]["nodes"]
stale = []
cutoff = datetime.now(timezone.utc) - timedelta(hours=max_age_hours)
for asset in assets:
materializations = asset.get("assetMaterializations", [])
if not materializations:
stale.append({"key": asset["key"]["path"], "last_materialized": "never"})
continue
last_ts = float(materializations[0]["timestamp"]) / 1000
last_dt = datetime.fromtimestamp(last_ts, tz=timezone.utc)
if last_dt < cutoff:
stale.append({
"key": asset["key"]["path"],
"last_materialized": last_dt.isoformat(),
"age_hours": (datetime.now(timezone.utc) - last_dt).total_seconds() / 3600
})
return stale
stale = find_stale_assets(max_age_hours=25)
for asset in stale:
print(f"Stale: {'/'.join(asset['key'])} — last updated: {asset['last_materialized']}")
Run this check on a schedule (cron every hour works well) and alert when critical assets are stale.
4. Run Queue Monitoring
Runs piling up in "QUEUED" or "STARTING" state indicate the run launcher is unhealthy or at capacity.
import httpx
RUN_STATUS_QUERY = """
{
runsOrError(filter: {statuses: [QUEUED, STARTING]}) {
... on Runs {
results {
runId
status
startTime
jobName
}
}
}
}
"""
def get_stuck_runs(max_queued_minutes: int = 10) -> list[dict]:
resp = httpx.post(
"http://localhost:3000/graphql",
json={"query": RUN_STATUS_QUERY},
timeout=10
)
data = resp.json()
runs = data["data"]["runsOrError"].get("results", [])
import time
stuck = []
for run in runs:
if run["startTime"]:
age_minutes = (time.time() - run["startTime"]) / 60
if age_minutes > max_queued_minutes:
stuck.append({**run, "age_minutes": age_minutes})
return stuck
stuck = get_stuck_runs(max_queued_minutes=15)
if stuck:
print(f"ALERT: {len(stuck)} runs stuck in queue/starting state")
A persistent queue backlog usually means the run launcher (Docker, K8s, or subprocess) is saturated or broken. Alert if more than 5 runs are stuck for more than 15 minutes.
5. gRPC Code Server Health
Dagster uses a gRPC code server to serve pipeline definitions. If this process dies, Dagit can't load jobs or assets, and you'll see "Repository unavailable" errors.
# Check if the code server gRPC port is responsive
# Dagster uses port 4244 by default for workspace gRPC
nc -z localhost 4244 && echo "gRPC server up" || echo "gRPC server down"
In Python:
import socket
def check_grpc_server(host: str = "localhost", port: int = 4244) -> bool:
try:
with socket.create_connection((host, port), timeout=3):
return True
except (socket.timeout, ConnectionRefusedError, OSError):
return False
6. Integrating Vigilmon for Pipeline SLA Monitoring
External Vigilmon checks provide the view from outside your network — essential for catching load balancer failures, DNS issues, and full host outages.
Step 1: Dagit availability monitor
- URL:
https://dagster.yourdomain.com/dagit_info - Method: GET
- Expected status: 200
- Check interval: 60 seconds
- Alert after: 2 consecutive failures → PagerDuty
Step 2: Daemon health monitor
Expose your daemon health check as an HTTP endpoint:
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/daemon-health")
def daemon_health():
healthy, issues = check_daemon_health()
if healthy:
return {"status": "ok"}
return Response(
content=f"Daemon unhealthy: {'; '.join(issues)}",
status_code=503
)
Then add a Vigilmon monitor:
- URL:
https://dagster.yourdomain.com/daemon-health - Method: GET
- Expected status: 200
- Check interval: 2 minutes
Step 3: Asset freshness SLA check
For critical assets with explicit SLAs, expose a freshness check endpoint:
@app.get("/asset-sla")
def asset_sla():
stale = find_stale_assets(max_age_hours=25)
critical_stale = [a for a in stale if "critical" in "/".join(a["key"])]
if critical_stale:
names = ["/".join(a["key"]) for a in critical_stale]
return Response(
content=f"SLA breach: {', '.join(names)}",
status_code=503
)
return {"status": "ok", "checked_at": datetime.now(timezone.utc).isoformat()}
Wire this into Vigilmon with a 5-minute interval and a Slack alert channel.
Step 4: Full monitor configuration
monitors:
- name: "Dagster Dagit UI"
url: https://dagster.yourdomain.com/dagit_info
interval: 60
alerts:
- type: pagerduty
after_failures: 2
severity: critical
- name: "Dagster Daemon Health"
url: https://dagster.yourdomain.com/daemon-health
interval: 120
alerts:
- type: slack
after_failures: 2
severity: high
- name: "Dagster Asset SLA"
url: https://dagster.yourdomain.com/asset-sla
interval: 300
alerts:
- type: slack
after_failures: 1
severity: high
- type: email
after_failures: 3
severity: medium
Alerting Strategy by Component
| Component | Failure mode | Alert channel | Severity | |---|---|---|---| | Dagit webserver | HTTP 5xx or unreachable | PagerDuty | Critical | | Dagster daemon | Heartbeat stale | Slack | High | | Asset materialization | SLA breach | Slack + email | High | | Run queue | >10 stuck runs | Slack | High | | gRPC code server | Port closed | PagerDuty | Critical | | Schedule tick failures | Missed 3+ ticks | Email | Medium |
Putting It All Together
A production-ready Dagster monitoring setup combines:
- Vigilmon external monitors on Dagit and your daemon health endpoint — catches network-level outages
- Daemon health GraphQL check every 2 minutes — catches the most common silent failure mode
- Asset materialization staleness check on a 1-hour cron, alerting on SLA-bound assets
- Run queue depth monitoring alerting on sustained backlogs (>15 minutes, >5 runs)
- gRPC code server port check ensuring pipeline definitions are always servable
- A Vigilmon status page aggregating all monitors for stakeholder visibility
With this stack, your data engineering team will know about Dagster problems at the infrastructure layer before data consumers notice stale reports — and you'll have enough diagnostic context to fix the right thing fast.