How to Set Up Prometheus Alerting in Kubernetes

How to Set Up Prometheus Alerting in Kubernetes

There’s a meaningful difference between “monitoring” and “alerting,” and I didn’t fully appreciate it until I was staring at a beautiful Grafana dashboard, fifteen minutes into an outage, realizing nothing had told me to look at it. Metrics existing somewhere is not the same as being notified when they cross a line that matters. This article is specifically about the Prometheus half of that pipeline — writing good alerting rules — with Alertmanager as the delivery mechanism downstream.

The Alerting Pipeline in Kubernetes

The flow has three distinct layers, and it’s worth being clear about where each one lives:

  1. Prometheus scrapes metrics and continuously evaluates alerting rules (defined as PromQL expressions) against them.
  2. When a rule’s condition is true for the configured duration, Prometheus fires an alert and sends it to Alertmanager.
  3. Alertmanager groups, deduplicates, routes, and delivers notifications (Slack, PagerDuty, email, etc.).

This article focuses on step 1 and 2 — writing rules that are actually useful, not just technically correct.

Prerequisites

Anatomy of an Alerting Rule

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: application-alerts
  namespace: monitoring
  labels:
    release: kube-prometheus
spec:
  groups:
  - name: application.rules
    rules:
    - alert: HighErrorRate
      expr: |
        sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
        /
        sum(rate(http_requests_total[5m])) by (service)
        > 0.05
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "High error rate on {{ $labels.service }}"
        description: "{{ $labels.service }} has a {{ $value | humanizePercentage }} error rate over the last 5 minutes."

Breaking this down:

Alert States

Understanding the three states matters for reading dashboards correctly:

kubectl port-forward -n monitoring svc/prometheus-operated 9090:9090

Check the Alerts tab in the Prometheus UI at localhost:9090/alerts to see live state for every rule.

Essential Kubernetes Alerting Rules

Here’s a practical, production-tested set covering the most common failure modes.

Pod and Container Health

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: kubernetes-pod-alerts
  namespace: monitoring
  labels:
    release: kube-prometheus
spec:
  groups:
  - name: kubernetes-pods
    rules:
    - alert: PodCrashLooping
      expr: rate(kube_pod_container_status_restarts_total[15m]) * 60 * 15 > 3
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} crash looping"
        description: "Container {{ $labels.container }} has restarted more than 3 times in 15 minutes."

    - alert: PodStuckPending
      expr: kube_pod_status_phase{phase="Pending"} == 1
      for: 15m
      labels:
        severity: warning
      annotations:
        summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} stuck Pending"
        description: "Pod has been Pending for over 15 minutes — check scheduling constraints or resource capacity."

    - alert: ContainerOOMKilled
      expr: kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
      for: 0m
      labels:
        severity: warning
      annotations:
        summary: "Container {{ $labels.container }} was OOMKilled"
        description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} container {{ $labels.container }} was killed due to OOM."

    - alert: DeploymentReplicasMismatch
      expr: kube_deployment_spec_replicas != kube_deployment_status_replicas_available
      for: 15m
      labels:
        severity: warning
      annotations:
        summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} replica mismatch"
        description: "Deployment has {{ $labels.spec_replicas }} desired but fewer available for 15+ minutes."

Node Health

    - alert: NodeNotReady
      expr: kube_node_status_condition{condition="Ready", status="true"} == 0
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "Node {{ $labels.node }} not ready"
        description: "Node has been NotReady for over 10 minutes."

    - alert: NodeHighDiskUsage
      expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) < 0.1
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Node {{ $labels.instance }} low on disk"
        description: "Less than 10% disk space remaining on root filesystem."

    - alert: NodeHighMemoryPressure
      expr: kube_node_status_condition{condition="MemoryPressure", status="true"} == 1
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Node {{ $labels.node }} under memory pressure"

Resource Saturation

    - alert: NamespaceCPUQuotaHigh
      expr: |
        sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace)
        /
        sum(kube_resourcequota{resource="cpu", type="hard"}) by (namespace)
        > 0.9
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Namespace {{ $labels.namespace }} approaching CPU quota"
        description: "CPU usage is above 90% of the namespace's resource quota."

    - alert: PersistentVolumeUsageHigh
      expr: (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.9
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} almost full"
        description: "Volume is over 90% full in namespace {{ $labels.namespace }}."

API Server and Control Plane

    - alert: APIServerHighLatency
      expr: |
        histogram_quantile(0.99, sum(rate(apiserver_request_duration_seconds_bucket{verb!="WATCH"}[5m])) by (le, verb))
        > 1
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Kubernetes API server high latency"
        description: "99th percentile request latency for {{ $labels.verb }} exceeds 1 second."

    - alert: APIServerErrorsHigh
      expr: |
        sum(rate(apiserver_request_total{code=~"5.."}[5m]))
        /
        sum(rate(apiserver_request_total[5m]))
        > 0.05
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "High API server 5xx error rate"

Apply everything:

kubectl apply -f kubernetes-pod-alerts.yaml

Confirm Prometheus picked the rules up:

kubectl port-forward -n monitoring svc/prometheus-operated 9090:9090

Check localhost:9090/rules to see all loaded rule groups.

Recording Rules: Precomputing Expensive Queries

Complex PromQL expressions evaluated repeatedly (by dashboards, alerts, or both) are wasteful to recompute from scratch each time. Recording rules precompute them into new time series:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: recording-rules
  namespace: monitoring
  labels:
    release: kube-prometheus
spec:
  groups:
  - name: recording.rules
    interval: 30s
    rules:
    - record: service:http_error_rate:ratio_5m
      expr: |
        sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
        /
        sum(rate(http_requests_total[5m])) by (service)

Now your alerting rule (and any Grafana dashboard) can reference service:http_error_rate:ratio_5m directly, cheaper to query and consistent across every consumer.

    - alert: HighErrorRate
      expr: service:http_error_rate:ratio_5m > 0.05
      for: 10m
      labels:
        severity: critical

Testing Alerting Rules Before Deploying

Use promtool to unit test rules without waiting for real conditions to occur in production:

# tests.yaml
rule_files:
  - kubernetes-pod-alerts.yaml
evaluation_interval: 1m
tests:
  - interval: 1m
    input_series:
      - series: 'kube_pod_status_phase{namespace="default", pod="test-pod", phase="Pending"}'
        values: '1x20'
    alert_rule_test:
      - eval_time: 16m
        alertname: PodStuckPending
        exp_alerts:
          - exp_labels:
              severity: warning
              namespace: default
              pod: test-pod
            exp_annotations:
              summary: "Pod default/test-pod stuck Pending"
promtool test rules tests.yaml

This catches broken for durations, wrong thresholds, or PromQL syntax errors before they ever hit a real cluster.

CI/CD Integration

Lint and validate rules as part of your pipeline before merging:

promtool check rules kubernetes-pod-alerts.yaml
# .github/workflows/validate-alerts.yaml
name: Validate Prometheus Rules
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Install promtool
      run: |
        wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
        tar xzf prometheus-2.53.0.linux-amd64.tar.gz
        sudo mv prometheus-2.53.0.linux-amd64/promtool /usr/local/bin/
    - name: Validate rules
      run: promtool check rules monitoring/*.yaml

Multi-Window, Multi-Burn-Rate Alerts (SLO-Based Alerting)

Simple threshold alerts work fine for infrastructure-level issues, but for services with a defined SLO (Service Level Objective), a more sophisticated technique — multi-window, multi-burn-rate alerting — catches problems faster while generating fewer false positives than a flat threshold. The idea: alert when your error budget is burning fast enough over a short window AND a longer window simultaneously, which distinguishes real incidents from brief blips far better than either window alone.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: slo-burn-rate-alerts
  namespace: monitoring
  labels:
    release: kube-prometheus
spec:
  groups:
  - name: slo.burnrate
    rules:
    - alert: ErrorBudgetBurnRateHigh
      expr: |
        (
          sum(rate(http_requests_total{status=~"5..", service="checkout"}[1h]))
          /
          sum(rate(http_requests_total{service="checkout"}[1h]))
          > (14.4 * 0.001)
        )
        and
        (
          sum(rate(http_requests_total{status=~"5..", service="checkout"}[5m]))
          /
          sum(rate(http_requests_total{service="checkout"}[5m]))
          > (14.4 * 0.001)
        )
      labels:
        severity: critical
      annotations:
        summary: "Checkout service burning error budget fast"
        description: "Error budget burn rate over both 1h and 5m windows exceeds 14.4x — at this rate the monthly SLO budget will be exhausted in hours, not weeks."

The 14.4 multiplier is a standard reference value (borrowed from widely published SRE burn-rate alerting guidance) representing a burn rate that would exhaust an entire month’s error budget in about 2 days if sustained — tune it against your own SLO target and budget window rather than copying it verbatim.

Silencing Known Issues During Deployments

A practical pattern for CI/CD pipelines is automatically creating a short-lived Alertmanager silence during a deployment window, so expected transient errors (brief 5xx spikes during a rolling update, for instance) don’t trigger a page for something already known and being actively managed:

amtool silence add \
  service="checkout" \
  --duration="10m" \
  --comment="Rolling deployment in progress - build ${CI_BUILD_ID}" \
  --alertmanager.url=http://alertmanager.monitoring.svc:9093
# .gitlab-ci.yml snippet
deploy:
  script:
    - ./scripts/create-deploy-silence.sh
    - kubectl apply -f deployment.yaml
    - kubectl rollout status deployment/checkout --timeout=300s

This is a deliberate, time-boxed, auditable silence tied to a specific deployment — very different from someone manually muting an alert indefinitely and forgetting to unmute it, which is one of the more common ways real incidents go unnoticed in practice.

Common Mistakes

Best Practices

Summary

Good Prometheus alerting comes down to writing PromQL expressions that reflect real user-facing symptoms, giving them appropriate for durations to avoid noise, labeling them consistently for routing, and testing them with promtool before they ever reach production. Combined with recording rules for efficiency and CI validation to catch mistakes early, this turns your metrics pipeline into something that actually tells the right people the right thing at the right time — which is the entire point of collecting metrics in the first place.

References

Exit mobile version