<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: devtocash</title>
    <description>The latest articles on DEV Community by devtocash (@devtocash).</description>
    <link>https://dev.to/devtocash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4002560%2F6ede7663-58de-4c06-a945-113cc245c5c6.png</url>
      <title>DEV Community: devtocash</title>
      <link>https://dev.to/devtocash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devtocash"/>
    <language>en</language>
    <item>
      <title>SLI vs SLO vs SLA: Real SRE Guide with Examples</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:07:24 +0000</pubDate>
      <link>https://dev.to/devtocash/sli-vs-slo-vs-sla-real-sre-guide-with-examples-2jg2</link>
      <guid>https://dev.to/devtocash/sli-vs-slo-vs-sla-real-sre-guide-with-examples-2jg2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/sli-vs-slo-vs-sla-real-sre-guide-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every engineering team talks about uptime. "We need five nines." "Our SLA is 99.9%." "We hit our SLO this quarter."&lt;/p&gt;

&lt;p&gt;But ask most engineers what an SLO actually means — mathematically, operationally, legally — and the confidence drops fast. Ask them what SLI their SLO is based on, and you will get a blank stare or a hand-wavy "uh, latency, I guess."&lt;/p&gt;

&lt;p&gt;This confusion has real costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Teams define SLOs against meaningless SLIs — like "overall uptime" of a system that has 47 microservices, five of which are critical and the rest are decorative.&lt;/li&gt;
&lt;li&gt;They set unrealistic targets because "five nines sounds good" without understanding the reliability budget.&lt;/li&gt;
&lt;li&gt;They confuse SLOs (internal reliability targets) with SLAs (external, often legal commitments) and end up over-engineering for contracts that don't require it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide fixes that. You will learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The precise definition of &lt;strong&gt;SLI&lt;/strong&gt;, &lt;strong&gt;SLO&lt;/strong&gt;, and &lt;strong&gt;SLA&lt;/strong&gt; — and the differences that matter.&lt;/li&gt;
&lt;li&gt;How to choose the &lt;strong&gt;right SLIs&lt;/strong&gt; for real production services.&lt;/li&gt;
&lt;li&gt;How to set &lt;strong&gt;achievable SLOs&lt;/strong&gt; with math that works.&lt;/li&gt;
&lt;li&gt;How &lt;strong&gt;SLAs&lt;/strong&gt; relate to SLOs (spoiler: they are not the same).&lt;/li&gt;
&lt;li&gt;Real &lt;strong&gt;Prometheus examples&lt;/strong&gt; for measuring SLIs and burning down SLOs.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;common mistakes&lt;/strong&gt; teams make — and how to avoid them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start with a single analogy that makes everything click.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Analogy: Speedometer, Speed Limit, Traffic Ticket
&lt;/h2&gt;

&lt;p&gt;Think of a car journey.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SLI&lt;/strong&gt; is the &lt;strong&gt;speedometer&lt;/strong&gt;. It measures something — how fast you are going right now. It is raw data. "The 95th percentile latency of the checkout endpoint over the last 5 minutes was 342 ms." That is an SLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLO&lt;/strong&gt; is the &lt;strong&gt;speed limit&lt;/strong&gt;. It says: "95th percentile latency should be under 500 ms over a 30-day rolling window." That is your target. You can choose to drive faster (higher risk) or slower (more cautious).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLA&lt;/strong&gt; is the &lt;strong&gt;traffic ticket&lt;/strong&gt;. If you violate the speed limit for too long, you pay a penalty. An SLA says: "If 95th percentile latency exceeds 500 ms for more than 0.1% of the month, we credit the customer 5% of their bill."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The speedometer tells you the current value. The speed limit tells you where you want to be. The ticket tells you what happens if you fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  SLI: The Raw Measurement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definition
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;Service Level Indicator (SLI)&lt;/strong&gt; is a carefully defined quantitative measure of some aspect of the service you care about.&lt;/p&gt;

&lt;p&gt;The key words are "carefully defined." A bad SLI definition leads to bad SLOs. A good SLI definition is specific, measurable, and meaningful to users.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Golden Signals
&lt;/h3&gt;

&lt;p&gt;Google's SRE literature defines four golden signals. Every service should have SLIs in at least these categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Example SLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How long it takes to respond&lt;/td&gt;
&lt;td&gt;"95th percentile HTTP response time for GET /api/orders"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much demand is placed on the system&lt;/td&gt;
&lt;td&gt;"Requests per second to the web frontend"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How many requests fail&lt;/td&gt;
&lt;td&gt;"Ratio of HTTP 500 responses to total requests"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saturation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How "full" the system is&lt;/td&gt;
&lt;td&gt;"CPU utilization percentage across the cluster"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams stop at latency and errors. That is a good start but incomplete. Saturation, in particular, is a leading indicator — if you only measure it when errors spike, you are always reacting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing Good SLIs
&lt;/h3&gt;

&lt;p&gt;A good SLI has three properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User-visible.&lt;/strong&gt; Measure what the user experiences, not what the infrastructure is doing. If the database is having replication lag but users are not affected, that is an ops concern, not an SLI. If users &lt;em&gt;are&lt;/em&gt; affected (stale data, timeouts), then it becomes an SLI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measurable consistently.&lt;/strong&gt; You need to collect the same measurement the same way every time. "Latency" is not an SLI. "p95 of the last 30 seconds of HTTP request duration measured server-side" is an SLI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Actionable.&lt;/strong&gt; If the SLI goes bad, someone should know what to do about it. "Number of times the database is restarted" is measurable but, alone, tells you nothing about what to fix.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Examples of Good vs Bad SLIs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;❌ Bad SLI&lt;/th&gt;
&lt;th&gt;Why it's bad&lt;/th&gt;
&lt;th&gt;✅ Good SLI&lt;/th&gt;
&lt;th&gt;Why it's better&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"System uptime"&lt;/td&gt;
&lt;td&gt;A monolith in a VM is up? That tells you nothing about responsiveness.&lt;/td&gt;
&lt;td&gt;"Ratio of successful HTTP requests (2xx) to total requests over 1-minute windows"&lt;/td&gt;
&lt;td&gt;Measures what users actually experience.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Average latency"&lt;/td&gt;
&lt;td&gt;Averages hide outliers. 99% of requests in 10ms, 1% in 30s — average is still ~300ms, looks fine.&lt;/td&gt;
&lt;td&gt;"p99 HTTP latency over 5-minute rolling windows"&lt;/td&gt;
&lt;td&gt;Captures the tail, which is what users feel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"CPU usage"&lt;/td&gt;
&lt;td&gt;CPU at 100% does not necessarily mean poor user experience.&lt;/td&gt;
&lt;td&gt;"p99 latency when CPU &amp;gt; 80% vs p99 latency when CPU &amp;lt; 80%"&lt;/td&gt;
&lt;td&gt;Ties infrastructure to user experience.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Defining SLIs in Prometheus
&lt;/h3&gt;

&lt;p&gt;Assume you have a service exposing metrics via &lt;code&gt;/metrics&lt;/code&gt;. To measure request duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Request duration histogram — already exposed by your instrumentation
http_request_duration_seconds_bucket{job="checkout-service", le="0.1"}
http_request_duration_seconds_bucket{job="checkout-service", le="0.25"}
http_request_duration_seconds_bucket{job="checkout-service", le="0.5"}
http_request_duration_seconds_bucket{job="checkout-service", le="1.0"}
http_request_duration_seconds_bucket{job="checkout-service", le="+Inf"}
http_request_duration_seconds_count{job="checkout-service"}
http_request_duration_seconds_sum{job="checkout-service"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;strong&gt;latency SLI&lt;/strong&gt; at p99 over the last 5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;histogram_quantile(
  0.99,
  rate(http_request_duration_seconds_bucket{job="checkout-service"}[5m])
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;strong&gt;error ratio SLI&lt;/strong&gt; over the last 5 minutes — the fraction of requests that returned HTTP 5xx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  sum(rate(http_requests_total{job="checkout-service", status=~"5.."}[5m]))
  /
  sum(rate(http_requests_total{job="checkout-service"}[5m]))
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;strong&gt;availability SLI&lt;/strong&gt; — the fraction of 1-minute windows where error ratio was under a threshold (e.g., under 1%):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;avg_over_time(
  (
    (
      sum(rate(http_requests_total{job="checkout-service", status=~"5.."}[1m]))
      /
      sum(rate(http_requests_total{job="checkout-service"}[1m]))
    ) &amp;lt; 0.01
  )[5m:]
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This last one is important. "Availability" measured as "number of good windows / total windows" is the standard approach used by Google (it is called &lt;em&gt;SLI burn rate&lt;/em&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  SLO: The Target
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definition
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;Service Level Objective (SLO)&lt;/strong&gt; is a target value or range for an SLI over a specified measurement window.&lt;/p&gt;

&lt;p&gt;Example: "&lt;strong&gt;p99 latency of the checkout service stays under 500 ms&lt;/strong&gt; for 99.9% of 1-minute windows in any rolling 30-day period."&lt;/p&gt;

&lt;p&gt;That sentence contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;SLI&lt;/strong&gt; (p99 latency)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;threshold&lt;/strong&gt; (under 500 ms)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;measurement window&lt;/strong&gt; (1-minute windows)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;compliance period&lt;/strong&gt; (30 rolling days)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;target&lt;/strong&gt; (99.9% of windows good)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Error Budget
&lt;/h3&gt;

&lt;p&gt;The most important concept in SRE. If your SLO says "99.9% good," then &lt;strong&gt;0.1% of measurement windows can be bad&lt;/strong&gt;. That 0.1% is your &lt;strong&gt;error budget&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a 30-day period:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total 1-minute windows: 30 days × 1440 minutes/day = &lt;strong&gt;43,200 windows&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Allowed bad windows at 99.9%: 43,200 × 0.001 = &lt;strong&gt;43 windows&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Allowed bad windows at 99.99%: 43,200 × 0.0001 = &lt;strong&gt;4 windows&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Allowed bad windows at 99.999% (five nines): 43,200 × 0.00001 = &lt;strong&gt;0.4 windows&lt;/strong&gt; (meaning you can barely afford any outage)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLO Target&lt;/th&gt;
&lt;th&gt;Minutes you can be down per month&lt;/th&gt;
&lt;th&gt;Realistic?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;99% ("one nine")&lt;/td&gt;
&lt;td&gt;432 min (7.2 hours)&lt;/td&gt;
&lt;td&gt;Easy for most services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.9% ("three nines")&lt;/td&gt;
&lt;td&gt;43 min&lt;/td&gt;
&lt;td&gt;Achievable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.95%&lt;/td&gt;
&lt;td&gt;22 min&lt;/td&gt;
&lt;td&gt;Good for critical services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.99% ("four nines")&lt;/td&gt;
&lt;td&gt;4.3 min&lt;/td&gt;
&lt;td&gt;Hard — requires automation and redundancy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99.999% ("five nines")&lt;/td&gt;
&lt;td&gt;26 seconds&lt;/td&gt;
&lt;td&gt;Almost impossible without multi-region active-active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The error budget changes team behaviour. When the budget is healthy, teams deploy with confidence. When it is running low, teams become conservative — they throttle deployments, add testing, strengthen canary checks. This is the &lt;strong&gt;error budget policy&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting SLOs: A Practical Approach
&lt;/h3&gt;

&lt;p&gt;Do not start with 99.9% because "it sounds right." Start with data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Collect your SLIs for at least 2–4 weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you set a target, you need to know where you are. Run Prometheus, instrument your services, and let the data accumulate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add OpenTelemetry instrumentation to your app&lt;/span&gt;
&lt;span class="c"&gt;# Example: Python with OpenTelemetry&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;opentelemetry-distro opentelemetry-exporter-prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Determine the worst acceptable performance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask product owners: "What is the slowest response time that would make you consider the service broken?" Not the ideal speed — the &lt;em&gt;worst acceptable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For an API: "If p99 latency exceeds 1 second for more than 5 minutes, users complain."&lt;br&gt;
For a payment service: "Any failed transaction is unacceptable — 100% of requests must succeed."&lt;br&gt;
For a background job: "If it doesn't complete within 2 hours, the morning report is late."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Add headroom.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your SLO should be stricter than the absolute worst acceptable. If "p99 under 1 second" is the hard limit, set your SLO at p99 under 800 ms. If zero failed transactions is the ideal, set your SLO at 99.95% success rate (giving you a small error budget to handle bad deployments).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Run it for a month and adjust.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first SLO you set will be wrong. That is normal. Track it for 30 days, see how often you burn through the budget, and adjust.&lt;/p&gt;
&lt;h3&gt;
  
  
  Monitoring SLOs in Prometheus
&lt;/h3&gt;

&lt;p&gt;You need to track your burn rate — how quickly you are consuming the error budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# SLO compliance for p99 latency under 500ms
# Good for 30-day period, evaluating over 1-minute windows

# Step 1: Which 1-minute windows are "bad"?
(
  histogram_quantile(
    0.99,
    rate(http_request_duration_seconds_bucket{job="checkout-service"}[1m])
  )
  &amp;gt; 0.5  # 500ms
)

# Step 2: Error budget consumed over last 30 days
1 - (
  avg_over_time(
    (
      histogram_quantile(
        0.99,
        rate(http_request_duration_seconds_bucket{job="checkout-service"}[1m])
      )
      &amp;lt;= 0.5
    )[30d:]
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A value of &lt;code&gt;0.002&lt;/code&gt; means you have consumed 0.2% of your error budget in the last 30 days. If your SLO is 99.9% (0.1% budget), you are at 200% consumption — in trouble.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerting on Error Budget Burn Rate
&lt;/h3&gt;

&lt;p&gt;Do not alert on raw latency spikes. Alert on &lt;strong&gt;the rate at which you are burning through your error budget&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Burn rate&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 0.5x&lt;/td&gt;
&lt;td&gt;Budget is being consumed slowly — normal operations.&lt;/td&gt;
&lt;td&gt;No alert.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;Exactly on target.&lt;/td&gt;
&lt;td&gt;Monitor but no action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2x&lt;/td&gt;
&lt;td&gt;Consuming budget twice as fast as planned.&lt;/td&gt;
&lt;td&gt;Investigate within 24 hours.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5x&lt;/td&gt;
&lt;td&gt;Serious degradation.&lt;/td&gt;
&lt;td&gt;Page the on-call engineer within 30 minutes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10x+&lt;/td&gt;
&lt;td&gt;Critical incident.&lt;/td&gt;
&lt;td&gt;War room. Immediate response.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example Prometheus alert rule for a 5x burn rate sustained over 30 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slo_alerts&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighErrorBudgetBurnRate&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;1 - (&lt;/span&gt;
              &lt;span class="s"&gt;sum(rate(http_requests_total{job="checkout-service", status=~"5.."}[30m]))&lt;/span&gt;
              &lt;span class="s"&gt;/&lt;/span&gt;
              &lt;span class="s"&gt;sum(rate(http_requests_total{job="checkout-service"}[30m]))&lt;/span&gt;
            &lt;span class="s"&gt;)&lt;/span&gt;
          &lt;span class="s"&gt;) &amp;lt; 0.90  # 90% success in last 30 minutes&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;page&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;budget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;burning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;5x&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;humanizePercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  SLA: The Contract
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Definition
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;Service Level Agreement (SLA)&lt;/strong&gt; is a formal, legally enforceable contract between a service provider and a customer. It specifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SLIs and SLOs the provider commits to.&lt;/li&gt;
&lt;li&gt;The measurement methodology.&lt;/li&gt;
&lt;li&gt;The penalties or credits if the SLO is not met.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SLAs are &lt;strong&gt;external&lt;/strong&gt;. SLOs are &lt;strong&gt;internal&lt;/strong&gt;. That distinction is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  SLA vs SLO: The Key Differences
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SLO&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal engineering team&lt;/td&gt;
&lt;td&gt;External customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guide operational decisions&lt;/td&gt;
&lt;td&gt;Contractual commitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consequence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Process changes, deployment throttle&lt;/td&gt;
&lt;td&gt;Financial penalties, legal liability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strictness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You can miss an SLO temporarily&lt;/td&gt;
&lt;td&gt;Missing an SLA costs real money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can be adjusted weekly&lt;/td&gt;
&lt;td&gt;Hard to change — written into contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Measurement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usually tighter than SLA&lt;/td&gt;
&lt;td&gt;Usually looser than SLO&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The SLO Margin
&lt;/h3&gt;

&lt;p&gt;Smart teams set their internal SLO &lt;strong&gt;stricter&lt;/strong&gt; than their external SLA. The gap is your safety margin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SLA to customer:  99.9% availability
Internal SLO:     99.95% availability &amp;lt;-- buffer of 0.05%
                  ^^^^^^^^
                  You have 22 min of downtime allowance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This margin means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You will miss the internal SLO long before you miss the external SLA.&lt;/li&gt;
&lt;li&gt;You have time to react before customers experience a contract violation.&lt;/li&gt;
&lt;li&gt;You can keep your infrastructure simpler (and cheaper) than if you had to guarantee the strictest target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When SLAs Go Wrong
&lt;/h3&gt;

&lt;p&gt;The most common SLA mistake: &lt;strong&gt;committing to something you cannot measure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Example: "We guarantee p99 latency under 100ms." Sounds great. But if you measure latency from your load balancer (inside the data centre) and the customer measures it from their browser in rural Australia — those are different numbers. Your SLA needs to specify exactly &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how&lt;/em&gt; latency is measured.&lt;/p&gt;

&lt;p&gt;Second mistake: &lt;strong&gt;committing to an SLO that is too tight for your architecture.&lt;/strong&gt; A single-region deployment cannot realistically offer five nines. A database with a single primary cannot survive a failover without a brief blip. Your SLA must reflect your architecture's actual failure modes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together: A Real Example
&lt;/h2&gt;

&lt;p&gt;Let us walk through a real scenario. You run a payment service called &lt;code&gt;payment-svc&lt;/code&gt; that processes credit card transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define Your SLIs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Payment service SLIs&lt;/span&gt;
&lt;span class="na"&gt;sli_latency_p99&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p99&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/api/charge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5-minute&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;windows"&lt;/span&gt;
&lt;span class="na"&gt;sli_error_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ratio&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;HTTP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5xx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;responses&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5-minute&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;windows"&lt;/span&gt;
&lt;span class="na"&gt;sli_throughput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Successful&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transactions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;second"&lt;/span&gt;
&lt;span class="na"&gt;sli_saturation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gRPC&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;utilization&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;percentage"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Set Your Internal SLOs
&lt;/h3&gt;

&lt;p&gt;Based on historical data and product requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Month-rolling SLOs for payment-svc&lt;/span&gt;
&lt;span class="na"&gt;slo_latency_p99&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;99.9%&lt;/span&gt;             &lt;span class="c1"&gt;# 0.1% bad windows allowed&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;300ms&lt;/span&gt;          &lt;span class="c1"&gt;# p99 under 300ms&lt;/span&gt;

&lt;span class="na"&gt;slo_error_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;99.99%&lt;/span&gt;            &lt;span class="c1"&gt;# 0.01% bad windows allowed&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.001&lt;/span&gt;          &lt;span class="c1"&gt;# less than 0.1% error rate per window&lt;/span&gt;

&lt;span class="na"&gt;slo_uptime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;99.95%&lt;/span&gt;            &lt;span class="c1"&gt;# based on simple request success count over 30d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Define Your External SLA
&lt;/h3&gt;

&lt;p&gt;Based on business requirements and what competitors offer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Customer SLA — intentionally looser than internal SLOs&lt;/span&gt;
&lt;span class="na"&gt;sla_uptime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;99.9%&lt;/span&gt;    &lt;span class="c1"&gt;# 43 minutes downtime per month&lt;/span&gt;
&lt;span class="na"&gt;sla_error_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;99.9%&lt;/span&gt;    &lt;span class="c1"&gt;# 0.1% error rate&lt;/span&gt;
&lt;span class="na"&gt;penalty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5%&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;monthly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0.1%&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;below&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SLA,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;max&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;50%&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the gap: &lt;strong&gt;internal SLO for error rate is 99.99%, external SLA is 99.9%&lt;/strong&gt;. That gives the team a 10× margin to absorb incidents before customers are impacted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Measure and Alert
&lt;/h3&gt;

&lt;p&gt;Your monitoring dashboard shows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time period&lt;/th&gt;
&lt;th&gt;Good windows&lt;/th&gt;
&lt;th&gt;Total windows&lt;/th&gt;
&lt;th&gt;Compliance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Last 24h&lt;/td&gt;
&lt;td&gt;1,439&lt;/td&gt;
&lt;td&gt;1,440&lt;/td&gt;
&lt;td&gt;99.93%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last 7d&lt;/td&gt;
&lt;td&gt;10,067&lt;/td&gt;
&lt;td&gt;10,080&lt;/td&gt;
&lt;td&gt;99.87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last 30d&lt;/td&gt;
&lt;td&gt;43,156&lt;/td&gt;
&lt;td&gt;43,200&lt;/td&gt;
&lt;td&gt;99.90%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 7-day window is 99.87% — below the 99.9% SLO. The team knows they need to investigate. But the 30-day SLA target (99.9%) is barely being met. No customer penalty yet, but one more incident will push it under.&lt;/p&gt;

&lt;p&gt;The alert rule catches this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Alert if 7-day compliance drops below 99.9%
(
  1 - avg_over_time(
    (
      sum(rate(http_requests_total{job="payment-svc", status=~"5.."}[1m]))
      /
      sum(rate(http_requests_total{job="payment-svc"}[1m]))
    ) &amp;gt; 0.001
  [7d:])
)
&amp;gt; 0.001  # More than 0.1% bad windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The team pages, investigates, finds a newly deployed service that is not properly handling database connection timeouts, rolls back the deployment, and the error budget recovers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes and How to Avoid Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Too Many SLIs
&lt;/h3&gt;

&lt;p&gt;Teams measure everything — p50, p90, p95, p99, p99.9 of every endpoint, error rates by status code, by region, by instance. The result: alert fatigue and no clear picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pick 3–5 SLIs per critical service. The golden signals are a good starting point. Add more only when you find a specific gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: SLOs Based on Averages
&lt;/h3&gt;

&lt;p&gt;"Average latency" and "average availability" hide the real story. A service can have 99.9% "average uptime" over a month while being completely down for individual users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use percentiles (p95, p99) for latency. Use the "good windows" approach for availability — count windows where the service was good, not the average value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Identical SLOs for Every Service
&lt;/h3&gt;

&lt;p&gt;A critical payment service and a background report generator should not share the same target. If you set all services at 99.99%, you are either over-engineering the background job or under-engineering the payment service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Classify services by criticality — &lt;strong&gt;Tier 1&lt;/strong&gt; (customer-facing, revenue-critical), &lt;strong&gt;Tier 2&lt;/strong&gt; (important but not urgent), &lt;strong&gt;Tier 3&lt;/strong&gt; (internal tools, batch jobs). Set different SLOs per tier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;SLO target&lt;/th&gt;
&lt;th&gt;On-call response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Payment service, API gateway&lt;/td&gt;
&lt;td&gt;99.95%&lt;/td&gt;
&lt;td&gt;15-minute page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Reporting service, admin dashboard&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;1-hour page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Internal data sync, ETL&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;Next business day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Mistake 4: Setting SLOs Without Error Budget Policy
&lt;/h3&gt;

&lt;p&gt;An SLO without an error budget policy is just a dashboard number. If you miss it, nothing happens differently. That defeats the entire purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Write a one-page error budget policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who decides when to stop deployments (typically the SRE lead or on-call).&lt;/li&gt;
&lt;li&gt;At what budget level deployments stop (e.g., "deployments frozen when budget &amp;lt; 10% remaining").&lt;/li&gt;
&lt;li&gt;How the budget resets (e.g., at the start of each calendar month).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mistake 5: Confusing SLA with SLO
&lt;/h3&gt;

&lt;p&gt;Committing the same target to customers that you use internally means zero margin for error. One incident = one missed SLA = financial penalties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Always set your internal SLO 5–10× tighter than your external SLA. The cost of running slightly better infrastructure is almost always less than the cost of paying SLA penalties.&lt;/p&gt;




&lt;h2&gt;
  
  
  Actionable Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with 3–5 SLIs per critical service.&lt;/strong&gt; Use the golden signals (latency, traffic, errors, saturation). Add more only when you find gaps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use percentiles, not averages.&lt;/strong&gt; p99 latency and the "good windows" approach for availability. Averages will lie to you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set SLOs based on data, not intuition.&lt;/strong&gt; Collect SLIs for at least 2 weeks before defining targets. The first SLO you set will be wrong — adjust it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always set internal SLOs tighter than external SLAs.&lt;/strong&gt; The gap is your safety margin. At least 2× on error budgets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alert on error budget burn rate, not raw metrics.&lt;/strong&gt; A latency spike that lasts 30 seconds is noise. A 10× burn rate sustained for 30 minutes is an incident.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write an error budget policy.&lt;/strong&gt; Define explicitly: when deployments stop, who decides, and how the budget resets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Classify services by criticality.&lt;/strong&gt; Tier 1 (99.95%), Tier 2 (99.9%), Tier 3 (99%). Do not apply one SLO to everything.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Need to implement SLIs in your infrastructure? Check out our *&lt;/em&gt;&lt;a href="https://dev.to/"&gt;Prometheus Monitoring Setup Guide&lt;/a&gt;** and &lt;strong&gt;&lt;a href="https://dev.to/"&gt;OpenTelemetry Tutorial&lt;/a&gt;&lt;/strong&gt; for production-ready instrumentation.*&lt;/p&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/sli-vs-slo-vs-sla-real-sre-guide-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>sre</category>
      <category>sli</category>
      <category>slo</category>
      <category>sla</category>
    </item>
    <item>
      <title>Kubernetes Pod Stuck in Pending (FailedScheduling): How to Fix It</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Thu, 16 Jul 2026 01:10:11 +0000</pubDate>
      <link>https://dev.to/devtocash/kubernetes-pod-stuck-in-pending-failedscheduling-how-to-fix-it-11b4</link>
      <guid>https://dev.to/devtocash/kubernetes-pod-stuck-in-pending-failedscheduling-how-to-fix-it-11b4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/2026-07-16-kubernetes-pod-pending-failedscheduling-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Pending actually means
&lt;/h2&gt;

&lt;p&gt;A pod stuck in &lt;code&gt;Pending&lt;/code&gt; has been accepted by the API server but has &lt;em&gt;no node to run on&lt;/em&gt;. The kube-scheduler looked at every node in the cluster, none satisfied the pod's constraints, and it gave up for now — recording exactly why in a &lt;code&gt;FailedScheduling&lt;/code&gt; event. Unlike a crash, nothing is wrong with your container yet. The image is never pulled, the process never starts. The problem is entirely about placement.&lt;/p&gt;

&lt;p&gt;This is a different failure from the "my container won't stay up" family — &lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://devtocash.com/blog/2026-07-14-kubernetes-crashloopbackoff-fix" rel="noopener noreferrer"&gt;&lt;code&gt;CrashLoopBackOff&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://devtocash.com/blog/2026-07-15-kubernetes-oomkilled-exit-code-137-fix" rel="noopener noreferrer"&gt;&lt;code&gt;OOMKilled&lt;/code&gt;&lt;/a&gt; all mean a node accepted the pod and &lt;em&gt;then&lt;/em&gt; something broke. &lt;code&gt;Pending&lt;/code&gt; means no node would take it in the first place. The good news: like the others, the scheduler tells you the exact reason. You never have to guess. This is the sequence I run to place a stuck pod, usually in a couple of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Read the scheduler's verdict
&lt;/h2&gt;

&lt;p&gt;Don't theorize from a &lt;code&gt;Pending&lt;/code&gt; status. Read the event the scheduler already wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f4c8b6-xk2mn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll to &lt;code&gt;Events&lt;/code&gt; at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Warning  FailedScheduling  default-scheduler
    0/5 nodes are available: 2 Insufficient cpu,
    2 node(s) had untolerated taint {dedicated: gpu},
    1 node(s) didn't match Pod's node affinity/selector.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line accounts for every node in the cluster and why each one rejected the pod. The scheduler evaluates nodes through filter predicates, and the message is the tally of which predicate failed where. Map the phrase to a root cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message fragment&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Go to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Insufficient cpu&lt;/code&gt; / &lt;code&gt;Insufficient memory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requests don't fit any node's free capacity&lt;/td&gt;
&lt;td&gt;Step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node(s) had untolerated taint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node is cordoned or reserved; pod lacks a toleration&lt;/td&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;didn't match Pod's node affinity/selector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nodeSelector&lt;/code&gt;/affinity points at labels no node has&lt;/td&gt;
&lt;td&gt;Step 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;had volume node affinity conflict&lt;/code&gt; / &lt;code&gt;unbound ... PersistentVolumeClaims&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Storage can't bind, or PV is zone-locked away from capacity&lt;/td&gt;
&lt;td&gt;Step 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;too many pods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Node hit its max-pods cap (often ENI/IP limits)&lt;/td&gt;
&lt;td&gt;Step 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;didn't match pod topology spread constraints&lt;/code&gt; / anti-affinity&lt;/td&gt;
&lt;td&gt;Spread/anti-affinity rules can't be satisfied&lt;/td&gt;
&lt;td&gt;Step 7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read this first and everything below collapses to one path. Often you'll see several fragments at once — fix them in order of how many nodes each blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Insufficient CPU or memory
&lt;/h2&gt;

&lt;p&gt;The most common cause. &lt;code&gt;Insufficient cpu&lt;/code&gt; doesn't mean the node is &lt;em&gt;using&lt;/em&gt; all its CPU — it means the sum of pod &lt;strong&gt;requests&lt;/strong&gt; already scheduled there leaves less than your pod asks for. Scheduling is based on requests, not live usage. A node at 10% actual CPU can still reject a pod if its requests are already reserved.&lt;/p&gt;

&lt;p&gt;First, see what the pod is asking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].resources.requests}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"4Gi"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then see what's actually free on the nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node ip-10-0-1-42 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A6&lt;/span&gt; &lt;span class="s2"&gt;"Allocated resources"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Resource   Requests      Limits
  cpu        3500m (87%)   6 (150%)
  memory     6Gi (78%)     10Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If free CPU is below your request on every node, you have two fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The request is oversized.&lt;/strong&gt; A pod asking for &lt;code&gt;2&lt;/code&gt; full cores that actually uses &lt;code&gt;150m&lt;/code&gt; is starving the scheduler of placements it could otherwise make. Right-size the request to real usage — the same discipline that keeps the &lt;a href="https://devtocash.com/blog/2026-03-10-kubernetes-cost-optimization-guide" rel="noopener noreferrer"&gt;Kubernetes bill down&lt;/a&gt;. The VPA in recommendation mode will suggest values from observed usage; see how it fits alongside HPA and KEDA in the &lt;a href="https://devtocash.com/blog/2026-06-28-kubernetes-pod-autoscaling-hpa-vpa-and-keda-explained" rel="noopener noreferrer"&gt;pod autoscaling guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cluster is genuinely full.&lt;/strong&gt; Every node is legitimately packed. You need more capacity — enable the &lt;strong&gt;cluster autoscaler&lt;/strong&gt; (or Karpenter) so a &lt;code&gt;Pending&lt;/code&gt; pod that can't fit triggers a new node instead of waiting forever. Confirm it's reacting:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system logs &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cluster-autoscaler &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; scale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the autoscaler logs &lt;code&gt;pod didn't trigger scale-up: max node group size reached&lt;/code&gt;, you've hit your node-group ceiling — raise the max, or the pod stays &lt;code&gt;Pending&lt;/code&gt; indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Untolerated taints
&lt;/h2&gt;

&lt;p&gt;A message like &lt;code&gt;node(s) had untolerated taint {node.kubernetes.io/unschedulable}&lt;/code&gt; means the target nodes are &lt;em&gt;tainted&lt;/em&gt; and your pod carries no matching toleration. Taints repel pods unless the pod explicitly tolerates them. See the taint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;NAME:.metadata.name,TAINTS:.spec.taints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two very different situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The node is cordoned.&lt;/strong&gt; &lt;code&gt;node.kubernetes.io/unschedulable&lt;/code&gt; appears after &lt;code&gt;kubectl cordon&lt;/code&gt; (or a drain during maintenance). If the cordon was intentional, that's expected — the pod schedules once you &lt;code&gt;kubectl uncordon&lt;/code&gt; the node or new capacity arrives. A pod stuck &lt;code&gt;Pending&lt;/code&gt; during a node upgrade is usually this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The node is deliberately reserved&lt;/strong&gt; — GPU nodes, a &lt;code&gt;dedicated=gpu:NoSchedule&lt;/code&gt; taint, spot-instance pools. If your pod &lt;em&gt;should&lt;/em&gt; run there, add a toleration:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tolerations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedicated"&lt;/span&gt;
      &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Equal"&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpu"&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NoSchedule"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A toleration lets a pod land on a tainted node; it does not &lt;em&gt;force&lt;/em&gt; it there. Pair it with a &lt;code&gt;nodeSelector&lt;/code&gt; or affinity (Step 4) when you want the pod to actually target that pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Node affinity / selector mismatch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;didn't match Pod's node affinity/selector&lt;/code&gt; means your &lt;code&gt;nodeSelector&lt;/code&gt; or &lt;code&gt;nodeAffinity&lt;/code&gt; names a label that no available node carries. Usually a typo, a decommissioned node pool, or a label that was never applied. Check what the pod demands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.nodeSelector}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"disktype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ssd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"topology.kubernetes.io/zone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1a"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check which nodes actually have those labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;disktype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ssd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns nothing, no node matches. Either the label is wrong on the pod, or it was never set on the nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label node ip-10-0-1-42 &lt;span class="nv"&gt;disktype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ssd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A subtle trap: pinning a pod to a single zone with &lt;code&gt;topology.kubernetes.io/zone&lt;/code&gt; means it can &lt;em&gt;only&lt;/em&gt; schedule in that zone. If that zone is full and your autoscaler scales a different zone, the pod stays &lt;code&gt;Pending&lt;/code&gt; forever. Prefer &lt;code&gt;preferredDuringScheduling&lt;/code&gt; over &lt;code&gt;requiredDuringScheduling&lt;/code&gt; unless the constraint is truly hard — a preference degrades gracefully instead of deadlocking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Unbound volumes and zone conflicts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pod has unbound immediate PersistentVolumeClaims&lt;/code&gt; or &lt;code&gt;volume node affinity conflict&lt;/code&gt; means storage is blocking placement. Storage and scheduling are coupled: a pod can only run where its volume can attach. Check the PVC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pvc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME            STATUS    VOLUME   CAPACITY   STORAGECLASS
data-payments   Pending                       gp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Pending&lt;/code&gt; PVC means no PersistentVolume satisfied the claim. Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No matching StorageClass or no dynamic provisioner.&lt;/strong&gt; &lt;code&gt;kubectl get storageclass&lt;/code&gt; — if there's no default class and the PVC names none, nothing provisions the volume. Set a default or name a valid class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;volume node affinity conflict&lt;/code&gt;&lt;/strong&gt; — the PV already exists in &lt;code&gt;us-east-1a&lt;/code&gt;, but the only node with free capacity is in &lt;code&gt;us-east-1b&lt;/code&gt;, and an EBS volume can't cross zones. Fix by setting &lt;code&gt;volumeBindingMode: WaitForFirstConsumer&lt;/code&gt; on the StorageClass so the volume is provisioned &lt;em&gt;after&lt;/em&gt; the scheduler picks the node, in the same zone — instead of binding first and pinning the pod to the wrong zone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Node is out of pod slots (&lt;code&gt;too many pods&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;too many pods&lt;/code&gt; means the node hit its &lt;code&gt;max-pods&lt;/code&gt; limit even though it has spare CPU and memory. On AWS EKS with the VPC CNI, the ceiling is often driven by how many IP addresses the instance's ENIs can hold — a small instance type may cap at 8–17 pods regardless of how much RAM it has. Check the node's capacity and current count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get node ip-10-0-1-42 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.capacity.pods}'&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;--all-namespaces&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; spec.nodeName&lt;span class="o"&gt;=&lt;/span&gt;ip-10-0-1-42 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-headers&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the count equals capacity, the node is full of pods, not resources. Fixes: use larger instance types (more ENIs = more IPs = more pods), enable prefix delegation on the VPC CNI to pack far more IPs per node, or add nodes. This is an easy one to miss because &lt;code&gt;kubectl top node&lt;/code&gt; shows the node as nearly idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Topology spread and anti-affinity deadlocks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;didn't match pod topology spread constraints&lt;/code&gt; or an anti-affinity rejection means your own placement rules can't be satisfied. A classic self-inflicted deadlock: a &lt;code&gt;podAntiAffinity&lt;/code&gt; with &lt;code&gt;requiredDuringScheduling&lt;/code&gt; that says "no two replicas on the same node" while you have 5 replicas and only 3 nodes. The 4th and 5th can never place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;affinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podAntiAffinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requiredDuringSchedulingIgnoredDuringExecution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# hard rule — deadlocks&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;topologyKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/hostname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch &lt;code&gt;required&lt;/code&gt; to &lt;code&gt;preferred&lt;/code&gt; so replicas spread when possible but still schedule when they can't, or add nodes so the hard rule is satisfiable. The same reasoning applies to &lt;code&gt;topologySpreadConstraints&lt;/code&gt; with &lt;code&gt;whenUnsatisfiable: DoNotSchedule&lt;/code&gt; — that's a hard gate; &lt;code&gt;ScheduleAnyway&lt;/code&gt; degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable checklist
&lt;/h2&gt;

&lt;p&gt;When a pod is stuck &lt;code&gt;Pending&lt;/code&gt;, run this in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl describe pod &amp;lt;pod&amp;gt;&lt;/code&gt; → read the &lt;code&gt;FailedScheduling&lt;/code&gt; event; note every &lt;code&gt;N node(s) ...&lt;/code&gt; fragment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Insufficient cpu/memory&lt;/code&gt; → right-size requests or enable the cluster autoscaler; check it isn't at max node-group size (Step 2).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;untolerated taint&lt;/code&gt; → uncordon the node, or add the matching toleration (Step 3).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;didn't match node affinity/selector&lt;/code&gt; → fix the label on the pod or apply it to nodes; avoid hard zone pins (Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unbound PVC&lt;/code&gt; / &lt;code&gt;volume node affinity conflict&lt;/code&gt; → fix the StorageClass; set &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; (Step 5).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;too many pods&lt;/code&gt; → node hit its IP/pod cap; bigger instances, prefix delegation, or more nodes (Step 6).&lt;/li&gt;
&lt;li&gt;Topology/anti-affinity rejection → relax &lt;code&gt;required&lt;/code&gt;/&lt;code&gt;DoNotSchedule&lt;/code&gt; to &lt;code&gt;preferred&lt;/code&gt;/&lt;code&gt;ScheduleAnyway&lt;/code&gt; (Step 7).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't let Pending pods hide
&lt;/h2&gt;

&lt;p&gt;A pod that can't schedule is invisible to a health check that only watches running pods — it never crashes, it just quietly never runs. That's how a scaled-up deployment silently serves at half capacity. Alert on pods stuck &lt;code&gt;Pending&lt;/code&gt; past a threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kube_pod_status_phase{phase="Pending"} == 1
  and on(pod, namespace) (time() - kube_pod_created) &amp;gt; 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that into the &lt;a href="https://devtocash.com/blog/2026-03-26-kubernetes-monitoring-prometheus-grafana" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring setup&lt;/a&gt; so a five-minute-stuck pod pages before a rollout stalls, and bake the seven-step triage into your &lt;a href="https://devtocash.com/blog/incident-management-runbook-template-2026" rel="noopener noreferrer"&gt;incident runbook&lt;/a&gt;. A cluster that's chronically full and can't place pods is one of the quiet &lt;a href="https://devtocash.com/blog/2026-04-11-kubernetes-mistakes-that-cost-companies-millions" rel="noopener noreferrer"&gt;Kubernetes mistakes that cost companies millions&lt;/a&gt; — cheap to catch, expensive to discover during a traffic spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-07-15-kubernetes-oomkilled-exit-code-137-fix" rel="noopener noreferrer"&gt;Kubernetes OOMKilled (Exit Code 137): How to Debug and Fix It&lt;/a&gt;&lt;/strong&gt; — the mirror image of oversized requests: too-low &lt;em&gt;limits&lt;/em&gt; get a running pod killed, while too-high &lt;em&gt;requests&lt;/em&gt; keep it from scheduling at all. Right-sizing fixes both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-06-28-kubernetes-pod-autoscaling-hpa-vpa-and-keda-explained" rel="noopener noreferrer"&gt;Kubernetes Pod Autoscaling: HPA, VPA, and KEDA Explained&lt;/a&gt;&lt;/strong&gt; — the VPA recommends the requests that decide whether a pod fits a node, and the cluster autoscaler is what turns a &lt;code&gt;Pending&lt;/code&gt; pod into a new node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;Kubernetes ImagePullBackOff: How to Debug and Fix It&lt;/a&gt;&lt;/strong&gt; — once the scheduler &lt;em&gt;does&lt;/em&gt; place your pod, the pull is the next thing that can fail; together with CrashLoopBackOff and OOMKilled these cover nearly every reason a pod fails to come up.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/2026-07-16-kubernetes-pod-pending-failedscheduling-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>sre</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>Incident Management Runbook: The Complete SRE Template for 2026</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:07:24 +0000</pubDate>
      <link>https://dev.to/devtocash/incident-management-runbook-the-complete-sre-template-for-2026-5h4l</link>
      <guid>https://dev.to/devtocash/incident-management-runbook-the-complete-sre-template-for-2026-5h4l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/incident-management-runbook-template-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every minute of downtime costs your company money. For an e-commerce platform, that is thousands of dollars per minute. For a fintech startup, it is lost trust that takes months to rebuild. Yet when an incident strikes, most teams still scramble through Slack, DMs, and scattered Google Docs — wasting the first 15 minutes just figuring out who owns what.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;Incident Management Runbook&lt;/strong&gt; fixes this. It is a single source of truth that tells everyone — engineer, manager, or new hire awakened at 3 AM — exactly what to do, who to call, and how to escalate. It eliminates guesswork. It compresses time-to-resolution. It saves your team from burnout.&lt;/p&gt;

&lt;p&gt;This guide provides a &lt;strong&gt;complete, copy-paste-ready incident management runbook&lt;/strong&gt; built from real-world SRE practices at companies like Google, Netflix, and PagerDuty. By the end, you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A severity-level framework your entire org can agree on&lt;/li&gt;
&lt;li&gt;A role assignment system (Incident Commander, Comms Lead, etc.)&lt;/li&gt;
&lt;li&gt;A step-by-step response lifecycle from detection to postmortem&lt;/li&gt;
&lt;li&gt;A runbook template you can deploy to your wiki or runbook tool today&lt;/li&gt;
&lt;li&gt;Automation patterns using PagerDuty, Opsgenie, and Slack integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you are a two-person startup or a 200-engineer platform team, this runbook scales with you. Let's build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What Is an Incident Management Runbook?
&lt;/h2&gt;

&lt;p&gt;A runbook is a documented, step-by-step procedure for responding to specific types of incidents. Unlike a general "incident response policy" (which says &lt;em&gt;what&lt;/em&gt; to do at a high level), a runbook specifies &lt;em&gt;exactly how&lt;/em&gt; to do it — which commands to run, which dashboards to check, which people to page.&lt;/p&gt;

&lt;p&gt;A good runbook answers these questions before the incident starts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; is on-call right now, and who is their backup?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; constitutes a SEV1 vs SEV2 vs SEV3?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How&lt;/strong&gt; do we declare an incident and notify stakeholders?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where&lt;/strong&gt; are the dashboards, logs, and runbooks for each service?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; do we escalate to the next tier or wake up the CTO?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runbook should be stored in a place accessible during an outage — which means &lt;strong&gt;not&lt;/strong&gt; on the company VPN if the VPN itself is down. Git repositories mirrored to multiple locations, printed copies in the NOC, or tools like PagerDuty Runbook Automation and Rundeck are common solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Incident Severity Levels
&lt;/h2&gt;

&lt;p&gt;Before anyone can respond, everyone must agree on what "SEV1" means. Without a shared severity framework, you get arguments during incidents about whether something is "really that bad" — wasting time when seconds matter.&lt;/p&gt;

&lt;p&gt;Here is the framework used by most SRE organizations, adapted from Google's SRE book:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Response SLA&lt;/th&gt;
&lt;th&gt;Escalation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SEV0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complete service outage. All users affected. Revenue impact.&lt;/td&gt;
&lt;td&gt;Website down, payment gateway offline, 100% 5xx errors&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5 min&lt;/strong&gt; acknowledge, &lt;strong&gt;15 min&lt;/strong&gt; resolve or escalate&lt;/td&gt;
&lt;td&gt;Page CTO + VP Eng immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SEV1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Major feature broken. Majority of users affected. No workaround.&lt;/td&gt;
&lt;td&gt;Login broken, checkout fails, API returning errors for 50%+ users&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;10 min&lt;/strong&gt; acknowledge, &lt;strong&gt;30 min&lt;/strong&gt; resolve or escalate&lt;/td&gt;
&lt;td&gt;Page Engineering Director + on-call manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SEV2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial degradation. Subset of users affected. Workaround exists.&lt;/td&gt;
&lt;td&gt;Slow page loads, search results stale, one region degraded&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;30 min&lt;/strong&gt; acknowledge, &lt;strong&gt;2 hours&lt;/strong&gt; resolve or escalate&lt;/td&gt;
&lt;td&gt;Notify team lead, on-call engineer handles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SEV3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minor issue. Cosmetic or non-critical.&lt;/td&gt;
&lt;td&gt;Typo on landing page, broken image in blog, non-critical cron job failure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Next business day&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create ticket, handle during business hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Customize the thresholds for your business.&lt;/strong&gt; An e-commerce site during Black Friday treats a 2% error rate as a SEV0. A SaaS tool on a Sunday afternoon treats it as a SEV2. Define what "revenue impact" means for your specific context.&lt;/p&gt;

&lt;p&gt;Every incident should reference the affected &lt;a href="https://devtocash.com/blog/error-budgets-sre-guide" rel="noopener noreferrer"&gt;error budget&lt;/a&gt; — if it's not burning your SLO budget, it may not warrant waking up the team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key principle:&lt;/strong&gt; Err on the side of over-declaring. It is always better to downgrade a SEV2 → SEV3 after investigation than to discover a SEV0 was misclassified as a SEV2 for 45 minutes.&lt;/p&gt;

&lt;p&gt;Incidents are triggered when SLIs breach SLO thresholds — &lt;a href="https://devtocash.com/blog/sli-vs-slo-vs-sla-real-sre-guide-2026" rel="noopener noreferrer"&gt;define these boundaries first&lt;/a&gt; so your severity levels map directly to your reliability targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Incident Response Roles
&lt;/h2&gt;

&lt;p&gt;Having clearly defined roles prevents the most common incident pitfall: everyone trying to do everything at once, drowning in Slack noise, and nobody communicating with stakeholders.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Who Usually Fills It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incident Commander (IC)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs the incident. Makes all decisions. Keeps the response moving. Only person who can declare "resolved."&lt;/td&gt;
&lt;td&gt;Most senior on-call engineer, or designated IC from rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operations Lead (OL)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Investigates and mitigates. Runs commands, checks dashboards, implements fixes.&lt;/td&gt;
&lt;td&gt;On-call engineer for the affected service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Communications Lead (CL)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manages all external communication — status page updates, Slack announcements, customer-facing messages. Shields IC from interruptions.&lt;/td&gt;
&lt;td&gt;Engineering manager, TPM, or designated comms person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scribe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Documents everything in real time — timeline, actions taken, hypotheses tested. Critical for postmortem.&lt;/td&gt;
&lt;td&gt;Junior engineer, intern, or automated via incident tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a small team, one person may wear multiple hats — but never combine IC and CL. The IC needs uninterrupted focus on resolution; the CL absorbs all external noise. If your team is 4+ engineers on-call, rotate the IC role so nobody burns out.&lt;/p&gt;

&lt;p&gt;When an incident is declared, the first person on the scene automatically becomes &lt;strong&gt;Interim IC&lt;/strong&gt; until a designated IC joins. They announce in the incident channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/incident declare
SEV: [1/2/3]
Title: [Brief description]
IC: @username (interim)
Channel: #incident-2026-0625-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The Incident Response Lifecycle
&lt;/h2&gt;

&lt;p&gt;Every incident follows five phases. Your runbook should have a clear procedure for each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Detection
&lt;/h3&gt;

&lt;p&gt;Incidents are detected through three channels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated monitoring&lt;/strong&gt; — Alerts from Prometheus Alertmanager, Datadog, Grafana, or New Relic that fire when SLO burn rates exceed thresholds. If you do not have SLO-based alerting yet, read our &lt;a href="https://devtocash.com/blog/error-budgets-sre-guide" rel="noopener noreferrer"&gt;Error Budgets Guide&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User reports&lt;/strong&gt; — Customer support tickets, social media complaints, or internal bug reports. Route these to the on-call channel automatically via webhook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineer observation&lt;/strong&gt; — A team member notices something wrong during a deployment or code review.&lt;/p&gt;

&lt;p&gt;Regardless of how it is detected, the first step is always the same: &lt;strong&gt;verify the signal is real.&lt;/strong&gt; Check the affected dashboard, run a quick smoke test, and confirm you are not chasing a monitoring false positive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Declaration
&lt;/h3&gt;

&lt;p&gt;Once verified, the on-call engineer declares the incident. This triggers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an incident channel&lt;/strong&gt; — &lt;code&gt;#incident-YYYY-MMDD-NNN&lt;/code&gt; in Slack or Teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page the on-call rotation&lt;/strong&gt; via PagerDuty/Opsgenie&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post initial status&lt;/strong&gt; to the status page (or internal status channel if no public page)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assign roles&lt;/strong&gt; — IC, OL, CL, Scribe&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A declaration message looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🚨 INCIDENT DECLARED — SEV2
Title: Checkout API returning 503 errors in us-east-1
IC: @alice (interim, @bob is primary IC joining in 2 min)
OL: @charlie
CL: @diana
Channel: #incident-2026-0625-003
Dashboard: https://grafana.example.com/d/checkout
Runbook: https://wiki.example.com/runbooks/checkout-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 3: Diagnosis &amp;amp; Mitigation
&lt;/h3&gt;

&lt;p&gt;This is the core of incident response. The OL investigates while the IC coordinates. The process follows a structured loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; — Isolate the blast radius. Which users? Which region? Which component?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypothesize&lt;/strong&gt; — Propose a likely cause. "Recent deploy changed the DB connection pool size."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; — Validate the hypothesis. Check deploy logs. Roll back if the hypothesis is strong enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitigate&lt;/strong&gt; — Stop the bleeding. Rollback, scale up, failover, feature flag off. &lt;strong&gt;Mitigation comes before root cause.&lt;/strong&gt; A customer does not care why the site is down — they want it back up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — Confirm the fix worked. Watch dashboards for 5-10 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Golden rule:&lt;/strong&gt; If you have not found the cause in 15 minutes, escalate. Call in more engineers. Wake up the service owner. Do not hero-solo an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Resolution
&lt;/h3&gt;

&lt;p&gt;The IC declares the incident resolved only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service is restored and verified for at least 10 minutes&lt;/li&gt;
&lt;li&gt;All alerts have returned to normal&lt;/li&gt;
&lt;li&gt;Customers are no longer impacted&lt;/li&gt;
&lt;li&gt;A rollback or permanent fix is in place (not a fragile workaround)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resolution message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ INCIDENT RESOLVED — SEV2
Duration: 47 minutes (14:03 – 14:50 UTC)
Root cause: Connection pool exhaustion after config deploy v2.4.1
Mitigation: Rolled back to v2.4.0, connection pool restored
Impact: ~12% of checkout requests failed (est. 1,200 affected users)
Postmortem: Scheduled for 2026-06-26 10:00 UTC
Action items: #INC-42, #INC-43
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 5: Postmortem (Blameless)
&lt;/h3&gt;

&lt;p&gt;Within 24-48 hours of resolution, hold a blameless postmortem. The goal is &lt;strong&gt;not&lt;/strong&gt; to assign blame — it is to prevent recurrence. We cover the full postmortem template in Section 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Runbook Template
&lt;/h2&gt;

&lt;p&gt;Here is the actual runbook template. Copy this into your wiki, Notion, Confluence, or runbook automation tool. Fill in the &lt;code&gt;[PLACEHOLDERS]&lt;/code&gt; for each service you own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Runbook: [SERVICE NAME]&lt;/span&gt;

&lt;span class="gu"&gt;## Service Overview&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Owner Team:**&lt;/span&gt; [Team Name]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**On-Call Rotation:**&lt;/span&gt; [PagerDuty/Opsgenie escalation policy link]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Primary Dashboard:**&lt;/span&gt; [Grafana/Datadog link]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Logs:**&lt;/span&gt; [Kibana/Loki/Splunk link]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Source Code:**&lt;/span&gt; [GitHub/GitLab link]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**CI/CD Pipeline:**&lt;/span&gt; [GitHub Actions/GitLab CI link]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Runbook Last Updated:**&lt;/span&gt; [YYYY-MM-DD]

&lt;span class="gu"&gt;## Dependencies&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Upstream:**&lt;/span&gt; [List services this depends on]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Downstream:**&lt;/span&gt; [List services that depend on this]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**External:**&lt;/span&gt; [Third-party APIs, databases, CDNs]

&lt;span class="gu"&gt;## Alert Triggers&lt;/span&gt;
| Alert Name | Severity | Threshold | Dashboard Link |
|-----------|----------|-----------|---------------|
| High Error Rate | SEV1 | &amp;gt;5% 5xx for 5 min | [link] |
| High Latency p99 | SEV2 | &amp;gt;2s for 10 min | [link] |
| Pod CrashLoopBackOff | SEV1 | Any pod restarting &amp;gt;3 times | [link] |
| Certificate Expiry | SEV3 | &amp;lt;30 days until expiry | [link] |

&lt;span class="gu"&gt;## Common Incidents&lt;/span&gt;

&lt;span class="gu"&gt;### 1. High 5xx Error Rate&lt;/span&gt;
&lt;span class="gs"&gt;**Symptoms:**&lt;/span&gt; Dashboard shows error rate spike, users report failures
&lt;span class="gs"&gt;**Likely Causes:**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Recent deployment introduced bug → Check deploy history
&lt;span class="p"&gt;-&lt;/span&gt; Upstream dependency failure → Check dependency dashboards
&lt;span class="p"&gt;-&lt;/span&gt; Database connection pool exhausted → Check DB metrics
&lt;span class="p"&gt;-&lt;/span&gt; Rate limiting triggered → Check API gateway metrics

&lt;span class="gs"&gt;**Immediate Actions:**&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Check recent deployments:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl rollout history deployment/[service-name] -n production&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2. Rollback if deployment was within last 30 minutes:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl rollout undo deployment/[service-name] -n production&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3. Check upstream dependencies:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;curl -s https://[dependency]/health&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4. Scale up replicas if traffic spike:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl scale deployment/[service-name] --replicas=10 -n production&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5. If none of the above help → escalate to [TEAM NAME], page [PERSON]

### 2. Pods in CrashLoopBackOff
**Symptoms:** `kubectl get pods` shows restarts, deployment not progressing
**Likely Causes:**
- Misconfigured environment variables or secrets
- Missing PersistentVolume or storage issue
- OOMKilled (memory limit too low)
- Readiness/Liveness probe misconfigured

**Immediate Actions:**
1. Check pod logs:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl logs [pod-name] -n production --tail=100&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2. Check previous container logs (if crash + restart):
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl logs [pod-name] -n production --previous&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3. Describe the pod for events:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl describe pod [pod-name] -n production&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4. Check resource usage:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubectl top pod [pod-name] -n production&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5. If OOMKilled → increase memory limits and restart

### 3. Certificate Expiry
**Preventative:** Run this check weekly via cron:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
echo | openssl s_client -servername [domain] -connect [domain]:443 2&amp;gt;/dev/null | \&lt;br&gt;
  openssl x509 -noout -dates&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Escalation Path
| Level | Who | When | Contact |
|-------|-----|------|---------|
| L1 | On-call engineer | Immediate | PagerDuty rotation |
| L2 | Service owner / Tech Lead | If unresolved after 15 min | Slack @team-leads |
| L3 | Engineering Manager | If unresolved after 30 min | Phone call |
| L4 | Director / VP Engineering | If SEV0 after 45 min | Phone call |
| L5 | CTO | SEV0 lasting &amp;gt;1 hour | Phone call |

## Post-Incident
- [ ] Create postmortem doc within 24 hours
- [ ] Create action items in issue tracker
- [ ] Update this runbook if new causes or fixes were discovered
- [ ] Verify monitoring covers the failure mode detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;/p&gt;

&lt;p&gt;This template is your starting point. Every service in your organization should have one. Keep it updated — an outdated runbook is worse than no runbook because it wastes time with stale information.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Automating the Runbook
&lt;/h2&gt;

&lt;p&gt;A static runbook in a wiki is step one. The real SRE progression is toward &lt;strong&gt;automated runbooks&lt;/strong&gt; — where the on-call engineer receives a pre-filled incident channel with relevant dashboards and diagnostic commands already executed.&lt;/p&gt;
&lt;h3&gt;
  
  
  PagerDuty + Rundeck Automation
&lt;/h3&gt;

&lt;p&gt;Integrate PagerDuty with Rundeck (or Ansible Automation Platform) to trigger diagnostic jobs automatically when an alert fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rundeck job definition triggered by PagerDuty webhook&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-api-auto-diagnose&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes-prod&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl get pods -n production -l app=checkout&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl top pods -n production -l app=checkout&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl logs -n production -l app=checkout --tail=50&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -s https://checkout-api/health&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is posted to the incident Slack channel before the on-call engineer even opens their laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slack Slash Commands
&lt;/h3&gt;

&lt;p&gt;Build Slack slash commands for common incident actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/incident declare checkout-api "503 errors in us-east-1" --sev=SEV2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ Creates &lt;code&gt;#incident-2026-0625-004&lt;/code&gt;, posts dashboard links, pages on-call, assigns IC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/incident diagnose checkout-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ Runs &lt;code&gt;kubectl describe&lt;/code&gt;, checks recent deployments, posts logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/incident resolve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ Prompts for root cause, duration, impact summary, posts resolution template.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitOps for Runbooks
&lt;/h3&gt;

&lt;p&gt;Store runbooks as Markdown in the same Git repository as the service code. This enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version control&lt;/strong&gt; — Every runbook change is reviewed via PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Co-location&lt;/strong&gt; — Developers update the runbook when they change the service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD integration&lt;/strong&gt; — Runbook validity checks in CI (e.g., lint markdown, verify links)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;my-service/
├── src/
├── Dockerfile
├── k8s/
└── RUNBOOK.md    &lt;span class="c"&gt;# ← Living next to the code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Common Pitfalls (and How to Avoid Them)
&lt;/h2&gt;

&lt;p&gt;Even teams with a runbook make these mistakes. Learn from them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 1: The Runbook Is Outdated
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; On-call follows a runbook that references a decommissioned dashboard, a renamed Slack channel, or a service that was migrated six months ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Treat the runbook as code. Require a runbook update as part of every significant deployment or service change. Use a CI check that verifies all links in the runbook return HTTP 200. Set a calendar reminder to audit all runbooks quarterly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CI check: verify all URLs in runbook&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'https?://[^\s)\]]+'&lt;/span&gt; RUNBOOK.md | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;url&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BROKEN: &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi
  done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 2: Too Many Alerts, Wrong Severity
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; The on-call phone buzzes 40 times per night. Engineers develop alert fatigue. A real SEV0 gets lost in the noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Every alert must be &lt;strong&gt;actionable&lt;/strong&gt; and correctly prioritized. If an alert fires and the correct response is "acknowledge and ignore," delete the alert. Use &lt;a href="https://devtocash.com/blog/error-budgets-sre-guide" rel="noopener noreferrer"&gt;error budgets&lt;/a&gt; as the gating mechanism — only page when the error budget is burning too fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 3: Hero Culture
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; One senior engineer tries to solve everything alone. They do not escalate, do not communicate, and 90 minutes later, the SEV2 is now a SEV0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Escalation is not weakness — it is process. The runbook's escalation path exists for a reason. The IC's job is to recognize when to pull in more people, not to solo the fix. Institute a hard rule: if the incident is not mitigated within the SLA window, escalation is &lt;strong&gt;mandatory&lt;/strong&gt;, not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 4: No Communication During Incidents
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Stakeholders flood the IC with DMs. "Is it fixed yet?" "When will it be back?" "The CEO is asking." The IC cannot focus on actually fixing the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; The Communications Lead exists for exactly this reason. Their only job is to post status updates at regular intervals (every 15 minutes for SEV1, every 30 for SEV2) so nobody has to ask. Template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📢 INCIDENT UPDATE — SEV2 — 14:20 UTC
Status: Still investigating. Checkout API returning 503s.
Mitigation attempted: Rollback to v2.4.0 — no improvement.
Current hypothesis: Upstream payment gateway timeout.
Next update: 14:35 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 5: Skipping the Postmortem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Incident resolved. Everyone is tired. "We'll do the postmortem later." Later never comes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Schedule the postmortem during the resolution call. Block 1 hour on everyone's calendar within 48 hours — while memory is fresh. A postmortem done a week later is half as valuable as one done while logs and timelines are still accessible. If your incident management tooling does not auto-schedule postmortems, add it as a manual step in your runbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Blameless Postmortem Template
&lt;/h2&gt;

&lt;p&gt;A postmortem is a written record of what happened, why, and what will change. It is &lt;strong&gt;not&lt;/strong&gt; about assigning fault. Use this template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Postmortem: [INCIDENT TITLE]&lt;/span&gt;

&lt;span class="gu"&gt;## Metadata&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Incident ID:**&lt;/span&gt; INC-YYYY-MMDD-NNN
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Date:**&lt;/span&gt; [YYYY-MM-DD]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Duration:**&lt;/span&gt; [HH:MM – HH:MM UTC] ([N] minutes)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Severity:**&lt;/span&gt; SEV[1/2/3]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Incident Commander:**&lt;/span&gt; @[name]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Postmortem Author:**&lt;/span&gt; @[name]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Status:**&lt;/span&gt; [Draft / Reviewed / Published]

&lt;span class="gu"&gt;## Summary&lt;/span&gt;
[One paragraph: what happened, impact, how it was fixed]

&lt;span class="gu"&gt;## Timeline (UTC)&lt;/span&gt;
| Time | Event |
|------|-------|
| 14:03 | Prometheus alert fired: checkout-api 5xx &amp;gt; 5% |
| 14:05 | @alice acknowledged, began investigation |
| 14:08 | Incident declared SEV2 in #incident-2026-0625-003 |
| 14:12 | Rollback to v2.4.0 attempted — no improvement |
| 14:18 | Upstream payment gateway identified as root cause |
| 14:25 | Payment gateway team paged, confirmed outage on their side |
| 14:35 | Retry circuit breaker activated — error rate dropping |
| 14:47 | All metrics green, 10 min verification passed |
| 14:50 | Incident resolved |

&lt;span class="gu"&gt;## Root Cause&lt;/span&gt;
[Detailed technical explanation. What specific change, failure, or condition triggered the incident?]

&lt;span class="gu"&gt;## Impact&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Users affected:**&lt;/span&gt; ~1,200 (12% of checkout traffic)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Revenue impact:**&lt;/span&gt; Estimated $3,400 in lost transactions
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Data loss:**&lt;/span&gt; None
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Security impact:**&lt;/span&gt; None

&lt;span class="gu"&gt;## What Went Well&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Alert fired within 2 minutes of error rate crossing threshold
&lt;span class="p"&gt;-&lt;/span&gt; Incident Commander declared within 8 minutes of alert
&lt;span class="p"&gt;-&lt;/span&gt; Communications Lead posted updates every 15 minutes
&lt;span class="p"&gt;-&lt;/span&gt; Rollback was attempted quickly even though it didn't help

&lt;span class="gu"&gt;## What Went Poorly&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Payment gateway was not listed in service dependencies — added 13 min to diagnosis
&lt;span class="p"&gt;-&lt;/span&gt; No circuit breaker was pre-configured for upstream failures
&lt;span class="p"&gt;-&lt;/span&gt; Secondary on-call (backup IC) was unreachable for 10 min

&lt;span class="gu"&gt;## Action Items&lt;/span&gt;
| # | Action | Owner | Priority | Due |
|---|--------|-------|----------|-----|
| INC-42 | Add payment gateway to service dependency list and runbook | @charlie | P0 | 2026-06-27 |
| INC-43 | Implement circuit breaker with retry for all upstream calls | @alice | P1 | 2026-07-01 |
| INC-44 | Verify secondary on-call contact info in PagerDuty | @diana | P0 | 2026-06-26 |
| INC-45 | Add synthetic check for payment gateway health | @bob | P2 | 2026-07-15 |

&lt;span class="gu"&gt;## Lessons Learned&lt;/span&gt;
[1-3 sentences capturing the key takeaway for the broader org]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store postmortems in a shared, searchable location. Over time, they become your organization's institutional memory — patterns emerge, recurring root causes become obvious, and you can justify infrastructure investments with real incident data.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Conclusion
&lt;/h2&gt;

&lt;p&gt;An incident management runbook does not prevent incidents. What it does is far more valuable: it &lt;strong&gt;compresses the time between "something is wrong" and "it is fixed."&lt;/strong&gt; It removes the cognitive load of deciding what to do under pressure and replaces it with a muscle-memory procedure.&lt;/p&gt;

&lt;p&gt;Incident management is a core SRE competency tested in interviews — see how it's covered in the &lt;a href="https://devtocash.com/blog/top-50-sre-interview-questions-2026" rel="noopener noreferrer"&gt;Top 50 SRE interview questions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick one service.&lt;/strong&gt; Write its runbook using the template in Section 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define your severity levels.&lt;/strong&gt; Get stakeholder alignment — nobody should argue about SEV during an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice.&lt;/strong&gt; Run a fire drill. Fake an incident and walk through the runbook. Find the gaps before a real outage does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate one step.&lt;/strong&gt; Even something small — an auto-created Slack channel or a diagnostic script — saves minutes during your next SEV2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best-run SRE teams do not have fewer incidents. They recover faster, communicate better, and learn more from each one. A runbook is how they do it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devtocash.com/blog/error-budgets-sre-guide" rel="noopener noreferrer"&gt;Error Budgets: Stop Wasting Your SRE Team's Time&lt;/a&gt; — Use error budgets to determine when to page and when to let it ride&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devtocash.com/blog/2026-06-24-sli-vs-slo-vs-sla-real-sre-guide-2026" rel="noopener noreferrer"&gt;SLI vs SLO vs SLA: The Real SRE Guide with Examples&lt;/a&gt; — The measurement framework that feeds your incident alerting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devtocash.com/blog/kubernetes-security-best-practices-2026" rel="noopener noreferrer"&gt;Kubernetes Security Best Practices 2026&lt;/a&gt; — Security incidents are incidents too — lock down your clusters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devtocash.com/blog/2026-03-26-kubernetes-monitoring-prometheus-grafana" rel="noopener noreferrer"&gt;Kubernetes Monitoring with Prometheus and Grafana&lt;/a&gt; — Build the monitoring stack your runbooks depend on&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/incident-management-runbook-template-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>incidentmanagement</category>
      <category>runbook</category>
      <category>sre</category>
      <category>incidentresponse</category>
    </item>
    <item>
      <title>Kubernetes OOMKilled (Exit Code 137): How to Debug and Fix It</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Wed, 15 Jul 2026 01:10:58 +0000</pubDate>
      <link>https://dev.to/devtocash/kubernetes-oomkilled-exit-code-137-how-to-debug-and-fix-it-4845</link>
      <guid>https://dev.to/devtocash/kubernetes-oomkilled-exit-code-137-how-to-debug-and-fix-it-4845</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/2026-07-15-kubernetes-oomkilled-exit-code-137-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What OOMKilled actually means
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;OOMKilled&lt;/code&gt; with exit code &lt;code&gt;137&lt;/code&gt; means the Linux kernel killed your container because it tried to use more memory than it was allowed. The &lt;code&gt;137&lt;/code&gt; is &lt;code&gt;128 + 9&lt;/code&gt; — the process received signal &lt;code&gt;9&lt;/code&gt; (&lt;code&gt;SIGKILL&lt;/code&gt;). It gets no warning, no chance to flush, no chance to log. One moment it's serving traffic; the next it's gone, and the pod restarts into a &lt;a href="https://devtocash.com/blog/2026-07-14-kubernetes-crashloopbackoff-fix" rel="noopener noreferrer"&gt;&lt;code&gt;CrashLoopBackOff&lt;/code&gt;&lt;/a&gt; if the same thing keeps happening.&lt;/p&gt;

&lt;p&gt;There are two completely different situations that both surface as &lt;code&gt;OOMKilled&lt;/code&gt;, and the entire fix depends on telling them apart:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Container-level OOM (cgroup):&lt;/strong&gt; the container exceeded its own &lt;code&gt;resources.limits.memory&lt;/code&gt;. Only that container dies. This is 90% of what you'll see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-level OOM:&lt;/strong&gt; the whole node ran out of physical memory, and the kernel's OOM killer picked a victim — sometimes not even the pod that caused the pressure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This playbook is the exact sequence I run to confirm which one you have and fix it for good, usually in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Confirm it's actually OOMKilled
&lt;/h2&gt;

&lt;p&gt;Don't guess from a restarting pod — read the terminated container's state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f4c8b6-xk2mn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;Last State&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Wed, 15 Jul 2026 09:14:02
      Finished:     Wed, 15 Jul 2026 09:14:48
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Reason: OOMKilled&lt;/code&gt; plus &lt;code&gt;Exit Code: 137&lt;/code&gt; is the signature. Note that a bare &lt;code&gt;137&lt;/code&gt; &lt;em&gt;without&lt;/em&gt; &lt;code&gt;Reason: OOMKilled&lt;/code&gt; is different — that's usually a &lt;code&gt;SIGKILL&lt;/code&gt; from a failing liveness probe, which is a probe problem, not a memory one. If you see that, the fix is in the &lt;a href="https://devtocash.com/blog/kubernetes-liveness-readiness-startup-probes-guide" rel="noopener noreferrer"&gt;liveness and readiness probes guide&lt;/a&gt;, not here.&lt;/p&gt;

&lt;p&gt;The app logs are almost always useless for OOM because &lt;code&gt;SIGKILL&lt;/code&gt; can't be caught — the process never logs its own death. So don't waste time in &lt;code&gt;kubectl logs&lt;/code&gt;. The evidence lives in the container state and, for node-level kills, the kernel log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: See the limit that was breached
&lt;/h2&gt;

&lt;p&gt;You can't reason about a memory kill without knowing the ceiling it hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].resources}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"limits"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"256Mi"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"128Mi"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now watch what the container actually consumes over time. You need &lt;code&gt;metrics-server&lt;/code&gt; installed for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pod payments-api-7d9f4c8b6-xk2mn &lt;span class="nt"&gt;--containers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POD                          NAME          CPU    MEMORY
payments-api-7d9f4c8b6-xk2mn payments-api  120m   248Mi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sitting at &lt;code&gt;248Mi&lt;/code&gt; against a &lt;code&gt;256Mi&lt;/code&gt; limit is the whole story: this container lives one request away from death. The next question is &lt;em&gt;why&lt;/em&gt; — and that's the fork that decides everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The decisive fork — too-low limit or a leak?
&lt;/h2&gt;

&lt;p&gt;Watch memory over a few minutes, not a single snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;watch &lt;span class="nt"&gt;-n&lt;/span&gt; 5 &lt;span class="s1"&gt;'kubectl top pod payments-api-7d9f4c8b6-xk2mn --containers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat plateau just under the limit&lt;/strong&gt; → the limit is genuinely too low. The app has a stable working set that simply doesn't fit. Fix: raise the limit (Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady climb that never comes down&lt;/strong&gt;, even under constant load → a memory leak. The app grows until it hits &lt;em&gt;any&lt;/em&gt; ceiling you set. Fix: a bigger limit only buys hours; you have to find the leak (Step 5).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this wrong wastes a day. If you raise the limit on a leaking app, it OOMs again the next night at 2 a.m. — same crash, bigger blast radius, because now it took more of the node down with it before dying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Right-size requests and limits
&lt;/h2&gt;

&lt;p&gt;If the working set is legitimately larger than the limit, raise it — but set &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;limits&lt;/code&gt; deliberately, because they mean different things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;   &lt;span class="c1"&gt;# what the scheduler reserves; guarantees placement&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;   &lt;span class="c1"&gt;# the hard ceiling; exceed it and you're OOMKilled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules that save real incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For memory, set requests == limits.&lt;/strong&gt; Memory isn't compressible like CPU — you can't throttle it. When &lt;code&gt;requests&lt;/code&gt; equals &lt;code&gt;limits&lt;/code&gt;, the pod gets the &lt;code&gt;Guaranteed&lt;/code&gt; QoS class and is the &lt;em&gt;last&lt;/em&gt; thing the kubelet evicts under node pressure. If &lt;code&gt;requests&lt;/code&gt; is much lower than &lt;code&gt;limits&lt;/code&gt; (the &lt;code&gt;Burstable&lt;/code&gt; class), the scheduler may overcommit the node, and your pod becomes a prime eviction target the moment the node gets tight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave headroom for the runtime, not just the heap.&lt;/strong&gt; A JVM or Node process needs the container limit to hold the heap &lt;em&gt;plus&lt;/em&gt; metaspace, thread stacks, and off-heap buffers. Set the runtime heap to roughly 75% of the container limit:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Node.js: heap capped below the container limit&lt;/span&gt;
node &lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;384 server.js      &lt;span class="c"&gt;# ~384Mi heap under a 512Mi limit&lt;/span&gt;

&lt;span class="c"&gt;# JVM: let it read the cgroup limit and cap the heap as a percentage&lt;/span&gt;
java &lt;span class="nt"&gt;-XX&lt;/span&gt;:MaxRAMPercentage&lt;span class="o"&gt;=&lt;/span&gt;75.0 &lt;span class="nt"&gt;-jar&lt;/span&gt; app.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;-Xmx&lt;/code&gt; equal to the container limit is a classic self-inflicted OOM: the heap fills to the limit, then the first thread stack allocation pushes the &lt;em&gt;container&lt;/em&gt; past its ceiling and the kernel kills it — even though the JVM heap wasn't technically full.&lt;/p&gt;

&lt;p&gt;Right-sizing is also a cost lever, not just a stability one. Over-provisioned memory &lt;code&gt;requests&lt;/code&gt; reserve capacity you pay for and never use, and that reservation is what pushes you onto extra nodes. The same discipline runs through &lt;a href="https://devtocash.com/blog/2026-03-10-kubernetes-cost-optimization-guide" rel="noopener noreferrer"&gt;Kubernetes cost optimization&lt;/a&gt; and &lt;a href="https://devtocash.com/blog/finops-kubernetes-cost-optimization" rel="noopener noreferrer"&gt;FinOps for Kubernetes&lt;/a&gt; — right-sized limits keep pods alive &lt;em&gt;and&lt;/em&gt; keep the bill down. If you'd rather not tune by hand, the &lt;strong&gt;VPA&lt;/strong&gt; in recommendation mode will watch actual usage and suggest values; see how it fits with HPA and KEDA in the &lt;a href="https://devtocash.com/blog/2026-06-28-kubernetes-pod-autoscaling-hpa-vpa-and-keda-explained" rel="noopener noreferrer"&gt;pod autoscaling guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Hunt the leak
&lt;/h2&gt;

&lt;p&gt;If memory climbs and never recovers, raising the limit is a stopgap. Confirm the trend first, then attack the source.&lt;/p&gt;

&lt;p&gt;Confirm with the raw cgroup metric Prometheus exposes. &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt; is the number the kernel actually compares against the limit (it excludes reclaimable page cache), so it's the honest signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container_memory_working_set_bytes{pod=~"payments-api-.*", container="payments-api"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sawtooth that resets on each restart, with the peak creeping up run over run, is a leak's fingerprint. Common culprits by runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js:&lt;/strong&gt; unbounded caches, listeners added in a loop, closures holding large buffers. Capture a heap snapshot with &lt;code&gt;node --inspect&lt;/code&gt; and diff two snapshots taken minutes apart in Chrome DevTools — the objects that grew are your leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JVM:&lt;/strong&gt; rising heap after full GC. Trigger a heap dump on OOM so the next kill leaves evidence:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-XX&lt;/span&gt;:+HeapDumpOnOutOfMemoryError &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-XX&lt;/span&gt;:HeapDumpPath&lt;span class="o"&gt;=&lt;/span&gt;/dumps/heap.hprof &lt;span class="nt"&gt;-jar&lt;/span&gt; app.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the &lt;code&gt;.hprof&lt;/code&gt; in Eclipse MAT and run the leak-suspects report.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go:&lt;/strong&gt; goroutine leaks (each holds a stack) or slices that keep a reference to a huge backing array. Hit &lt;code&gt;/debug/pprof/heap&lt;/code&gt; with &lt;code&gt;go tool pprof&lt;/code&gt; and look at &lt;code&gt;inuse_space&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is the same everywhere: a leak is a code bug. Kubernetes can only contain it, not cure it, and every limit you set is just a delay timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: When the whole node runs out of memory
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;describe&lt;/code&gt; shows &lt;code&gt;OOMKilled&lt;/code&gt; but the container was nowhere near its own limit, you have node-level OOM. The node ran out of physical RAM — often because too many &lt;code&gt;Burstable&lt;/code&gt; pods were overcommitted — and the kernel's OOM killer chose a victim by &lt;code&gt;oom_score&lt;/code&gt;, which can be a bystander pod. Check the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node ip-10-0-1-42 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A6&lt;/span&gt; &lt;span class="s2"&gt;"Allocated resources"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If total memory &lt;code&gt;Limits&lt;/code&gt; far exceed the node's allocatable RAM, you're overcommitted. You may also see the kubelet &lt;em&gt;evicting&lt;/em&gt; pods before the kernel even acts — an &lt;code&gt;Evicted&lt;/code&gt; status with &lt;code&gt;The node was low on resource: memory&lt;/code&gt;. That's the kubelet trying to reclaim memory gracefully, and &lt;code&gt;Guaranteed&lt;/code&gt; QoS pods survive it while &lt;code&gt;BestEffort&lt;/code&gt; (no requests/limits at all) pods die first.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set memory &lt;code&gt;requests&lt;/code&gt; on every pod so the scheduler stops overcommitting the node.&lt;/li&gt;
&lt;li&gt;Give critical workloads &lt;code&gt;Guaranteed&lt;/code&gt; QoS (requests == limits) so they're evicted last.&lt;/li&gt;
&lt;li&gt;Add capacity or enable the cluster autoscaler so pressure triggers a scale-out instead of a massacre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the deepest confirmation, the kernel logs the kill on the node itself: &lt;code&gt;dmesg -T | grep -i oom&lt;/code&gt; shows the &lt;code&gt;Out of memory: Killed process ...&lt;/code&gt; line with the exact PID and RSS the kernel objected to.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable checklist
&lt;/h2&gt;

&lt;p&gt;When a pod is &lt;code&gt;OOMKilled&lt;/code&gt;, run this in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl describe pod &amp;lt;pod&amp;gt;&lt;/code&gt; → confirm &lt;code&gt;Reason: OOMKilled&lt;/code&gt; + &lt;code&gt;Exit Code: 137&lt;/code&gt; in &lt;code&gt;Last State&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl get pod &amp;lt;pod&amp;gt; -o jsonpath='{.spec.containers[0].resources}'&lt;/code&gt; → read the limit that was breached.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;watch kubectl top pod &amp;lt;pod&amp;gt; --containers&lt;/code&gt; → &lt;strong&gt;flat plateau = raise the limit; steady climb = leak.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Too-low limit → set &lt;code&gt;requests == limits&lt;/code&gt;, leave runtime headroom (&lt;code&gt;MaxRAMPercentage=75&lt;/code&gt;, &lt;code&gt;--max-old-space-size&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Leak → confirm with &lt;code&gt;container_memory_working_set_bytes&lt;/code&gt;, then heap-dump the runtime and fix the code.&lt;/li&gt;
&lt;li&gt;Container under its limit but still killed → node-level OOM; check overcommit and QoS, add requests, scale out.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't wait for the kill — alert on the approach
&lt;/h2&gt;

&lt;p&gt;OOMKilled is one of the few pod failures you can see coming. When a container's working set is riding at 90% of its limit, it's a scheduled outage, not a surprise. Wire that into monitoring so it pages before the kernel acts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container_memory_working_set_bytes{container!=""}
  / on(pod, container) kube_pod_container_resource_limits{resource="memory"}
  &amp;gt; 0.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alert on that ratio in the &lt;a href="https://devtocash.com/blog/2026-03-26-kubernetes-monitoring-prometheus-grafana" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring setup&lt;/a&gt; and you convert a 2 a.m. crash loop into a business-hours ticket. Bake the six-step diagnosis into your &lt;a href="https://devtocash.com/blog/incident-management-runbook-template-2026" rel="noopener noreferrer"&gt;incident runbook&lt;/a&gt; so the next on-call engineer fixes it in minutes. Mis-set memory limits and hard OOMs are among the quiet &lt;a href="https://devtocash.com/blog/2026-04-11-kubernetes-mistakes-that-cost-companies-millions" rel="noopener noreferrer"&gt;Kubernetes mistakes that cost companies millions&lt;/a&gt; — cheap to prevent, expensive to sleep through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-07-14-kubernetes-crashloopbackoff-fix" rel="noopener noreferrer"&gt;Kubernetes CrashLoopBackOff: How to Debug and Fix It&lt;/a&gt;&lt;/strong&gt; — OOMKilled is one of several reasons a container crash-loops; this covers the full exit-code decision tree (1, 127, 143, 0) when the cause isn't memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;Kubernetes ImagePullBackOff: How to Debug and Fix It&lt;/a&gt;&lt;/strong&gt; — the third member of the "my pod won't run" trilogy: the image never pulled, so the container never even started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-06-28-kubernetes-pod-autoscaling-hpa-vpa-and-keda-explained" rel="noopener noreferrer"&gt;Kubernetes Pod Autoscaling: HPA, VPA, and KEDA Explained&lt;/a&gt;&lt;/strong&gt; — let the VPA recommend memory requests from real usage instead of tuning limits by hand every time.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/2026-07-15-kubernetes-oomkilled-exit-code-137-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>sre</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>OpenTelemetry Tutorial 2026: Complete Setup Guide for SRE &amp; DevOps</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:07:24 +0000</pubDate>
      <link>https://dev.to/devtocash/opentelemetry-tutorial-2026-complete-setup-guide-for-sre-devops-482c</link>
      <guid>https://dev.to/devtocash/opentelemetry-tutorial-2026-complete-setup-guide-for-sre-devops-482c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/open-telemetry-tutorial-setup-guide-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you operate microservices in production, you already know the pain. A user reports a slow checkout. You open three different dashboards — Grafana for metrics, Jaeger for traces, and grep for logs. By the time you correlate the request ID across all three, the incident has been open for 45 minutes.&lt;/p&gt;

&lt;p&gt;OpenTelemetry (OTel) solves this by unifying all three signals under one standard. It is now the CNCF's second-most active project after Kubernetes, and every major observability vendor — Datadog, Honeycomb, Grafana Labs, New Relic — has adopted its protocol. In 2026, if you are not instrumenting with OpenTelemetry, you are building technical debt every time you ship code.&lt;/p&gt;

&lt;p&gt;This tutorial walks you through a complete OpenTelemetry setup: instrumentation with the OTel SDK, collector configuration, and exporting traces and metrics to Jaeger and Prometheus. Everything is hands-on with real YAML and code snippets you can run today.&lt;/p&gt;

&lt;p&gt;By the end, you will have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Python service auto-instrumented with traces and metrics&lt;/li&gt;
&lt;li&gt;An OpenTelemetry Collector processing and exporting telemetry&lt;/li&gt;
&lt;li&gt;Traces visible in Jaeger and metrics scraped by Prometheus&lt;/li&gt;
&lt;li&gt;A working mental model of OTel's pipeline architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is OpenTelemetry, Actually?
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry is not a backend. It is not a database, a dashboard, or an alerting engine. It is a &lt;strong&gt;telemetry pipeline standard&lt;/strong&gt; — a specification, a set of SDKs, and a collector binary that together generate, process, and export traces, metrics, and logs.&lt;/p&gt;

&lt;p&gt;The project emerged from the 2019 merger of OpenTracing and OpenCensus. Both were CNCF observability projects with overlapping goals. Rather than compete, they merged into a single standard. Today, OTel is at version 1.34+ and is considered stable for traces and metrics.&lt;/p&gt;

&lt;p&gt;Three things make OpenTelemetry different from what came before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vendor-neutral instrumentation.&lt;/strong&gt; You instrument once with the OTel SDK. Changing backends — from Jaeger to Honeycomb, or from Prometheus to Datadog — means changing an exporter config, not rewriting code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Collector.&lt;/strong&gt; A standalone binary that receives, processes, and exports telemetry. You can run it as a sidecar, a daemonset, or a central gateway. It handles batching, filtering, sampling, and routing — all config-driven.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context propagation.&lt;/strong&gt; The &lt;code&gt;traceparent&lt;/code&gt; header (W3C Trace Context standard) passes trace context across HTTP, gRPC, and message queues. Every hop in your distributed system links back to a single root span without custom headers.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The telemetry pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application Code --&amp;gt; OTel SDK --&amp;gt; OTel Collector --&amp;gt; Backend (Jaeger/Prometheus/...)
     (API calls)       (auto/manual)    (process/route)         (store/query)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK generates spans and metrics inside your application process. The Collector — a separate binary — receives them via OTLP (OpenTelemetry Protocol) over gRPC or HTTP, then applies processors and exports to one or more backends.&lt;/p&gt;

&lt;p&gt;This separation matters. Your application never talks directly to Jaeger or Prometheus. It only talks to the Collector. The Collector absorbs backend changes without touching application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry Architecture: The Pipeline Model
&lt;/h2&gt;

&lt;p&gt;Every observability signal in OTel follows the same pipeline: &lt;strong&gt;Instrumentation → Processing → Export.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Instrumentation Libraries (SDK)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The SDK lives inside your application process. It creates spans, records metrics, and captures log events. OTel provides SDKs for Python, Go, Java, JavaScript, .NET, Rust, and more. You can use auto-instrumentation (zero code changes — the agent injects hooks at runtime) or manual instrumentation (explicit &lt;code&gt;start_span()&lt;/code&gt; and &lt;code&gt;end_span()&lt;/code&gt; calls in your code).&lt;/p&gt;

&lt;p&gt;Auto-instrumentation covers most common libraries by default: HTTP frameworks (Flask, Express, Spring), database drivers (psycopg2, pgx, JDBC), and gRPC clients. For custom business logic, you add manual spans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The OpenTelemetry Collector&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Collector is the backbone of any production OTel deployment. It is a single Go binary (&lt;code&gt;otelcol-contrib&lt;/code&gt;) that runs three types of components in a pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receivers:&lt;/strong&gt; Accept telemetry data (OTLP gRPC, OTLP HTTP, Jaeger, Zipkin, Prometheus scrape)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processors:&lt;/strong&gt; Transform data in-flight (batch, filter, tail sampling, attributes mutation, redaction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exporters:&lt;/strong&gt; Send data to backends (Jaeger, Prometheus, Datadog, Honeycomb, Kafka, stdout)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Collector decouples your application from backends. If you switch from Jaeger to Tempo, or add a second exporter for Honeycomb, you change one YAML file — not every microservice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Exporters and Backends&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Exporters are protocol-specific components that push data to observability backends. Common exporters include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exporter&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Typical Backend&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;otlp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gRPC/HTTP&lt;/td&gt;
&lt;td&gt;Any OTLP-compatible backend (Jaeger, Tempo, Grafana Agent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP scrape&lt;/td&gt;
&lt;td&gt;Prometheus server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jaeger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Thrift/gRPC&lt;/td&gt;
&lt;td&gt;Jaeger backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stdout&lt;/td&gt;
&lt;td&gt;Debugging during development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafka&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kafka&lt;/td&gt;
&lt;td&gt;Long-term buffering, multi-datacenter pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The OTLP Protocol
&lt;/h3&gt;

&lt;p&gt;All communication between the SDK and the Collector uses OTLP (OpenTelemetry Protocol). OTLP is a Protobuf-based protocol that runs over gRPC (port 4317) or HTTP/1.1 (port 4318). In 2026, OTLP over HTTP has matured enough that many teams prefer it over gRPC for simpler firewall traversal and load balancer compatibility.&lt;/p&gt;

&lt;p&gt;A typical OTLP trace payload is a binary-encoded Protobuf message containing resource attributes (service name, host, namespace), span data (trace ID, span ID, parent span ID, start/end timestamps, attributes, events), and instrumentation scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the OpenTelemetry Collector
&lt;/h2&gt;

&lt;p&gt;Let's start with the Collector — it is the first piece you deploy because your applications need somewhere to send telemetry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Collector
&lt;/h3&gt;

&lt;p&gt;The recommended distribution is &lt;code&gt;otelcol-contrib&lt;/code&gt;, which includes receivers and exporters for every major observability tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux (AMD64)&lt;/span&gt;
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.110.0/otelcol-contrib_0.110.0_linux_amd64.tar.gz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; otelcol-contrib_0.110.0_linux_amd64.tar.gz
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;otelcol-contrib /usr/local/bin/

&lt;span class="c"&gt;# Verify&lt;/span&gt;
otelcol-contrib &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Docker-based development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; otel-collector &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 4317:4317 &lt;span class="nt"&gt;-p&lt;/span&gt; 4318:4318 &lt;span class="nt"&gt;-p&lt;/span&gt; 8888:8888 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/otel-config.yaml:/etc/otelcol/config.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  otel/opentelemetry-collector-contrib:0.110.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the Collector Configuration
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;otel-config.yaml&lt;/code&gt;. This is the heart of your observability pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4318&lt;/span&gt;

&lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;send_batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
  &lt;span class="na"&gt;memory_limiter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;check_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
    &lt;span class="na"&gt;limit_mib&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
  &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environment&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upsert&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jaeger-collector:14250&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:8889&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel&lt;/span&gt;
  &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;loglevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;memory_limiter&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;jaeger&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;memory_limiter&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration does several things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receivers&lt;/strong&gt; listen on ports 4317 (gRPC) and 4318 (HTTP) for OTLP data from applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processors&lt;/strong&gt; batch spans for efficiency, limit memory usage to 512 MiB, and add an &lt;code&gt;environment=production&lt;/code&gt; attribute to every span&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exporters&lt;/strong&gt; forward traces to Jaeger, expose metrics on port 8889 for Prometheus scraping, and log debug output to stdout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelines&lt;/strong&gt; wire everything together — traces and metrics take different paths through the same Collector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;batch&lt;/code&gt; processor is critical for production. Without it, the Collector sends one span at a time to Jaeger, creating massive network overhead. Batching amortizes the cost across 512 spans.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Collector
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;otelcol-contrib &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;otel-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see log output confirming that all receivers, processors, and exporters are active. The Collector is now ready to receive telemetry from your applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting Your First Application
&lt;/h2&gt;

&lt;p&gt;Now that the Collector is running, let's instrument a Python web service. We will use Flask for the HTTP layer and the OpenTelemetry Python SDK for auto-instrumentation, then add manual spans for custom business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask opentelemetry-api opentelemetry-sdk &lt;span class="se"&gt;\&lt;/span&gt;
  opentelemetry-instrumentation-flask &lt;span class="se"&gt;\&lt;/span&gt;
  opentelemetry-instrumentation-requests &lt;span class="se"&gt;\&lt;/span&gt;
  opentelemetry-exporter-otlp-proto-grpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;opentelemetry-api&lt;/code&gt; and &lt;code&gt;opentelemetry-sdk&lt;/code&gt; — the core OTel SDK&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opentelemetry-instrumentation-flask&lt;/code&gt; — auto-instrumentation for Flask (creates spans for each HTTP request automatically)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opentelemetry-instrumentation-requests&lt;/code&gt; — auto-instrumentation for the &lt;code&gt;requests&lt;/code&gt; library (spans for outbound HTTP calls)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opentelemetry-exporter-otlp-proto-grpc&lt;/code&gt; — the OTLP exporter that sends data to our Collector&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Write the Application
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.resources&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SERVICE_NAME&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.exporter.otlp.proto.grpc.trace_exporter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.instrumentation.flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FlaskInstrumentor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.instrumentation.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RequestsInstrumentor&lt;/span&gt;

&lt;span class="c1"&gt;# --- OTel Setup ---
&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout-service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployment.environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;otlp_exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:4317&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;insecure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otlp_exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Application ---
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Auto-instrument Flask and outgoing HTTP requests
&lt;/span&gt;&lt;span class="nc"&gt;FlaskInstrumentor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;instrument_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;RequestsInstrumentor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Get a tracer for manual instrumentation
&lt;/span&gt;&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Process a checkout — spans created automatically by FlaskInstrumentor.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Manual span for the payment processing step
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_payment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment.amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment.method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Simulate payment work
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;payment_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment.status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Manual span for inventory update
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update_inventory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inventory.items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])))&lt;/span&gt;

        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Outbound HTTP call — automatically traced by RequestsInstrumentor
&lt;/span&gt;        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://inventory-service:5001/update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inventory.response_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ord-2026-abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Simulated payment gateway call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txn-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What This Code Does
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;/checkout&lt;/code&gt; request now generates a trace with multiple spans:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Root span&lt;/strong&gt; — created automatically by &lt;code&gt;FlaskInstrumentor&lt;/code&gt; for the HTTP request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;process_payment&lt;/code&gt;&lt;/strong&gt; — manual span wrapping payment logic, with custom attributes (amount, method, status)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;update_inventory&lt;/code&gt;&lt;/strong&gt; — manual span wrapping inventory logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP POST to inventory-service&lt;/strong&gt; — nested span created by &lt;code&gt;RequestsInstrumentor&lt;/code&gt;, linked to the parent &lt;code&gt;update_inventory&lt;/code&gt; span&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Context propagation is automatic. When the &lt;code&gt;/checkout&lt;/code&gt; handler calls &lt;code&gt;requests.post(...)&lt;/code&gt;, the OTel SDK injects the &lt;code&gt;traceparent&lt;/code&gt; header into the outbound HTTP request. If the inventory service is also instrumented with OTel, it extracts that header and continues the same trace — creating a single distributed trace across both services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run and Verify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: Start the Collector (if not already running)&lt;/span&gt;
otelcol-contrib &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;otel-config.yaml

&lt;span class="c"&gt;# Terminal 2: Start the Flask app&lt;/span&gt;
python app.py

&lt;span class="c"&gt;# Terminal 3: Generate a trace&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5000/checkout &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount": 49.99, "method": "card", "items": [{"id": 1}, {"id": 2}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the Collector's debug log output. You should see spans being received, processed, and exported. The logging exporter will print span summaries to stdout — useful for debugging before you wire up Jaeger.&lt;/p&gt;

&lt;p&gt;Look for lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Span #0
    Trace ID       : 6e8f4c7a1b2d3e4f5a6b7c8d9e0f1a2b
    Parent ID      :
    ID             : 3a4b5c6d7e8f9a0b
    Name           : POST /checkout
    Kind           : Server
    ...

Span #1
    Trace ID       : 6e8f4c7a1b2d3e4f5a6b7c8d9e0f1a2b
    Parent ID      : 3a4b5c6d7e8f9a0b
    ID             : 1b2c3d4e5f6a7b8c
    Name           : process_payment
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared &lt;code&gt;Trace ID&lt;/code&gt; across both spans confirms that context propagation is working — both spans belong to the same distributed trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exporting Traces to Jaeger
&lt;/h2&gt;

&lt;p&gt;The logging exporter is useful for debugging, but you need a real trace backend. Let's set up Jaeger and configure the Collector to forward traces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Run Jaeger All-in-One
&lt;/h3&gt;

&lt;p&gt;For development, Jaeger's all-in-one image bundles the collector, query UI, and in-memory storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; jaeger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;COLLECTOR_OTLP_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 16686:16686 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 4317:4317 &lt;span class="se"&gt;\&lt;/span&gt;
  jaegertracing/all-in-one:1.62
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Port 16686: Jaeger UI (open &lt;code&gt;http://localhost:16686&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Port 4317: OTLP gRPC receiver (Jaeger can accept OTLP directly as of 1.35+)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, routing through our Collector is the production pattern. Update the Collector config to point at Jaeger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# otel-config.yaml (exporter section update)&lt;/span&gt;
&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp/jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:4317&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="c1"&gt;# ... keep the other exporters&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;memory_limiter&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp/jaeger&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Generate Traces and Inspect
&lt;/h3&gt;

&lt;p&gt;Send a few checkout requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 5&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5000/checkout &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"amount": 49.99, "method": "card", "items": [{"id": 1}]}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open Jaeger UI at &lt;code&gt;http://localhost:16686&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;code&gt;checkout-service&lt;/code&gt; from the Service dropdown&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Find Traces&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You should see 5 traces, each containing multiple spans&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Click any trace to view the waterfall diagram. You will see the parent &lt;code&gt;POST /checkout&lt;/code&gt; span and its children — &lt;code&gt;process_payment&lt;/code&gt;, &lt;code&gt;update_inventory&lt;/code&gt;, and potentially the outbound HTTP call to inventory-service. Expand a span to see attributes like &lt;code&gt;payment.amount&lt;/code&gt;, &lt;code&gt;payment.method&lt;/code&gt;, and &lt;code&gt;payment.status&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging Tip: Missing Spans
&lt;/h3&gt;

&lt;p&gt;If you see the root span but not the child spans, check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify the Collector is receiving spans&lt;/span&gt;
curl http://localhost:8888/metrics | &lt;span class="nb"&gt;grep &lt;/span&gt;otelcol_receiver_accepted_spans

&lt;span class="c"&gt;# Check Collector logs for export errors&lt;/span&gt;
otelcol-contrib &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;otel-config.yaml 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch processor delay:&lt;/strong&gt; Spans are batched for up to 5 seconds before export. Wait at least 5 seconds after sending a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTLP endpoint mismatch:&lt;/strong&gt; The SDK sends to &lt;code&gt;localhost:4317&lt;/code&gt; but the Collector listens on a different host. Use &lt;code&gt;0.0.0.0:4317&lt;/code&gt; in the Collector config for local dev.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS mismatch:&lt;/strong&gt; If the Collector expects TLS but the SDK sends plaintext (or vice versa), the connection fails silently. Match &lt;code&gt;insecure: true&lt;/code&gt; settings on both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exporting Metrics to Prometheus
&lt;/h2&gt;

&lt;p&gt;Traces tell you what happened. Metrics tell you how often and how fast. OTel's metrics pipeline works the same way, but the Prometheus exporter is an HTTP server that Prometheus scrapes — it does not push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Configure Prometheus Scrape
&lt;/h3&gt;

&lt;p&gt;Add a scrape target to your &lt;code&gt;prometheus.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;otel-collector"&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:8889"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Collector's Prometheus exporter already listens on port 8889 (from our earlier config). No additional setup is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Auto-Instrument Metrics
&lt;/h3&gt;

&lt;p&gt;The Flask instrumentation also captures HTTP server metrics automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add to app.py after the trace setup
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MeterProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PeriodicExportingMetricReader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.exporter.otlp.proto.grpc.metric_exporter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OTLPMetricExporter&lt;/span&gt;

&lt;span class="n"&gt;metric_reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PeriodicExportingMetricReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;OTLPMetricExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:4317&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;insecure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;export_interval_millis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;meter_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MeterProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metric_readers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;metric_reader&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_meter_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exports HTTP request counts, latency histograms, and error rates — all generated automatically by &lt;code&gt;FlaskInstrumentor&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Verify Metrics in Prometheus
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check that Prometheus is scraping the Collector&lt;/span&gt;
curl http://localhost:9090/api/v1/targets | jq &lt;span class="s1"&gt;'.data.activeTargets[] | select(.labels.job=="otel-collector")'&lt;/span&gt;

&lt;span class="c"&gt;# Query a metric&lt;/span&gt;
curl &lt;span class="s2"&gt;"http://localhost:9090/api/v1/query?query=http_server_duration_milliseconds_bucket"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metrics pipeline is now live: your application generates metrics, the SDK ships them to the Collector, and Prometheus scrapes the Collector's Prometheus exporter endpoint. Grafana can query Prometheus to build dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a Custom Metric
&lt;/h3&gt;

&lt;p&gt;Beyond auto-instrumentation, add a business-level counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;

&lt;span class="n"&gt;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_meter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;order_counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkout.orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Number of completed checkouts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# ... existing code ...
&lt;/span&gt;    &lt;span class="n"&gt;order_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a &lt;code&gt;checkout_orders_total&lt;/code&gt; metric in Prometheus, labeled by payment method. Query it to track business throughput — not just infrastructure health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying OpenTelemetry on Kubernetes
&lt;/h2&gt;

&lt;p&gt;Running the Collector as a standalone binary works for development. In production, you deploy it to Kubernetes using one of three patterns. Each has tradeoffs in scalability, latency, and operational complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Sidecar (Per-Pod Collector)
&lt;/h3&gt;

&lt;p&gt;A Collector container runs alongside your application container in the same pod. The application sends telemetry to &lt;code&gt;localhost:4317&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# deployment.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-service&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-service&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-service:latest&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:4317"&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel/opentelemetry-collector-contrib:0.110.0&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config=/etc/otelcol/config.yaml"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-config&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/otelcol&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-config&lt;/span&gt;
          &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector-sidecar-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Simple, no network hops, pod-level isolation.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; One Collector per pod wastes resources. 100 pods = 100 Collectors. Not suitable for large clusters unless you run low-resource Collector replicas.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pattern 2: DaemonSet (Per-Node Collector)
&lt;/h3&gt;

&lt;p&gt;One Collector runs on every node as a DaemonSet. All pods on that node send telemetry to the node-local Collector via the host network or a node port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# otel-daemonset.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DaemonSet&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;observability&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel/opentelemetry-collector-contrib:0.110.0&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config=/etc/otelcol/config.yaml"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4317&lt;/span&gt;
              &lt;span class="na"&gt;hostPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4317&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4318&lt;/span&gt;
              &lt;span class="na"&gt;hostPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4318&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-config&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/otelcol&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-config&lt;/span&gt;
          &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector-daemonset-config&lt;/span&gt;


&lt;span class="c1"&gt;## Advanced Sampling Strategies&lt;/span&gt;

&lt;span class="s"&gt;Tail sampling is one of OpenTelemetry's most powerful features — and one of the easiest to misconfigure. Understanding the decision flow will save you from exploding your telemetry bill or dropping critical traces.&lt;/span&gt;

&lt;span class="c1"&gt;### Head-Based Sampling (Probabilistic)&lt;/span&gt;

&lt;span class="s"&gt;Head sampling happens at span creation time. The SDK decides immediately whether to keep or drop a span — no buffering, no delay. This is the default if you configure nothing else&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;/p&gt;
&lt;h1&gt;
  
  
  Collector config for head-based probabilistic sampling
&lt;/h1&gt;

&lt;p&gt;processors:&lt;br&gt;
  probabilistic_sampler:&lt;br&gt;
    sampling_percentage: 10&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
This means 10% of all spans are kept, 90% are dropped instantly. The Collector never sees the dropped spans at all — those bytes never leave the application process. Use this when:

- **You are cost-sensitive:** Every exported span costs storage and network. At 1,000 requests per second, keeping 100% of spans can saturate your observability budget.
- **You want trace completeness, not sample size:** If you are debugging a specific slow request, dropping spans at the head means you lose context. Probabilistic sampling gives you a representative subset.

### Tail-Based Sampling

Tail sampling makes the decision **after** all spans in a trace complete — 5 to 30 seconds later, when the full trace is assembled in the Collector. The processor evaluates decision policies:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;br&gt;
processors:&lt;br&gt;
  tail_sampling:&lt;br&gt;
    decision_wait: 30s&lt;br&gt;
    policies:&lt;br&gt;
      - name: errors-and-slow&lt;br&gt;
        type: and&lt;br&gt;
        and_sub_policy:&lt;br&gt;
          - name: status_code&lt;br&gt;
            type: status_code&lt;br&gt;
            status_code: {status_codes: [ERROR]}&lt;br&gt;
          - name: latency-over-2s&lt;br&gt;
            type: latency&lt;br&gt;
            latency: {threshold_ms: 2000}&lt;br&gt;
      - name: probabilistic&lt;br&gt;
        type: probabilistic&lt;br&gt;
        probabilistic: {sampling_percentage: 25}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
This configuration keeps 100% of traces that contain an error status code, samples 25% of all other traces, and drops the rest. Additionally, it keeps any span whose total duration exceeds 2 seconds. Tail sampling lets you capture the full picture of every slow request without storing every fast one.

### When to Use Each

| Strategy | When to Use |
|----------|-------------|
| **Head (probabilistic)** | You have a strict sampling budget. You cannot store more than X spans per second. Use for high-throughput, cost-sensitive, always-on observability. |
| **Tail (policy-based)** | You need every trace from a specific slow request. Use when debugging errors, analyzing latency, or auditing compliance. |

## Common Pitfalls and Troubleshooting

### 1. The Collector Is Dropping Spans Silently

This is the most common OTel production issue. The Collector receives spans from the SDK, processes them through the batch processor, then drops them silently at the exporter. Root cause: the gRPC connection between the SDK and Collector times out.

**Fix:** Increase the `send_batch_size` and reduce `timeout` in the batch processor:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;br&gt;
processors:&lt;br&gt;
  batch:&lt;br&gt;
    timeout: 10s&lt;br&gt;
    send_batch_size: 2048&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Why this works: the default batch size is 512 spans. If the Collector receives 2,000 spans in 1 second, 1,488 of them exceed the default gRPC message size (4 MiB). The SDK sends 512 spans at a time. The Collector times out waiting for the remaining 488 — and drops them. Increase the batch size to 2,048 so the SDK sends larger chunks, fewer network round-trips.

### 2. The `traceparent` Header Is Missing

Your service A calls service B over HTTP. Service B is also instrumented with OTel. But the trace breaks — service B does not receive the `traceparent` header, so spans link back to service A but not to the same trace.

**Diagnosis:** Check for `traceparent` in the outbound HTTP headers:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
curl -H "traceparent: 00-..." &lt;a href="http://service-b:5000/endpoint" rel="noopener noreferrer"&gt;http://service-b:5000/endpoint&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
If the response header is missing, service B is not propagating context. The SDK does not inject `traceparent` into the outbound request. Fix: verify the instrumentation library is loaded and the HTTP client is configured.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;/p&gt;
&lt;h1&gt;
  
  
  Explicitly configure the OTLP exporter with headers
&lt;/h1&gt;

&lt;p&gt;from opentelemetry.propagators.textmap import TextMapPropagator&lt;br&gt;
from opentelemetry import trace&lt;/p&gt;
&lt;h1&gt;
  
  
  Set the global propagator BEFORE creating the TracerProvider
&lt;/h1&gt;

&lt;p&gt;trace.set_span_processor(&lt;br&gt;
    CompositePropagator(&lt;br&gt;
        propagators=[&lt;br&gt;
            W3CTraceContextPropagator(),&lt;br&gt;
            BaggagePropagator()&lt;br&gt;
        ]&lt;br&gt;
    )&lt;br&gt;
)&lt;/p&gt;
&lt;h1&gt;
  
  
  Then create the TracerProvider
&lt;/h1&gt;

&lt;p&gt;provider = TracerProvider()&lt;br&gt;
trace.set_tracer_provider(provider)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
The `W3CTraceContextPropagator` injects the W3C `traceparent` header into every outbound HTTP request. Without it, distributed context propagation fails silently.

### 3. High Cardinality Attributes Crash the Backend

Span attributes like `user.id`, `request.id`, and `session.id` are unbounded. If a span carries thousands of unique attributes, the Jaeger backend rejects the entire batch.

**Remediation:** Drop high-cardinality attributes at the SDK level:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
python&lt;/p&gt;
&lt;h1&gt;
  
  
  Create a custom SpanProcessor that truncates attributes
&lt;/h1&gt;

&lt;p&gt;from opentelemetry.sdk.trace.export import SpanExporter, BatchSpanProcessor&lt;/p&gt;

&lt;p&gt;class AttributeLimitingProcessor(BatchSpanProcessor):&lt;br&gt;
    def on_end(self, span):&lt;br&gt;
        # Keep only these attributes — drop everything else&lt;br&gt;
        allowed_keys = {"http.method", "http.url", "http.status_code"}&lt;br&gt;
        span.attributes = {&lt;br&gt;
            k: v for k, v in span.attributes.items()&lt;br&gt;
            if k in allowed_keys&lt;br&gt;
        }&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
This SpanProcessor limits attributes to `http.method`, `http.url`, and `http.status_code` — dropping `user.id`, session tokens, and every other high-cardinality field. The backend stays stable.

### 4. Memory Usage Grows Unbounded

The Collector's memory consumption grows linearly with every span. Under sustained load, 512 MiB becomes 1 GiB, then 2 GiB. The OOM killer strikes.

**Fix:** Configure the `memory_limiter` processor aggressively:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;br&gt;
processors:&lt;br&gt;
  memory_limiter:&lt;br&gt;
    limit_mib: 256&lt;br&gt;
    spike_limit_mib: 512&lt;br&gt;
    check_interval: 1s&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
The `limit_mib` sets a hard cap at 256 MiB. The `spike_limit_mib` allows brief spikes to 512 MiB during batch exports. Set both lower than the container memory limit if the Collector also runs a sidecar.

## Security: Redacting Sensitive Data

OpenTelemetry traces can leak secrets. A span attribute like `credit_card_number` or `user.email` travels from your SDK through the Collector to Jaeger — and into your observability vendor's cloud. Every hop stores the attribute permanently.

**Prevention:** Filter sensitive attributes at the Collector level before they leave your network:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
yaml&lt;br&gt;
processors:&lt;br&gt;
  attributes:&lt;br&gt;
    actions:&lt;br&gt;
      - key: user.email&lt;br&gt;
        action: delete&lt;br&gt;
      - key: user.phone&lt;br&gt;
        action: delete&lt;br&gt;
      - key: credit_card.*&lt;br&gt;
        action: delete&lt;br&gt;
      - key: password&lt;br&gt;
        action: delete&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


This configuration strips `user.email`, `user.phone`, and any attribute matching the pattern `credit_card.*` or `password` from every span before it reaches the exporter. The sensitive data never leaves your boundary. Combine this with the [`k8sattributes` processor](https://opentelemetry.io/docs/kubernetes/collector/processor/) to redact by label or annotation.

For full defense in depth, review the [OTel Security documentation](https://opentelemetry.io/docs/security/).

## Further Reading

If you have made it this far, you now have a working OpenTelemetry pipeline — instrumentation, a Collector, and at least one observability backend. Here is where to go next:

- **[Kubernetes Security Best Practices 2026](https://devtocash.com/blog/kubernetes-security-best-practices-2026)** — Hardening your cluster before instrumenting your workloads. Security is not optional when observability is production.
- **[Error Budgets: Stop Wasting Your SRE Team's Time](https://devtocash.com/blog/error-budgets-sre-guide)** — Budget for reliability, not just velocity. Your error budget is a policy decision, not a suggestion.
- **OpenTelemetry Tracing: Instrument Your First Application** *(forthcoming)* — Distributed tracing with manual context propagation. A complete guide to instrumenting every service.

## Conclusion

OpenTelemetry is not a tool — it is a standard. Adopting it means instrumenting once with the SDK, processing through the Collector, and exporting to any backend without rewriting code. You have now walked through a complete setup: instrumentation with Python and Flask, Collector configuration in YAML, Jaeger for trace visualization, Prometheus for metrics, Kubernetes for production deployment, and operational patterns from DaemonSet to Gateway.

The most important things to remember:

1. **Instrument once, export anywhere.** The OTel SDK decouples your application from every backend. Changing exporters in the Collector config is not a code change.
2. **The Collector is your control plane.** Receivers, processors, and exporters form a pipeline. Data flows one way — from your code through the SDK to the Collector, then to Jaeger and Prometheus. You control the flow.
3. **Tail sampling saves budget.** Not every span is worth storing. Decide what to keep at the head (probabilistic) or at the tail (policy-based). The Collector makes the decision.

The observability landscape in 2026 is converging on OpenTelemetry. Every major vendor now speaks OTLP. The standard is the protocol — adopt it before it becomes a migration project.

---

📌 **Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on [devtocash.com](https://devtocash.com/blog/open-telemetry-tutorial-setup-guide-2026).**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>distributedtracing</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>Kubernetes ImagePullBackOff: How to Debug and Fix It (2026)</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:17:40 +0000</pubDate>
      <link>https://dev.to/devtocash/kubernetes-imagepullbackoff-how-to-debug-and-fix-it-2026-2cmm</link>
      <guid>https://dev.to/devtocash/kubernetes-imagepullbackoff-how-to-debug-and-fix-it-2026-2cmm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What ImagePullBackOff actually means
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt; means the kubelet tried to pull your container image, failed, and is now waiting — with an exponential backoff — before trying again. Like &lt;code&gt;CrashLoopBackOff&lt;/code&gt;, it is a &lt;em&gt;symptom&lt;/em&gt;, not a root cause. The real failure is the pull itself, and the kubelet already recorded exactly why in the pod's events. You never have to guess.&lt;/p&gt;

&lt;p&gt;The important distinction: &lt;code&gt;ErrImagePull&lt;/code&gt; is the &lt;em&gt;first&lt;/em&gt; failed attempt, and &lt;code&gt;ImagePullBackOff&lt;/code&gt; is what you see after the kubelet starts backing off (10s, 20s, 40s, capped at 5 minutes). Same underlying problem. Deleting the pod does nothing — a new pod pulls the same broken reference and lands in the same state. This is the exact sequence I run to find the cause in about a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Read the actual pull error
&lt;/h2&gt;

&lt;p&gt;Never start from theory. Start from &lt;code&gt;describe&lt;/code&gt;, because the &lt;code&gt;Events&lt;/code&gt; block quotes the container runtime's error verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f4c8b6-xk2mn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scroll to the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Warning  Failed     kubelet  Failed to pull image
           "myregistry/payments-api:1.4.2": failed to resolve reference:
           unexpected status: 401 Unauthorized
  Warning  Failed     kubelet  Error: ErrImagePull
  Normal   BackOff    kubelet  Back-off pulling image
  Warning  Failed     kubelet  Error: ImagePullBackOff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line — &lt;code&gt;401 Unauthorized&lt;/code&gt; — routes the entire investigation. The runtime error text maps cleanly to a root cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error text&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Go to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;not found&lt;/code&gt; / &lt;code&gt;manifest unknown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Wrong image name or tag&lt;/td&gt;
&lt;td&gt;Step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401 Unauthorized&lt;/code&gt; / &lt;code&gt;denied&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Missing or wrong registry credentials&lt;/td&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;429 Too Many Requests&lt;/code&gt; / &lt;code&gt;toomanyrequests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Docker Hub anonymous rate limit&lt;/td&gt;
&lt;td&gt;Step 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no such host&lt;/code&gt; / &lt;code&gt;i/o timeout&lt;/code&gt; / &lt;code&gt;connection refused&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Node can't reach the registry&lt;/td&gt;
&lt;td&gt;Step 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no match for platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture mismatch (arm64 vs amd64)&lt;/td&gt;
&lt;td&gt;Step 6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read this first and the rest collapses to a single path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Wrong image name or tag (&lt;code&gt;manifest unknown&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The most common cause is the simplest: the image reference doesn't exist. A typo in the repository, a tag that was never pushed, or a CI pipeline that pushed &lt;code&gt;1.4.2&lt;/code&gt; while your manifest still says &lt;code&gt;1.4.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Confirm exactly what the pod is asking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].image}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify that reference actually exists in the registry from your workstation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker manifest inspect myregistry/payments-api:1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns &lt;code&gt;manifest unknown&lt;/code&gt;, the tag isn't there. Two things to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A CI race.&lt;/strong&gt; Your deploy ran before the image push finished. This is common when build and deploy are separate jobs — pin the deploy to the image digest (&lt;code&gt;@sha256:...&lt;/code&gt;) instead of a moving tag so a deploy can never reference a non-existent image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;:latest&lt;/code&gt; trap.&lt;/strong&gt; If you use &lt;code&gt;:latest&lt;/code&gt; with &lt;code&gt;imagePullPolicy: IfNotPresent&lt;/code&gt;, a node that already cached an old &lt;code&gt;latest&lt;/code&gt; will &lt;em&gt;silently run stale code&lt;/em&gt; instead of failing. Always tag immutably (&lt;code&gt;1.4.2&lt;/code&gt;, a git SHA) in production. This is one of the &lt;a href="https://devtocash.com/blog/2026-04-11-kubernetes-mistakes-that-cost-companies-millions" rel="noopener noreferrer"&gt;Kubernetes mistakes that quietly cost companies money&lt;/a&gt; — a "successful" deploy that shipped nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Private registry authentication (&lt;code&gt;401&lt;/code&gt; / &lt;code&gt;denied&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;If the image exists but the pull returns &lt;code&gt;401 Unauthorized&lt;/code&gt; or &lt;code&gt;denied&lt;/code&gt;, the kubelet has no valid credentials for that registry. Kubernetes pulls images using an &lt;code&gt;imagePullSecrets&lt;/code&gt; reference, &lt;em&gt;not&lt;/em&gt; your local &lt;code&gt;docker login&lt;/code&gt;. The node never sees your laptop's &lt;code&gt;~/.docker/config.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create the pull secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret docker-registry regcred &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myregistry.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deploy-bot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then attach it. Either reference it on the pod spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regcred&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myregistry.example.com/payments-api:1.4.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or — cleaner for a whole namespace — patch the default ServiceAccount so every pod inherits it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch serviceaccount default &lt;span class="nt"&gt;-n&lt;/span&gt; prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"imagePullSecrets":[{"name":"regcred"}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas that eat hours:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Namespace scope.&lt;/strong&gt; A pull secret only works in the namespace it was created in. A pod in &lt;code&gt;prod&lt;/code&gt; cannot use a secret in &lt;code&gt;default&lt;/code&gt;. If you have 12 namespaces, you need the secret in each one that pulls private images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker-server&lt;/code&gt; must match the image host exactly.&lt;/strong&gt; If your image is &lt;code&gt;myregistry.example.com/...&lt;/code&gt;, the secret's server must be &lt;code&gt;myregistry.example.com&lt;/code&gt; — not &lt;code&gt;https://...&lt;/code&gt;, not a trailing slash. A mismatch means the kubelet holds a valid credential it never applies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because these secrets carry registry write tokens in some setups, scope them tightly and rotate them — the least-privilege reasoning in the &lt;a href="https://devtocash.com/blog/kubernetes-rbac-deep-dive-2026" rel="noopener noreferrer"&gt;Kubernetes RBAC deep dive&lt;/a&gt; and the broader &lt;a href="https://devtocash.com/blog/kubernetes-security-best-practices-2026" rel="noopener noreferrer"&gt;Kubernetes security best practices&lt;/a&gt; both apply directly to pull credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Docker Hub rate limits (&lt;code&gt;429 Too Many Requests&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;If you pull public images from Docker Hub anonymously, you're capped at 100 pulls per 6 hours per IP. On a busy cluster where many nodes share one NAT egress IP, you hit that ceiling fast, and pulls start failing with &lt;code&gt;toomanyrequests&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to pull image "nginx:1.27": toomanyrequests: You have reached
your pull rate limit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to authenticate even for public pulls, which raises the limit substantially. Create a Docker Hub pull secret exactly as in Step 3 (server &lt;code&gt;docker.io&lt;/code&gt;) and attach it to the ServiceAccount. Better still, run a &lt;strong&gt;pull-through cache&lt;/strong&gt; (Harbor, or the registry mirror built into most managed clusters) so each image is fetched from Docker Hub once and served internally forever after — that also cuts pull latency on scale-ups and reduces your dependency on an external service during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The node can't reach the registry (&lt;code&gt;no such host&lt;/code&gt; / timeout)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;i/o timeout&lt;/code&gt;, &lt;code&gt;no such host&lt;/code&gt;, or &lt;code&gt;connection refused&lt;/code&gt; means DNS or network — the pull never got far enough to check credentials. The problem is on the node, so debug from the node's perspective, not your laptop's.&lt;/p&gt;

&lt;p&gt;Find which node the pod landed on, then test resolution and reachability with a throwaway pod scheduled there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="nt"&gt;-o&lt;/span&gt; wide   &lt;span class="c"&gt;# note the NODE&lt;/span&gt;
kubectl run netcheck &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nicolaka/netshoot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"nslookup myregistry.example.com &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
         curl -sSv https://myregistry.example.com/v2/ 2&amp;gt;&amp;amp;1 | head"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CoreDNS is down or misconfigured&lt;/strong&gt; — every pull &lt;em&gt;and&lt;/em&gt; every service lookup fails cluster-wide. Check &lt;code&gt;kubectl get pods -n kube-system -l k8s-app=kube-dns&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A private registry behind a VPC endpoint&lt;/strong&gt; where the node's security group or NAT route was changed. The registry is reachable from your office VPN but not from the node subnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A self-hosted registry with an untrusted TLS cert&lt;/strong&gt; — &lt;code&gt;x509: certificate signed by unknown authority&lt;/code&gt;. The node's container runtime needs the CA added to its trust store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Persistent pull failures across many nodes are exactly the kind of signal worth alerting on before a scale-up stalls a rollout — the &lt;a href="https://devtocash.com/blog/2026-03-26-kubernetes-monitoring-prometheus-grafana" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring setup&lt;/a&gt; can watch &lt;code&gt;kubelet_image_pull&lt;/code&gt; errors and page you before users notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Architecture mismatch (&lt;code&gt;no match for platform&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;If you build on an Apple Silicon laptop and deploy to &lt;code&gt;amd64&lt;/code&gt; nodes, an image built only for &lt;code&gt;arm64&lt;/code&gt; fails with &lt;code&gt;no match for platform in manifest&lt;/code&gt;. The pull "succeeds" in finding the manifest but has no layer for the node's CPU. Build multi-arch images explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; myregistry.example.com/payments-api:1.4.2 &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pull-time twin of the &lt;code&gt;exec format error&lt;/code&gt; you'd see at &lt;em&gt;runtime&lt;/em&gt; in a crash loop — same architecture root cause, caught one stage earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable checklist
&lt;/h2&gt;

&lt;p&gt;When a pod is stuck in &lt;code&gt;ImagePullBackOff&lt;/code&gt;, run this in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl describe pod&lt;/code&gt; → read the &lt;code&gt;Failed to pull image&lt;/code&gt; event text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;manifest unknown&lt;/code&gt; → wrong name/tag; verify with &lt;code&gt;docker manifest inspect&lt;/code&gt; (Step 2).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;401&lt;/code&gt; / &lt;code&gt;denied&lt;/code&gt; → create and attach an &lt;code&gt;imagePullSecrets&lt;/code&gt;, right namespace, exact server (Step 3).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;429&lt;/code&gt; / &lt;code&gt;toomanyrequests&lt;/code&gt; → authenticate to Docker Hub or run a pull-through cache (Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no such host&lt;/code&gt; / timeout → debug DNS and network &lt;em&gt;from the node&lt;/em&gt; (Step 5).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no match for platform&lt;/code&gt; → build a multi-arch image (Step 6).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ImagePullBackOff looks intimidating because the pod never even starts, but the container runtime always quotes the exact reason in the pod's events. Read that line first and the fix is almost always mechanical. Pin immutable tags, put pull secrets on the ServiceAccount in every namespace, and mirror public images — and most of these loops never reach production. Pair this with the &lt;a href="https://devtocash.com/blog/kubernetes-liveness-readiness-startup-probes-guide" rel="noopener noreferrer"&gt;liveness, readiness, and startup probes guide&lt;/a&gt;, and you've covered nearly every reason a Kubernetes pod fails to come up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-06-28-kubernetes-pod-autoscaling-hpa-vpa-and-keda-explained" rel="noopener noreferrer"&gt;Kubernetes Pod Autoscaling: HPA, VPA, and KEDA Explained&lt;/a&gt;&lt;/strong&gt; — every replica an autoscaler adds has to pull the same image reference; a scale-up event is the fastest way to discover a broken tag or a Docker Hub rate limit at the worst possible time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/docker-multi-stage-builds" rel="noopener noreferrer"&gt;Docker Multi-Stage Builds: Slash Your Image Size by 90%&lt;/a&gt;&lt;/strong&gt; — smaller images pull faster and burn through fewer of Docker Hub's rate-limited requests, which directly reduces how often you hit the &lt;code&gt;429 Too Many Requests&lt;/code&gt; failure mode in Step 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-03-24-docker-compose-vs-kubernetes-when-to-use-what" rel="noopener noreferrer"&gt;Docker Compose vs Kubernetes: When to Use What (2026 Guide)&lt;/a&gt;&lt;/strong&gt; — if your team is still running a single-server Docker Compose stack, you won't hit &lt;code&gt;ImagePullBackOff&lt;/code&gt; the same way; this guide explains when the jump to Kubernetes (and its &lt;code&gt;imagePullSecrets&lt;/code&gt; model) actually pays off.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>sre</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>Kubernetes CrashLoopBackOff: How to Debug and Fix It (2026)</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:16:25 +0000</pubDate>
      <link>https://dev.to/devtocash/kubernetes-crashloopbackoff-how-to-debug-and-fix-it-2026-30o1</link>
      <guid>https://dev.to/devtocash/kubernetes-crashloopbackoff-how-to-debug-and-fix-it-2026-30o1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/2026-07-14-kubernetes-crashloopbackoff-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What CrashLoopBackOff actually means
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CrashLoopBackOff&lt;/code&gt; means your container &lt;em&gt;started&lt;/em&gt;, ran, and then exited — and the kubelet has restarted it enough times that it's now waiting, with an exponential backoff, before trying again. Unlike &lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt;&lt;/a&gt;, the image pulled fine and the process actually executed. Something inside the container is dying, and Kubernetes is doing exactly what you told it to: restart a failed container.&lt;/p&gt;

&lt;p&gt;The word &lt;code&gt;BackOff&lt;/code&gt; is the important part. The kubelet restarts a crashed container immediately, then after 10s, 20s, 40s, doubling up to a 5-minute cap. So a pod that's been crashing for an hour might only restart once every five minutes — the &lt;code&gt;RESTARTS&lt;/code&gt; count climbs slowly even though the loop is constant. Deleting the pod does nothing useful: the replacement runs the same broken code or config and lands in the same state. This is the exact sequence I run to find the cause, usually in a couple of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Read the crash logs — including the &lt;em&gt;previous&lt;/em&gt; container
&lt;/h2&gt;

&lt;p&gt;Never theorize. The crashed process almost always printed why it died before exiting. The catch: by the time you look, the kubelet may have already started a &lt;em&gt;new&lt;/em&gt; container, so plain &lt;code&gt;logs&lt;/code&gt; shows the fresh (and often empty) attempt. You want the logs from the instance that actually crashed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs payments-api-7d9f4c8b6-xk2mn &lt;span class="nt"&gt;--previous&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--previous&lt;/code&gt; (or &lt;code&gt;-p&lt;/code&gt;) flag is the single most useful thing in this entire playbook. It dumps stdout/stderr from the last &lt;em&gt;terminated&lt;/em&gt; container, which is where the stack trace, the &lt;code&gt;panic:&lt;/code&gt;, or the &lt;code&gt;Error: connect ECONNREFUSED&lt;/code&gt; lives. Nine times out of ten the answer is right there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: Missing required environment variable DATABASE_URL
    at loadConfig (/app/config.js:14:11)
&lt;/span&gt;&lt;span class="gp"&gt;    at Object.&amp;lt;anonymous&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;/app/server.js:3:16&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the log is empty, the process died before it could log — a missing binary, a bad entrypoint, or an instant OOM kill. That's what Step 2 is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Decode the exit code from &lt;code&gt;describe&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every terminated container records an exit code, and the exit code narrows the cause immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f4c8b6-xk2mn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;Last State&lt;/code&gt; block under the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;Last State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;Terminated&lt;/span&gt;
      &lt;span class="s"&gt;Reason&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;Error&lt;/span&gt;
      &lt;span class="s"&gt;Exit Code&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;Started&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="na"&gt;Finished&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Map the exit code to a root cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code / Reason&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Go to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; (Reason: Error)&lt;/td&gt;
&lt;td&gt;Application threw and exited — bad config, missing env, startup exception&lt;/td&gt;
&lt;td&gt;Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;137&lt;/code&gt; + Reason &lt;code&gt;OOMKilled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Kernel killed the container for exceeding its memory limit&lt;/td&gt;
&lt;td&gt;Step 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;137&lt;/code&gt; / &lt;code&gt;143&lt;/code&gt; (Reason: Error)&lt;/td&gt;
&lt;td&gt;Process got SIGKILL/SIGTERM — usually a failing liveness probe&lt;/td&gt;
&lt;td&gt;Step 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;127&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;command not found&lt;/code&gt; — bad &lt;code&gt;command&lt;/code&gt;/entrypoint or missing binary&lt;/td&gt;
&lt;td&gt;Step 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;126&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Command found but not executable (bad permissions / wrong arch)&lt;/td&gt;
&lt;td&gt;Step 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; (still looping)&lt;/td&gt;
&lt;td&gt;Process exits cleanly but has nothing to keep it alive&lt;/td&gt;
&lt;td&gt;Step 7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the exit code first and the rest of the investigation collapses to a single branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The app crashes on startup (exit code 1)
&lt;/h2&gt;

&lt;p&gt;Exit &lt;code&gt;1&lt;/code&gt; with a stack trace is the friendliest case — the app told you exactly what's wrong. In practice it's almost always one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A missing or wrong environment variable.&lt;/strong&gt; &lt;code&gt;DATABASE_URL&lt;/code&gt;, an API key, a feature flag the code assumes is set. Check what's actually injected:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;set env &lt;/span&gt;pod/payments-api-7d9f4c8b6-xk2mn &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A missing ConfigMap or Secret key.&lt;/strong&gt; If the pod references a key that doesn't exist, the container may not even start; if it starts but the value is empty, it crashes on first use. Verify the source object exists and has the key:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secret app-secrets &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A bad migration or unreachable dependency at boot.&lt;/strong&gt; Many apps run DB migrations or open a connection pool during startup. If the database isn't reachable yet, they exit non-zero — and Kubernetes crash-loops them until the dependency comes up. The fix is either an &lt;code&gt;initContainer&lt;/code&gt; that waits for the dependency, or making the app retry with backoff instead of exiting. Hard-exiting on a transient dependency failure is one of the quiet &lt;a href="https://devtocash.com/blog/2026-04-11-kubernetes-mistakes-that-cost-companies-millions" rel="noopener noreferrer"&gt;Kubernetes mistakes that cost companies money&lt;/a&gt; — a single slow database restart cascades into every dependent service crash-looping at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: OOMKilled (exit code 137)
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;describe&lt;/code&gt; shows &lt;code&gt;Reason: OOMKilled&lt;/code&gt; and exit &lt;code&gt;137&lt;/code&gt;, the container tried to use more memory than its limit and the kernel killed it. This one is treacherous because the app logs are often empty — the process is killed with SIGKILL and gets no chance to write anything.&lt;/p&gt;

&lt;p&gt;Confirm it and see the limit that was breached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f4c8b6-xk2mn | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"Last State"&lt;/span&gt;
kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].resources}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two distinct fixes, and picking the wrong one wastes a day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The limit is genuinely too low.&lt;/strong&gt; The app needs more memory than you granted. Raise the &lt;code&gt;resources.limits.memory&lt;/code&gt; (and &lt;code&gt;requests&lt;/code&gt; to match, so the scheduler places it correctly). If a JVM or Node process, remember the runtime's heap must fit &lt;em&gt;inside&lt;/em&gt; the container limit with headroom — set &lt;code&gt;-Xmx&lt;/code&gt; or &lt;code&gt;--max-old-space-size&lt;/code&gt; to roughly 75% of the container limit, not equal to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's a memory leak.&lt;/strong&gt; The app grows until it hits any limit you set. Raising the limit just delays the crash. Watch usage over time with &lt;code&gt;kubectl top pod&lt;/code&gt; and fix the leak — a bigger box only buys hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Right-sizing these limits is also a cost lever, not just a stability one: over-provisioned memory requests reserve capacity you pay for and never use. The same discipline shows up in &lt;a href="https://devtocash.com/blog/2026-03-10-kubernetes-cost-optimization-guide" rel="noopener noreferrer"&gt;Kubernetes cost optimization&lt;/a&gt; and &lt;a href="https://devtocash.com/blog/finops-kubernetes-cost-optimization" rel="noopener noreferrer"&gt;FinOps for Kubernetes&lt;/a&gt; — right-sized limits keep pods alive &lt;em&gt;and&lt;/em&gt; keep the bill down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: A failing liveness probe is killing a healthy app
&lt;/h2&gt;

&lt;p&gt;This is the most misdiagnosed CrashLoopBackOff of all. Your app is fine — but the liveness probe is failing, so the kubelet kills the container (SIGTERM, then SIGKILL → exit &lt;code&gt;143&lt;/code&gt;/&lt;code&gt;137&lt;/code&gt;) on a schedule, and the pod loops forever. The tell is in the events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning  Unhealthy  kubelet  Liveness probe failed: HTTP probe failed
                              with statuscode: 500
Normal   Killing    kubelet  Container failed liveness probe, will be
                              restarted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The probe hits an endpoint that depends on a slow downstream&lt;/strong&gt; (a &lt;code&gt;/health&lt;/code&gt; that checks the database). If the DB blips, the probe fails and Kubernetes kills an otherwise-serving app. Liveness should test &lt;em&gt;the process&lt;/em&gt;, not its dependencies — use readiness for dependency checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initialDelaySeconds&lt;/code&gt; is too short&lt;/strong&gt; for an app with a slow boot (JVM warmup, large cache load). The probe starts failing before the app has finished starting, so it never gets a chance to become healthy. Use a &lt;code&gt;startupProbe&lt;/code&gt; for slow starters so the liveness clock doesn't start until the app is actually up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong port or path&lt;/strong&gt; — the probe checks &lt;code&gt;:8080/healthz&lt;/code&gt; but the app serves &lt;code&gt;:3000/health&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full breakdown of which probe does what — and why conflating liveness and readiness causes exactly this loop — is in the &lt;a href="https://devtocash.com/blog/kubernetes-liveness-readiness-startup-probes-guide" rel="noopener noreferrer"&gt;liveness, readiness, and startup probes guide&lt;/a&gt;. Fixing the probe config, not the app, is the fix here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Bad command or wrong architecture (exit 127 / 126)
&lt;/h2&gt;

&lt;p&gt;Exit &lt;code&gt;127&lt;/code&gt; means the container's entrypoint or &lt;code&gt;command&lt;/code&gt; pointed at something that doesn't exist — a typo'd binary path, a script that isn't in the image, or a shell form that assumes &lt;code&gt;/bin/sh&lt;/code&gt; in a distroless image that has none. Exit &lt;code&gt;126&lt;/code&gt; means the file is there but isn't executable, or is built for the wrong CPU (&lt;code&gt;exec format error&lt;/code&gt; — an &lt;code&gt;amd64&lt;/code&gt; binary on an &lt;code&gt;arm64&lt;/code&gt; node, the runtime twin of the platform mismatch you'd catch at pull time).&lt;/p&gt;

&lt;p&gt;Check what the container is actually told to run, and confirm the binary exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod payments-api-7d9f4c8b6-xk2mn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[0].command} {.spec.containers[0].args}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's an architecture mismatch, rebuild multi-arch (&lt;code&gt;docker buildx build --platform linux/amd64,linux/arm64 ...&lt;/code&gt;). If it's a missing shell in a distroless base, switch the probe/command to exec form with a real binary rather than &lt;code&gt;sh -c&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: The container exits 0 and loops anyway
&lt;/h2&gt;

&lt;p&gt;Sometimes the container exits &lt;em&gt;cleanly&lt;/em&gt; (code &lt;code&gt;0&lt;/code&gt;) and still crash-loops. That happens when the main process finishes and there's nothing to keep PID 1 alive — a script that runs once and returns, or a web server started in the background while the foreground command exits. Kubernetes treats "container finished" as "restart it" under the default &lt;code&gt;restartPolicy: Always&lt;/code&gt;. The fix is to run the long-lived process in the foreground as PID 1. For genuine run-once workloads, use a &lt;code&gt;Job&lt;/code&gt; or &lt;code&gt;CronJob&lt;/code&gt; instead of a &lt;code&gt;Deployment&lt;/code&gt; — those are &lt;em&gt;designed&lt;/em&gt; to complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable checklist
&lt;/h2&gt;

&lt;p&gt;When a pod is stuck in &lt;code&gt;CrashLoopBackOff&lt;/code&gt;, run this in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl logs &amp;lt;pod&amp;gt; --previous&lt;/code&gt; → read the crash output from the container that actually died.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl describe pod &amp;lt;pod&amp;gt;&lt;/code&gt; → read the &lt;strong&gt;exit code&lt;/strong&gt; and &lt;strong&gt;Reason&lt;/strong&gt; in &lt;code&gt;Last State&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Exit &lt;code&gt;1&lt;/code&gt; → app-level crash; check env vars, ConfigMaps/Secrets, and startup dependencies (Step 3).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OOMKilled&lt;/code&gt; / &lt;code&gt;137&lt;/code&gt; → raise the memory limit &lt;em&gt;or&lt;/em&gt; fix the leak — decide which with &lt;code&gt;kubectl top&lt;/code&gt; (Step 4).&lt;/li&gt;
&lt;li&gt;Exit &lt;code&gt;143&lt;/code&gt;/&lt;code&gt;137&lt;/code&gt; with a &lt;code&gt;Liveness probe failed&lt;/code&gt; event → fix the probe, not the app (Step 5).&lt;/li&gt;
&lt;li&gt;Exit &lt;code&gt;127&lt;/code&gt;/&lt;code&gt;126&lt;/code&gt; → bad command path or wrong architecture (Step 6).&lt;/li&gt;
&lt;li&gt;Exit &lt;code&gt;0&lt;/code&gt; looping → run the process in the foreground, or use a &lt;code&gt;Job&lt;/code&gt; (Step 7).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CrashLoopBackOff looks alarming because the pod visibly refuses to stay up, but the container runtime records the exit code and the process almost always logs its own cause. Read &lt;code&gt;--previous&lt;/code&gt; logs first, decode the exit code second, and the fix is nearly always mechanical. A rising &lt;code&gt;kube_pod_container_status_restarts_total&lt;/code&gt; is worth alerting on before a loop takes out a whole service — wire it into the &lt;a href="https://devtocash.com/blog/2026-03-26-kubernetes-monitoring-prometheus-grafana" rel="noopener noreferrer"&gt;Prometheus + Grafana monitoring setup&lt;/a&gt; and capture the diagnosis path in your &lt;a href="https://devtocash.com/blog/incident-management-runbook-template-2026" rel="noopener noreferrer"&gt;incident runbook&lt;/a&gt; so the next on-call engineer solves it in minutes, not hours. Pair this with the &lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;ImagePullBackOff playbook&lt;/a&gt; and you've covered nearly every reason a Kubernetes pod fails to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-07-13-kubernetes-imagepullbackoff-errimagepull-fix" rel="noopener noreferrer"&gt;Kubernetes ImagePullBackOff: How to Debug and Fix It (2026)&lt;/a&gt;&lt;/strong&gt; — the other half of "my pod won't start." ImagePullBackOff means the image never pulled; CrashLoopBackOff means it pulled and the container keeps dying. Same &lt;code&gt;describe&lt;/code&gt;-first discipline, different root causes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/kubernetes-liveness-readiness-startup-probes-guide" rel="noopener noreferrer"&gt;Kubernetes Liveness, Readiness, and Startup Probes: A Practical Guide&lt;/a&gt;&lt;/strong&gt; — misconfigured liveness probes are the single most common cause of a &lt;em&gt;healthy&lt;/em&gt; app crash-looping; this explains exactly which probe to use where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devtocash.com/blog/2026-04-11-kubernetes-mistakes-that-cost-companies-millions" rel="noopener noreferrer"&gt;10 Kubernetes Mistakes That Cost Companies Millions&lt;/a&gt;&lt;/strong&gt; — hard-exiting on transient dependency failures and mis-set resource limits both show up here as expensive, avoidable crash loops.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/2026-07-14-kubernetes-crashloopbackoff-fix" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>sre</category>
      <category>devops</category>
      <category>containers</category>
    </item>
    <item>
      <title>SelfMem: One Memory Layer Across Every AI Assistant — Free &amp; Open MCP Tool</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:20:46 +0000</pubDate>
      <link>https://dev.to/devtocash/selfmem-one-memory-layer-across-every-ai-assistant-free-open-mcp-tool-4e13</link>
      <guid>https://dev.to/devtocash/selfmem-one-memory-layer-across-every-ai-assistant-free-open-mcp-tool-4e13</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/selfmem-mcp-memory-review-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SelfMem is one of the most useful MCP tools I've tested this year — and it's free, open-source, and self-hostable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SelfMem Matters for DevOps/SRE Teams
&lt;/h2&gt;

&lt;p&gt;If you run multiple AI coding assistants (Claude, Cursor, Windsurf, etc.), you've probably dealt with context fragmentation — each tool has its own memory, none of them talk to each other. SelfMem solves this with an MCP-native memory server that all your AI assistants share.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid Search&lt;/strong&gt;: PostgreSQL full-text search + pgvector semantic search — you get keyword precision AND semantic understanding in one query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 MCP Tools&lt;/strong&gt;: create, search, update, delete, tag, bookmark, and more — all exposed as standard MCP tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hostable&lt;/strong&gt;: Docker one-liner. No vendor lock-in. Your data stays on your infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier&lt;/strong&gt;: No credit card required. Generous free tier for individual developers and small teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;p&gt;I've tested Mem0, ChromaDB, and a few other memory solutions for AI agents. SelfMem wins on simplicity, search quality (hybrid FTS + vector beats pure-vector), MCP-native design, and zero recurring cost for small teams.&lt;/p&gt;

&lt;p&gt;If you're building AI-assisted DevOps workflows, this is worth 10 minutes of your time to spin up.&lt;/p&gt;

&lt;p&gt;Read the full hands-on review, setup guide, and comparison →&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally published at &lt;a href="https://devtocash.com/blog/selfmem-mcp-memory-review-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/selfmem-mcp-memory-review-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>eBPF Observability for SRE: The End of Sidecars in 2026</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:27:14 +0000</pubDate>
      <link>https://dev.to/devtocash/ebpf-observability-for-sre-the-end-of-sidecars-in-2026-3hdl</link>
      <guid>https://dev.to/devtocash/ebpf-observability-for-sre-the-end-of-sidecars-in-2026-3hdl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/ebpf-observability-sre-end-of-sidecars-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sidecars were the observability pattern of the 2020s. eBPF is the pattern of the 2026 and beyond — and the difference is dramatic.&lt;/p&gt;

&lt;p&gt;Traditional monitoring with DaemonSets and sidecars adds 10-30% resource overhead per pod. eBPF observability with tools like Cilium Hubble and Pixie runs in the kernel with less than 1% overhead. Zero code changes. Zero sidecar injection. Just attach a BPF program and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP/gRPC latency at the kernel level (no proxy hop)&lt;/li&gt;
&lt;li&gt;Network flow logs with process-level attribution&lt;/li&gt;
&lt;li&gt;CPU flame graphs by container without profiling agents&lt;/li&gt;
&lt;li&gt;File system and DNS activity per pod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is elegant: BPF programs hook into kernel tracepoints and kprobes, a user-space agent (running as a DaemonSet) collects and enriches the data, and metrics flow to your existing Prometheus/Grafana stack. No sidecars. No mTLS overhead. No Envoy configuration.&lt;/p&gt;

&lt;p&gt;The full article walks through setting up Cilium Hubble on an EKS cluster, configuring Pixie for auto-telemetry, and building eBPF-based SLO dashboards that correlate kernel events to user-facing latency.&lt;/p&gt;

&lt;p&gt;Dive into the kernel: &lt;a href="https://devtocash.com/blog/ebpf-observability-sre-end-of-sidecars-2026" rel="noopener noreferrer"&gt;https://devtocash.com/blog/ebpf-observability-sre-end-of-sidecars-2026&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/ebpf-observability-sre-end-of-sidecars-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>sre</category>
      <category>kubernetes</category>
      <category>observability</category>
    </item>
    <item>
      <title>Kubernetes LLM Inference: Deploy and Scale Open-Source LLMs in 2026</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:21:21 +0000</pubDate>
      <link>https://dev.to/devtocash/kubernetes-llm-inference-deploy-and-scale-open-source-llms-in-2026-n27</link>
      <guid>https://dev.to/devtocash/kubernetes-llm-inference-deploy-and-scale-open-source-llms-in-2026-n27</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/kubernetes-llm-inference-deploy-scale-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running your own LLMs on Kubernetes isn't just a cost play — it's about latency, data sovereignty, and fine-tuning control. But GPU scheduling at scale is a different beast entirely.&lt;/p&gt;

&lt;p&gt;Here's what a production K8s LLM inference stack looks like in 2026: vLLM or TGI for the inference server, NVIDIA GPU Operator for driver management, KEDA for request-based autoscaling, and spot instances for dev/staging environments to cut costs by 60-70%.&lt;/p&gt;

&lt;p&gt;The numbers matter: a single A100-80GB can serve Llama 3 70B with vLLM at ~30 tokens/second for 4 concurrent users. With continuous batching, that jumps to 8-10 users. But cold starts are brutal — 45-90 seconds for large models — which is why you need keep-warm pods and predictive scaling.&lt;/p&gt;

&lt;p&gt;My article covers the complete architecture: GPU node pool setup, vLLM deployment manifests, HPA vs KEDA tradeoffs, model caching strategies with PersistentVolume, and cost optimization with spot/preemptible instances.&lt;/p&gt;

&lt;p&gt;Get the full deployment guide with working YAML manifests at &lt;a href="https://devtocash.com/blog/kubernetes-llm-inference-deploy-scale-2026" rel="noopener noreferrer"&gt;https://devtocash.com/blog/kubernetes-llm-inference-deploy-scale-2026&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/kubernetes-llm-inference-deploy-scale-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>llm</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI Agents for SRE: Autonomous Incident Response in 2026</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Sat, 27 Jun 2026 20:21:20 +0000</pubDate>
      <link>https://dev.to/devtocash/ai-agents-for-sre-autonomous-incident-response-in-2026-35na</link>
      <guid>https://dev.to/devtocash/ai-agents-for-sre-autonomous-incident-response-in-2026-35na</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/ai-agents-sre-autonomous-incident-response-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When your pager goes off at 3 AM, what if an AI agent could handle the entire incident before you even wake up?&lt;/p&gt;

&lt;p&gt;That future is already here. AI agents powered by LLMs are transforming how SRE teams handle incidents — from automated diagnosis using RAG over internal runbooks, to executing remediation playbooks via PagerDuty integration, to generating blameless postmortem drafts before the war room even starts.&lt;/p&gt;

&lt;p&gt;The key architecture: a supervisor agent orchestrating specialized sub-agents for log analysis, metric correlation, and remediation. Each sub-agent has access to specific tools — kubectl, Prometheus queries, Slack for escalation, and your internal knowledge base via semantic search.&lt;/p&gt;

&lt;p&gt;But it's not plug-and-play. You need careful guardrails: human-in-the-loop for production changes, audit trails for every action, and progressive rollout (shadow mode → suggestion mode → semi-autonomous → full auto).&lt;/p&gt;

&lt;p&gt;The article breaks down the full implementation: tool definitions, RAG pipeline for runbooks, PagerDuty webhook integration, and a working Python code example you can adapt today.&lt;/p&gt;

&lt;p&gt;Read the complete guide with code at &lt;a href="https://devtocash.com/blog/ai-agents-sre-autonomous-incident-response-2026" rel="noopener noreferrer"&gt;https://devtocash.com/blog/ai-agents-sre-autonomous-incident-response-2026&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/ai-agents-sre-autonomous-incident-response-2026" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sre</category>
      <category>devops</category>
      <category>incidentresponse</category>
    </item>
    <item>
      <title>Claude Proxy: Turn Claude Code CLI into an OpenAI-Compatible API Server</title>
      <dc:creator>devtocash</dc:creator>
      <pubDate>Sat, 27 Jun 2026 17:01:23 +0000</pubDate>
      <link>https://dev.to/devtocash/claude-proxy-turn-claude-code-cli-into-an-openai-compatible-api-server-3dkd</link>
      <guid>https://dev.to/devtocash/claude-proxy-turn-claude-code-cli-into-an-openai-compatible-api-server-3dkd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Originally published on &lt;a href="https://devtocash.com/blog/claude-proxy-openai-api-server" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;&lt;/strong&gt; — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Claude Max Has No API Key
&lt;/h2&gt;

&lt;p&gt;You subscribe to Claude Max. You use Claude Code daily. But the moment you try to plug Claude into Cursor, Continue.dev, Aider, or any agent framework that speaks the OpenAI API, you hit a wall: Claude Max has no raw API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A 300-Line Zero-Dependency Proxy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rephapeng/claude-proxy" rel="noopener noreferrer"&gt;claude-proxy&lt;/a&gt; wraps your local &lt;code&gt;claude&lt;/code&gt; CLI and exposes a standard &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint. Any OpenAI-compatible client can use your authenticated Claude Code session as the backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenAI-API client --&amp;gt; /v1/chat/completions --&amp;gt; claude_openai_proxy.py --&amp;gt; claude CLI --&amp;gt; Anthropic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3 commands to run. No &lt;code&gt;pip install&lt;/code&gt;. No Docker required. Stream and non-stream. Prompt-based function/tool calling with false-refusal hardening.&lt;/p&gt;

&lt;p&gt;Supports sonnet, opus, haiku. Concurrency-controlled (200-400MB per CLI process). systemd and Docker deployment included. Evonic integration example in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Tool calling is prompt-emulated (not native API), one CLI process per request (stateless), and using Claude Max as a generic API backend may violate Anthropic's ToS. For production, a real API key is the supported path. For internal tools and agent frameworks on a budget, this is gold.&lt;/p&gt;

&lt;p&gt;Full architecture breakdown, concurrency design, env var reference, Docker compose, and comparison table -- I walk through every piece at devtocash.com. Repo: github.com/rephapeng/claude-proxy&lt;/p&gt;




&lt;p&gt;📌 &lt;strong&gt;Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability &amp;amp; cloud-cost guides — on &lt;a href="https://devtocash.com/blog/claude-proxy-openai-api-server" rel="noopener noreferrer"&gt;devtocash.com&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>api</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
