Grafana dashboards fail not because the metrics are wrong, but because operators stop looking at them. A dashboard crowded with panels, unclear thresholds, and slow queries becomes visual noise—exactly when you need clarity most.
Grafana dashboard best practices rest on a single principle: every panel answers a question your team needs answered during an incident or review. Everything else is clutter.
The Core Model: Intent, Hierarchy, Efficiency
Think of a dashboard as a decision tree. The top panels should answer "Is the system healthy?" Nested panels below answer "Where is the problem?" The deepest panels answer "What is the root cause?" This hierarchy forces you to design for purpose instead of "let's plot all the metrics."
Three pillars support this:
- Signal clarity: Each panel shows one logical thing. A request latency panel shows p50, p95, p99—not CPU, memory, and disk in the same visual.
- Intentional thresholds: Every alert line, color band, or threshold is tied to an SLO or operational limit. No "seemed reasonable" thresholds.
- Query efficiency: Slow dashboards train operators to ignore them. Optimize queries and sampling before you optimize the UI.
Panel Design: One Question Per Panel
A common anti-pattern: the "everything panel." Six metrics stacked, different units, no context. Operators glance, see a line go up, and move on.
Instead, group metrics by operational question:
- Request flow: Request rate, success rate, error rate. These three live together because you read them in sequence during an incident.
- Latency: p50, p95, p99 on one panel. Percentiles are comparative; they belong together.
- Resource saturation: CPU, memory, or disk—but only if they affect the same service behavior. Don't mix database CPU with application memory.
- Dependency health: One panel per critical dependency (database, cache, external API). Each shows availability or error rate.
Use the right visualization for the question:
- Time series (line graph): Trends over time. Default choice. Use it unless you have a reason not to.
- Stat (big number): Current value. Use when the absolute number matters more than the trend (e.g., "active connections right now").
- Gauge: Utilization ratio. Only if the upper bound is meaningful (0–100% CPU, 0–disk capacity). Avoid for unbounded metrics.
- Table: Multiple dimensions at a point in time. Use sparingly; most dashboards over-use tables.
- Heatmap: Distribution over time. Useful for latency percentiles or error codes, but harder to read than time series.
Gotcha: Avoid stacking multiple metrics with different units on one panel. Your eye can't compare "requests/sec" and "milliseconds" on the same Y-axis. Split them.
Thresholds and Alerting: Explicit Limits
A threshold without context is a guess. Every alert line on your dashboard should map to a decision: "If this crosses this line, we do X."
Thresholds come in three types:
- SLO-based: Your service commits to p99 latency < 200ms. Plot the 200ms line. Operators see immediately if you're in breach.
- Capacity-based: Your database connection pool is 100. Plot 100 as a warning, 90 as a soft limit. Operators see headroom. For teams also managing their database setup, the configuration guide on devbox.id covers PostgreSQL connection pool tuning in a local development context.
- Anomaly-based: You know normal traffic is 1000–2000 req/s. Plot those bounds. Anything outside is worth investigating.
Color bands (alert regions) work. Use them conservatively:
- Green: Normal operating range.
- Yellow: Degraded but acceptable (e.g., 50% of SLO budget spent).
- Red: SLO breach or critical resource exhaustion.
Avoid rainbow dashboards. Three colors maximum per panel. Too many colors train the eye to ignore them.
Gotcha: Don't alert on the dashboard and in Alertmanager with different thresholds. They drift. The dashboard threshold is for context during an incident. Alerting rules are the source of truth.
Layout and Navigation: Scannable Structure
Dashboards are read top-to-bottom, left-to-right. Organize panels in reading order:
- Top row: System health summary. Request rate, error rate, p99 latency. Operators answer "Is it broken?" in 3 seconds.
- Second row: Component breakdown. Database, cache, queue, external APIs. Answers "Where is the problem?"
- Below: Deep dives. Specific error types, slow query counts, resource breakdowns. Answers "Why is it broken?"
Use row groups (collapsible sections) to hide complexity. A dashboard with 20 panels is unusable. A dashboard with 5 visible panels and 3 collapsible rows is scannable.
Dashboard size: Aim for a single screen without scrolling on a typical monitor (1920×1080). If you need to scroll, you've added too much. Create a second dashboard for the deep dive.
Naming matters. Panel titles should be questions or statements, not metric names:
- Bad: "http_requests_total"
- Good: "Request rate (req/s)"
- Better: "Request rate (req/s) vs. SLO target"
Gotcha: Avoid dashboard sprawl. One dashboard per service is the target. Multiple dashboards for the same service means operators don't know which one to check during an incident. If you need multiple views, use row groups instead.
Query Optimization: Speed Over Completeness
A dashboard that takes 10 seconds to load is a dashboard nobody opens. Operators will use the CLI or logs instead—exactly when you want them looking at metrics.
Optimization checklist:
- Sampling: Use
rate()orincrease()over a 1-minute window, not raw counters. Reduces data points by orders of magnitude. - Aggregation: Push aggregation to the query, not the visualization.
sum(rate(...)) by (service)is faster than plotting 50 series and summing on the client. - Time range: Default to 6 hours, not 30 days. Operators drilling into a 6-hour window can always expand. A 30-day default loads slowly and is hard to read.
- Label cardinality: Don't query on high-cardinality labels (user ID, request ID). Filter on low-cardinality labels (service, environment, region).
- Recording rules: Pre-compute expensive aggregations. If you always query
sum(rate(...)) by (service), create a recording rule.
Test dashboard load time. If the time picker changes and the dashboard takes >3 seconds to refresh, optimize the queries.
Gotcha: Grafana's auto-refresh can hammer your Prometheus or backend if not configured carefully. Set a 30-second minimum refresh interval. Disable auto-refresh by default; let operators enable it when needed.
When This Breaks: Trade-Offs and Limits
Dashboards are not incident playbooks. A dashboard shows the state; a runbook shows what to do. If your dashboard is so complex that operators need a guide to read it, move that context to a runbook.
Dashboards are not logs. If you're trying to debug a specific request or trace a transaction, use logs or a tracing system. Metrics are aggregates; they hide individual failures.
Dashboards are not a substitute for alerting. A dashboard that requires constant human observation is a failed alert. If you're staring at a panel waiting for a number to change, you should have an alert instead.
Dashboards are not static. Review them quarterly. Remove panels nobody uses. Add panels for new services or SLOs. Dashboards that drift from reality become noise.
One-Line Takeaway
Build Grafana dashboards for the questions operators ask during incidents, not for the metrics you have.