<?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: Sheryar Ahmed</title>
    <description>The latest articles on DEV Community by Sheryar Ahmed (@sheryar_ahmed).</description>
    <link>https://dev.to/sheryar_ahmed</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%2F2307239%2F55509ae9-f4f7-491a-93f4-95188b650c33.jpg</url>
      <title>DEV Community: Sheryar Ahmed</title>
      <link>https://dev.to/sheryar_ahmed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sheryar_ahmed"/>
    <language>en</language>
    <item>
      <title>Why Kubernetes Says ‘Insufficient CPU’ When the Node Looks Idle</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:29:04 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/why-kubernetes-says-insufficient-cpu-when-the-node-looks-idle-2h9</link>
      <guid>https://dev.to/sheryar_ahmed/why-kubernetes-says-insufficient-cpu-when-the-node-looks-idle-2h9</guid>
      <description>&lt;p&gt;Your deployment is stuck. The Pod has been &lt;code&gt;Pending&lt;/code&gt; for several minutes, and the event looks conclusive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0/3 nodes are available: 3 Insufficient cpu.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You open the dashboard. The nodes are using 30–40% CPU.&lt;/p&gt;

&lt;p&gt;So where did the rest of the CPU go?&lt;/p&gt;

&lt;p&gt;Nowhere. Kubernetes and your dashboard are simply answering different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your dashboard:&lt;/strong&gt; How much CPU is being used right now?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scheduler:&lt;/strong&gt; How much CPU has already been accounted for, and can this Pod's request fit on one eligible node?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That difference—&lt;strong&gt;usage versus requests&lt;/strong&gt;—is the entire mystery. But it leads to a few traps that are easy to miss in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;The Kubernetes scheduler does not place a Pod based on live CPU usage. It primarily uses the Pod's &lt;strong&gt;CPU request&lt;/strong&gt; and each node's &lt;strong&gt;allocatable CPU&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In simplified form, a Pod fits when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new Pod request
    &amp;lt;=
node allocatable CPU - CPU already requested on that node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what is missing from that calculation: current CPU usage.&lt;/p&gt;

&lt;p&gt;This is deliberate. If the scheduler packed nodes according to a quiet moment in a monitoring graph, several workloads could spike together and overwhelm the node.&lt;/p&gt;

&lt;h2&gt;
  
  
  A node can be idle and still be “full”
&lt;/h2&gt;

&lt;p&gt;Imagine a node with 8 CPU cores allocatable to Pods:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;CPU request&lt;/th&gt;
&lt;th&gt;Current usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;2 CPU&lt;/td&gt;
&lt;td&gt;0.7 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker&lt;/td&gt;
&lt;td&gt;2 CPU&lt;/td&gt;
&lt;td&gt;0.5 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;2 CPU&lt;/td&gt;
&lt;td&gt;0.6 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6 CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.8 CPU&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your monitoring system reports roughly 22.5% CPU usage. The node looks quiet.&lt;/p&gt;

&lt;p&gt;Now you deploy another Pod requesting 3 CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6 CPU already requested + 3 CPU requested = 9 CPU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node has only 8 CPU allocatable, so the Pod does not fit. The scheduler reports &lt;code&gt;Insufficient cpu&lt;/code&gt; even though actual usage is below 2 CPU.&lt;/p&gt;

&lt;p&gt;There is no contradiction. The dashboard is showing &lt;strong&gt;activity&lt;/strong&gt;; the scheduler is checking &lt;strong&gt;commitments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One nuance matters here: a CPU request is not necessarily a dedicated core locked away for that container. Think of it as an entry in the scheduler's capacity ledger. At runtime, it also influences the container's CPU weight when the node is contended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request, limit, and usage are three different numbers
&lt;/h2&gt;

&lt;p&gt;Consider this 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;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;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&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;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Kubernetes CPU units, &lt;code&gt;1000m&lt;/code&gt; is 1 CPU, so &lt;code&gt;500m&lt;/code&gt; is half a CPU.&lt;/p&gt;

&lt;p&gt;The three numbers mean:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;What it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much CPU should the scheduler account for when placing the Pod?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much CPU time may the container consume before it is throttled?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Usage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How much CPU is the container consuming at this moment?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scheduler uses the request for placement. It does not use &lt;code&gt;kubectl top&lt;/code&gt; as a live packing signal, and a higher CPU limit does not normally require that whole limit to be available before scheduling.&lt;/p&gt;

&lt;p&gt;This also explains two opposite failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requests set too high:&lt;/strong&gt; Pods are hard to schedule and the cluster looks underused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests set too low:&lt;/strong&gt; Pods schedule easily, but too many may land together and fight for CPU during a traffic spike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make requests as small as possible. The goal is to make them honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capacity is not allocatable CPU
&lt;/h2&gt;

&lt;p&gt;A 16-core machine does not always offer all 16 cores to normal Pods.&lt;/p&gt;

&lt;p&gt;Kubernetes distinguishes between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capacity:&lt;/strong&gt; the resources present on the node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocatable:&lt;/strong&gt; the portion available to Pods after reservations for the operating system, kubelet, container runtime, and eviction thresholds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check both with:&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 &amp;lt;node-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for output similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capacity:
  cpu: 16
Allocatable:
  cpu: 14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scheduling, 14 CPU is the useful number.&lt;/p&gt;

&lt;p&gt;Further down, &lt;code&gt;kubectl describe node&lt;/code&gt; includes an &lt;strong&gt;Allocated resources&lt;/strong&gt; section. Its requests column is much more relevant to this incident than the node's live utilization graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allocated resources:
  Resource  Requests       Limits
  cpu       12500m (89%)   22000m (157%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing CPU limits above 100% can be valid because CPU limits may be overcommitted. The requests column is the important scheduling signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug it in five minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Read the complete scheduling event
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not stop after reading &lt;code&gt;Insufficient cpu&lt;/code&gt;. The complete event may say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0/6 nodes are available:
2 Insufficient cpu,
2 node(s) had untolerated taint,
2 node(s) didn't match Pod's node affinity.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that case, CPU is only part of the story. Your Pod effectively has just two CPU-eligible nodes, not six.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inspect the Pod's actual request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check &lt;code&gt;resources.requests.cpu&lt;/code&gt; for &lt;strong&gt;every container&lt;/strong&gt;, including sidecars. A Pod's normal-container CPU request is generally the sum of its containers' requests.&lt;/p&gt;

&lt;p&gt;Also inspect init containers. Because init containers run sequentially, Kubernetes uses the largest relevant init-container request when calculating the Pod's effective request, rather than simply adding all init containers together. A heavyweight migration or setup container can therefore make a seemingly small application Pod difficult to schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check each eligible node's allocatable and requested CPU
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe node &amp;lt;node-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allocatable CPU
- existing CPU requests
= schedulable headroom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare that headroom with the new Pod's effective request.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Use live usage for diagnosis—not for placement math
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top nodes
kubectl top pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands help answer a different but valuable question: are requests aligned with reality?&lt;/p&gt;

&lt;p&gt;If a service requests 2 CPU per replica but stays near &lt;code&gt;100m&lt;/code&gt; even during representative peaks, its request may be oversized. Do not resize it from one quiet snapshot, though. Look at a meaningful time window, peak traffic, startup behavior, batch jobs, and latency under CPU contention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four traps that make this more confusing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Free CPU is fragmented across nodes
&lt;/h3&gt;

&lt;p&gt;Suppose three nodes each have 2 CPU of schedulable headroom. The cluster has 6 CPU free in total, but a single Pod requesting 3 CPU still cannot run.&lt;/p&gt;

&lt;p&gt;A Pod must fit on &lt;strong&gt;one&lt;/strong&gt; node. Cluster-wide free CPU cannot be pooled across nodes for one Pod.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A namespace policy may add requests for you
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;LimitRange&lt;/code&gt; can inject default requests and limits when a workload omits them. Also, when a container specifies a CPU limit but no request, Kubernetes can assign a request equal to that limit.&lt;/p&gt;

&lt;p&gt;Check for defaults with:&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 limitrange &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;namespace&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always inspect the created Pod, not only the Helm values or Deployment template you expected to be applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. DaemonSets consume space on every node
&lt;/h3&gt;

&lt;p&gt;Logging agents, security agents, CNI components, and monitoring exporters often run as DaemonSets. Their requests count too. A small per-node request becomes significant when node sizes are small or the list of agents grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. “Insufficient CPU” may appear beside other hard constraints
&lt;/h3&gt;

&lt;p&gt;Node selectors, required affinity, pod anti-affinity, taints, topology spread rules, and volume topology can shrink the set of valid nodes before CPU is considered.&lt;/p&gt;

&lt;p&gt;The cluster may have plenty of CPU—just not on a node your Pod is allowed to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you fix?
&lt;/h2&gt;

&lt;p&gt;The event tells you the symptom, not the correct remedy. Choose the fix that matches the evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requests are unrealistic:&lt;/strong&gt; Right-size them using historical usage, representative peaks, load tests, and latency or throughput goals. A Vertical Pod Autoscaler in recommendation mode can provide another data point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The request is legitimate:&lt;/strong&gt; Add or scale suitable nodes, or use larger nodes that can fit the Pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity is fragmented:&lt;/strong&gt; Rebalance workloads, reconsider placement rules, or provide a node shape large enough for the biggest Pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only a narrow node pool is eligible:&lt;/strong&gt; Revisit affinity, selectors, taints, and topology rules—or scale that specific pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rollout temporarily needs extra room:&lt;/strong&gt; Account for &lt;code&gt;maxSurge&lt;/code&gt; and other deployment behavior in your capacity plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you should not do is blindly lower the request until the Pod schedules. That can turn a visible scheduling problem into a less obvious production performance problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model to keep
&lt;/h2&gt;

&lt;p&gt;When a node looks idle but Kubernetes says &lt;code&gt;Insufficient cpu&lt;/code&gt;, remember:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usage       = what is happening now
request     = what the scheduler plans around
allocatable = what the node offers to Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is this Pod's effective CPU request?&lt;/li&gt;
&lt;li&gt;How much CPU is allocatable on each eligible node?&lt;/li&gt;
&lt;li&gt;How much CPU has already been requested there?&lt;/li&gt;
&lt;li&gt;What other rules make nodes ineligible?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you separate live usage from scheduler accounting, &lt;code&gt;Insufficient cpu&lt;/code&gt; stops looking like a Kubernetes lie. The node can be quiet and still have no room for the promise your new Pod is asking Kubernetes to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/" rel="noopener noreferrer"&gt;Resource Management for Pods and Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/" rel="noopener noreferrer"&gt;Reserve Compute Resources for System Daemons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/" rel="noopener noreferrer"&gt;Assigning Pods to Nodes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" rel="noopener noreferrer"&gt;Init Containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why a Kubernetes Pod Being 'Running' Doesn't Mean It's Healthy</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Thu, 01 Oct 2026 09:11:37 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/why-a-kubernetes-pod-being-running-doesnt-mean-its-healthy-h9h</link>
      <guid>https://dev.to/sheryar_ahmed/why-a-kubernetes-pod-being-running-doesnt-mean-its-healthy-h9h</guid>
      <description>&lt;h2&gt;
  
  
  Why a Kubernetes Pod Being &lt;code&gt;Running&lt;/code&gt; Doesn't Mean It's Healthy
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A Pod can be Running while your application is broken, unable to receive traffic, or quietly failing every job.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the easiest Kubernetes mistakes to make is also one of the most dangerous:&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 pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                          READY   STATUS    RESTARTS
worker-7f9    1/1     Running   0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The Pod is healthy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Running&lt;/code&gt; is not a health check.&lt;/p&gt;

&lt;p&gt;It's a statement about &lt;strong&gt;Pod lifecycle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And understanding that difference completely changes how you debug Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first mental model
&lt;/h2&gt;

&lt;p&gt;A Kubernetes Pod has several different questions that can be answered independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the Pod running?
        ↓
Is the container ready?
        ↓
Can it receive traffic?
        ↓
Is the application actually functioning?
        ↓
Can it successfully perform its real work?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;not the same question&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think about a normal server.&lt;/p&gt;

&lt;p&gt;A process can be alive:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;while the application is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stuck&lt;/li&gt;
&lt;li&gt;unable to connect to a dependency&lt;/li&gt;
&lt;li&gt;returning errors&lt;/li&gt;
&lt;li&gt;unable to process requests&lt;/li&gt;
&lt;li&gt;consuming jobs and failing them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes is no different.&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;code&gt;Running&lt;/code&gt; means something very specific
&lt;/h1&gt;

&lt;p&gt;Kubernetes Pod phases include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pending
Running
Succeeded
Failed
Unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a Pod is &lt;code&gt;Running&lt;/code&gt;, Kubernetes knows that the Pod has been scheduled and that its containers have started.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; automatically mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ application is healthy
✓ application can serve traffic
✓ dependencies are working
✓ requests are succeeding
✓ background jobs are succeeding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the first distinction to remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Running describes lifecycle. Healthy describes behavior.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  So how does Kubernetes know whether a container is healthy?
&lt;/h1&gt;

&lt;p&gt;This is where &lt;strong&gt;probes&lt;/strong&gt; enter the picture.&lt;/p&gt;

&lt;p&gt;Kubernetes gives us three primary probe types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup Probe
Readiness Probe
Liveness Probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They answer three different questions.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Startup Probe
&lt;/h1&gt;

&lt;h3&gt;
  
  
  "Has the application finished starting?"
&lt;/h3&gt;

&lt;p&gt;Imagine your application takes 40 seconds to initialize.&lt;/p&gt;

&lt;p&gt;During those 40 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container = alive
application = still starting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you immediately run a liveness check, Kubernetes might conclude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This application is broken."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And restart it.&lt;/p&gt;

&lt;p&gt;Then it starts again.&lt;/p&gt;

&lt;p&gt;Then the probe fails again.&lt;/p&gt;

&lt;p&gt;Then Kubernetes restarts it again.&lt;/p&gt;

&lt;p&gt;You can accidentally create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start
 ↓
probe fails
 ↓
restart
 ↓
start
 ↓
probe fails
 ↓
restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A startup probe protects slow-starting applications from that situation.&lt;/p&gt;

&lt;p&gt;Example:&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;startupProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/live&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these settings, Kubernetes gives the application roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 × 15 = 30 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to successfully start before treating startup as failed.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Readiness Probe
&lt;/h1&gt;

&lt;p&gt;This is one of the most important concepts in production Kubernetes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Readiness asks:
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Should this Pod receive work right now?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service
   ↓
Pod A
Pod B
Pod C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Pod B fails its readiness probe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod A → Ready
Pod B → NotReady
Pod C → Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes can stop sending Service traffic to Pod B.&lt;/p&gt;

&lt;p&gt;But here's the important part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A failed readiness probe does not restart the container.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's intentional.&lt;/p&gt;

&lt;p&gt;Maybe the application is temporarily unable to serve traffic but is expected to recover.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database temporarily unavailable
        ↓
readiness fails
        ↓
Pod stays alive
        ↓
Pod removed from traffic
        ↓
database recovers
        ↓
readiness succeeds
        ↓
Pod receives traffic again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's very different from liveness.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Liveness Probe
&lt;/h1&gt;

&lt;p&gt;Liveness asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Is this application so broken that restarting the container is appropriate?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, suppose your application enters a deadlock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;process = alive
HTTP server = technically running
application = permanently stuck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A liveness probe can detect that condition.&lt;/p&gt;

&lt;p&gt;If it repeatedly fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liveness fails
      ↓
Kubernetes restarts container
      ↓
application starts fresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's useful.&lt;/p&gt;

&lt;p&gt;But it can also be dangerous.&lt;/p&gt;




&lt;h1&gt;
  
  
  The dangerous liveness probe
&lt;/h1&gt;

&lt;p&gt;Imagine your liveness endpoint checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database
Redis
external API
third-party service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Redis goes down.&lt;/p&gt;

&lt;p&gt;Your application itself is perfectly capable of running, but your liveness probe says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis unavailable
      ↓
liveness fails
      ↓
restart application
      ↓
application starts
      ↓
Redis unavailable
      ↓
liveness fails
      ↓
restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've just turned a dependency outage into a &lt;strong&gt;restart storm&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's why liveness should generally answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Is this process/application locally stuck or irrecoverably broken?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;rather than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Are all of my dependencies healthy?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The three probes in one picture
&lt;/h1&gt;

&lt;p&gt;Remember 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
                     │
          ┌──────────┼──────────┐
          ↓          ↓          ↓
       STARTUP    READINESS   LIVENESS
          │          │          │
          ↓          ↓          ↓
     "Can you      "Should     "Should I
      start?"       receive      restart
                    work?"      you?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even shorter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup   → Can you start?
Readiness → Can you work?
Liveness  → Should I restart you?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the mental model.&lt;/p&gt;




&lt;h1&gt;
  
  
  But there's another trap
&lt;/h1&gt;

&lt;p&gt;Here's where real production systems become interesting.&lt;/p&gt;

&lt;p&gt;Imagine a worker that consumes jobs from a queue.&lt;/p&gt;

&lt;p&gt;Its architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue
  ↓
Worker
  ↓
Browser
  ↓
Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine the browser inside the worker becomes unhealthy.&lt;/p&gt;

&lt;p&gt;You might create a readiness check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser unhealthy
       ↓
readiness = false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds correct.&lt;/p&gt;

&lt;p&gt;But what happens to the worker?&lt;/p&gt;

&lt;p&gt;If the worker is still consuming jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue
 ↓
Worker
 ↓
Browser ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the worker may continue receiving jobs even though it cannot successfully process them.&lt;/p&gt;

&lt;p&gt;Now you've created a worse situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job arrives
   ↓
worker receives it
   ↓
browser unhealthy
   ↓
job fails
   ↓
next job
   ↓
fails
   ↓
next job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pod can be:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;while the actual business operation is failing.&lt;/p&gt;

&lt;p&gt;This is exactly why readiness needs to be designed around &lt;strong&gt;how the workload actually receives work&lt;/strong&gt;, not simply around whether a health endpoint returns &lt;code&gt;200&lt;/code&gt;. Your infrastructure notes flag this as an area requiring careful review. &lt;/p&gt;




&lt;h1&gt;
  
  
  The subtle lesson: Readiness doesn't stop everything
&lt;/h1&gt;

&lt;p&gt;This is especially important for background workers.&lt;/p&gt;

&lt;p&gt;For a normal HTTP application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Load Balancer
     ↓
Service
     ↓
Readiness
     ↓
Pod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Readiness has a very direct effect.&lt;/p&gt;

&lt;p&gt;But for a worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pub/Sub / Queue
      ↓
Worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the application may pull work directly.&lt;/p&gt;

&lt;p&gt;There may be no Kubernetes Service sitting between the queue and the worker.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;doesn't automatically mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stop consuming jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an application-level behavior.&lt;/p&gt;

&lt;p&gt;This is one of the biggest differences between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Not receiving HTTP traffic"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Not receiving background work."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Kubernetes health is layered
&lt;/h1&gt;

&lt;p&gt;A better mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1
Pod lifecycle
      ↓
Running?

Layer 2
Container health
      ↓
Startup / Liveness

Layer 3
Traffic readiness
      ↓
Ready?

Layer 4
Application health
      ↓
Can requests actually succeed?

Layer 5
Business health
      ↓
Are jobs actually completing successfully?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that last layer is often forgotten.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Pod can look perfectly healthy and still be failing
&lt;/h1&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                    READY   STATUS
worker-abc              1/1     Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looks good.&lt;/p&gt;

&lt;p&gt;But your metrics say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs_received = 10,000
jobs_completed = 0
jobs_failed = 10,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Kubernetes' perspective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod = Running
Container = alive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your business perspective:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Both can be true at the same time.&lt;/p&gt;

&lt;p&gt;That's why production observability cannot rely on:&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 pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;alone.&lt;/p&gt;




&lt;h1&gt;
  
  
  What should you actually monitor?
&lt;/h1&gt;

&lt;p&gt;For a production workload, I'd think in layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes signals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod phase
Container restarts
OOMKilled
CPU throttling
Memory usage
Pending Pods
Scheduling failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Probe signals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup failures
Readiness failures
Liveness failures
Time spent NotReady
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Application signals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request success rate
Error rate
Latency
Dependency failures
Queue consumption
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Business signals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jobs completed
Jobs failed
Jobs expired
Queue wait time
Success rate
Customer-visible failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The further down you go, the closer you get to the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Is the system actually doing what users need?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  A practical debugging flow
&lt;/h1&gt;

&lt;p&gt;When someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The Pod is unhealthy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't immediately restart it.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Is it running?
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: What are the conditions?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conditions
Events
Restart Count
Last State
Reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Which probe is failing?
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup probe
Readiness probe
Liveness probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Test the endpoint yourself
&lt;/h3&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:8080/health/live
curl localhost:8080/health/ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Check application logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &amp;lt;pod&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Check actual business behavior
&lt;/h3&gt;

&lt;p&gt;For a worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are jobs being consumed?
Are jobs succeeding?
Are they failing?
Are they expiring?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's where you stop debugging Kubernetes and start debugging the actual system.&lt;/p&gt;




&lt;h1&gt;
  
  
  A useful production rule
&lt;/h1&gt;

&lt;p&gt;Here's the rule I now keep in my head:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't design health checks around what is easy to check. Design them around what Kubernetes needs to know.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  Readiness
&lt;/h3&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can this Pod safely receive work?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Potentially dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is every dependency in the entire system healthy?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Liveness
&lt;/h3&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this process/application fundamentally stuck?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Potentially dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is every external dependency healthy?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Business monitoring
&lt;/h3&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Are jobs actually succeeding?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This belongs in metrics and alerting, not necessarily in liveness.&lt;/p&gt;




&lt;h1&gt;
  
  
  The final mental model
&lt;/h1&gt;

&lt;p&gt;Next time you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATUS: Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;don't translate it to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Everything is healthy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translate it to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The Pod is alive according to Kubernetes' lifecycle state. Now I need to determine whether it is ready, responsive, and actually doing useful work."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much safer production mindset.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10-second revision
&lt;/h2&gt;

&lt;p&gt;If you remember nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Running
  ≠
Healthy
&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;Startup   → Can you start?
Readiness → Can you receive work?
Liveness  → Should you be restarted?
Business  → Are you actually succeeding?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the most important production lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes can tell you that your process is alive. Your application metrics need to tell you whether your system is actually working.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  What I learned from this
&lt;/h3&gt;

&lt;p&gt;The interesting part of Kubernetes isn't memorizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod
Deployment
Service
Probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's understanding the &lt;strong&gt;failure semantics between them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A readiness failure doesn't restart a container.&lt;/p&gt;

&lt;p&gt;A liveness failure does.&lt;/p&gt;

&lt;p&gt;A Pod being &lt;code&gt;Running&lt;/code&gt; doesn't make it &lt;code&gt;Ready&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A Pod being &lt;code&gt;Ready&lt;/code&gt; doesn't guarantee that a business operation succeeds.&lt;/p&gt;

&lt;p&gt;And for background workers, being &lt;code&gt;NotReady&lt;/code&gt; doesn't necessarily mean the worker has stopped consuming work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's where Kubernetes knowledge starts becoming production engineering.&lt;/strong&gt;&lt;/p&gt;




</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>devops</category>
      <category>worker</category>
    </item>
    <item>
      <title>Kubernetes Isn't a Container Manager, It's a Reconciliation System</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:07:22 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/kubernetes-isnt-a-container-manager-its-a-reconciliation-system-1kkf</link>
      <guid>https://dev.to/sheryar_ahmed/kubernetes-isnt-a-container-manager-its-a-reconciliation-system-1kkf</guid>
      <description>&lt;h4&gt;
  
  
  Subtitle
&lt;/h4&gt;

&lt;p&gt;A first-principles mental model for understanding Kubernetes controllers, desired state, actual state, and why this changes the way you debug production systems.&lt;/p&gt;




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

&lt;p&gt;When I first learned Kubernetes, I mostly thought about it in terms of containers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pods run containers.&lt;br&gt;
Deployments create Pods.&lt;br&gt;
Services expose Pods.&lt;br&gt;
Scheduler puts Pods on nodes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those statements are true.&lt;/p&gt;

&lt;p&gt;But they don't really explain &lt;strong&gt;what Kubernetes is doing underneath&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mental model that made Kubernetes much easier for me was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes is a reconciliation system.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At its core, you declare the state you want, and Kubernetes continuously works to make the actual system match that desired state.&lt;/p&gt;

&lt;p&gt;Once this clicks, many Kubernetes concepts start fitting together naturally.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Desired State vs Actual State
&lt;/h1&gt;

&lt;p&gt;Imagine I create a Deployment with:&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not really saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Kubernetes, immediately start five containers.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm declaring:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“I want five replicas of this workload to exist.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the &lt;strong&gt;desired state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cluster has some &lt;strong&gt;actual state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Maybe currently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired:
5 Pods

Actual:
5 Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything matches.&lt;/p&gt;

&lt;p&gt;But now imagine one Pod crashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired:
5 Pods

Actual:
4 Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Something changed.&lt;/p&gt;

&lt;p&gt;Kubernetes doesn't need me to manually say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hey Kubernetes, one of my Pods died. Please create another one.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system observes the difference and works toward the desired state again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired: 5
Actual: 4
       ↓
Controller notices difference
       ↓
New Pod is created
       ↓
Scheduler finds a suitable node
       ↓
Kubelet starts the workload
       ↓
Actual state approaches 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is reconciliation.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. The Core Loop
&lt;/h1&gt;

&lt;p&gt;A useful simplified mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ┌─────────────────┐
        │  Desired State  │
        │   replicas: 5   │
        └────────┬────────┘
                 ↓
          Kubernetes API
                 ↓
        ┌─────────────────┐
        │    Controller   │
        └────────┬────────┘
                 ↓
          Take an action
                 ↓
        ┌─────────────────┐
        │   Actual State  │
        │    4 Pods       │
        └────────┬────────┘
                 ↓
              Observe
                 │
                 └──────────────→ Reconcile again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important word here is &lt;strong&gt;continuously&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Kubernetes isn't executing a one-time script.&lt;/p&gt;

&lt;p&gt;It's continuously observing the system and responding to differences between what should exist and what actually exists.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Where does the Desired State live?
&lt;/h1&gt;

&lt;p&gt;Kubernetes objects describe desired state.&lt;/p&gt;

&lt;p&gt;For example:&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;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;my-app&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;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The YAML itself isn't Kubernetes.&lt;/p&gt;

&lt;p&gt;It's simply a representation of an object that we submit to the Kubernetes API.&lt;/p&gt;

&lt;p&gt;The API Server becomes the front door through which Kubernetes components interact with the cluster state.&lt;/p&gt;

&lt;p&gt;A simplified flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl / GitOps / API client
          ↓
     API Server
          ↓
     Cluster state
          ↓
      Controllers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing is that Kubernetes components don't all independently maintain some secret copy of what the cluster should look like.&lt;/p&gt;

&lt;p&gt;They interact through the Kubernetes API and its stored state.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Controllers Are the Reconciliation Engine
&lt;/h1&gt;

&lt;p&gt;This is where the idea becomes really powerful.&lt;/p&gt;

&lt;p&gt;Different controllers are responsible for different kinds of resources.&lt;/p&gt;

&lt;p&gt;For a Deployment, a simplified chain 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;Deployment
     ↓
ReplicaSet
     ↓
Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose the Deployment says:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The Deployment controller works toward that desired state.&lt;/p&gt;

&lt;p&gt;The ReplicaSet is responsible for maintaining the desired number of Pods associated with it.&lt;/p&gt;

&lt;p&gt;So if one Pod disappears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired:
5 Pods

Actual:
4 Pods

        ↓

ReplicaSet reconciles

        ↓

Create replacement Pod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why the controller model is more important than memorizing individual Kubernetes commands.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Kubernetes Doesn't Guarantee That Desired State Is Possible
&lt;/h1&gt;

&lt;p&gt;This is an important production lesson.&lt;/p&gt;

&lt;p&gt;Declaring:&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't mean Kubernetes has magically created the capacity for 100 Pods.&lt;/p&gt;

&lt;p&gt;You are declaring:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I want 100.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes then tries to make that happen.&lt;/p&gt;

&lt;p&gt;But the cluster might not have enough resources.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired replicas: 100

Running:          70
Pending:           30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The desired state is still 100.&lt;/p&gt;

&lt;p&gt;But the actual state is only 70 running Pods.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Maybe the scheduler can't find nodes with enough requested CPU or memory.&lt;/p&gt;

&lt;p&gt;Maybe node constraints don't match.&lt;/p&gt;

&lt;p&gt;Maybe the cluster needs to scale.&lt;/p&gt;

&lt;p&gt;Maybe some other scheduling constraint prevents placement.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes can maintain a desired state only when the underlying system has the ability to satisfy it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is one reason simply looking at:&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 pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;isn't enough when debugging production.&lt;/p&gt;

&lt;p&gt;You need to understand &lt;strong&gt;why actual state differs from desired state&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Running Doesn't Mean Healthy
&lt;/h1&gt;

&lt;p&gt;The reconciliation model also helps explain something that initially confused me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod phase = Running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't necessarily mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“My application is healthy.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Pod can be running while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application is returning errors&lt;/li&gt;
&lt;li&gt;a dependency is unavailable&lt;/li&gt;
&lt;li&gt;the application isn't ready to receive traffic&lt;/li&gt;
&lt;li&gt;the process is alive but stuck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's where concepts such as &lt;strong&gt;readiness and liveness&lt;/strong&gt; become important.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pod exists
   ↓
Container running
   ↓
Readiness probe
   ↓
Can this workload receive traffic?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes may successfully maintain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired: 5 Pods
Actual: 5 Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the application itself is still unhealthy.&lt;/p&gt;

&lt;p&gt;This distinction is critical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes can reconcile infrastructure state without understanding your application's business correctness.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  7. The Same Idea Appears Everywhere
&lt;/h1&gt;

&lt;p&gt;Once you start seeing Kubernetes through the reconciliation lens, many features become easier to understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired replicas
       ↓
Controller
       ↓
Actual Pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Horizontal Pod Autoscaler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired workload capacity
       ↓
HPA
       ↓
Deployment replicas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cluster Autoscaler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired node capacity
       ↓
Autoscaler
       ↓
Actual nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  KEDA
&lt;/h3&gt;

&lt;p&gt;A workload metric changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue grows
   ↓
KEDA
   ↓
HPA
   ↓
More replicas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details differ, but the broader pattern remains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observe → Compare → Act → Observe again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the mental model.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. A Better Way to Debug Kubernetes
&lt;/h1&gt;

&lt;p&gt;This is probably the most useful part of the mental model for me.&lt;/p&gt;

&lt;p&gt;When something is wrong, instead of immediately running random &lt;code&gt;kubectl&lt;/code&gt; commands, start with:&lt;/p&gt;

&lt;h3&gt;
  
  
  Question 1 — What is the desired state?
&lt;/h3&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deployment wants:
100 replicas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Question 2 — What is the actual state?
&lt;/h3&gt;

&lt;p&gt;Maybe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;70 Running
30 Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we've established the mismatch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Question 3 — Who is responsible for closing the gap?
&lt;/h3&gt;

&lt;p&gt;Is it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Deployment controller?&lt;/li&gt;
&lt;li&gt;a scheduler?&lt;/li&gt;
&lt;li&gt;HPA?&lt;/li&gt;
&lt;li&gt;KEDA?&lt;/li&gt;
&lt;li&gt;Cluster Autoscaler?&lt;/li&gt;
&lt;li&gt;the application itself?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Question 4 — Why can't the next reconciliation step succeed?
&lt;/h3&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 desired
70 running
30 pending

        ↓

Scheduler says:
Insufficient CPU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we've moved from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Kubernetes isn't working.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The desired state is 100 replicas, but 30 Pods cannot be scheduled because available node capacity doesn't satisfy their resource requests.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a completely different debugging conversation.&lt;/p&gt;




&lt;h1&gt;
  
  
  9. The Mental Model I Keep
&lt;/h1&gt;

&lt;p&gt;I now think about Kubernetes 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;             DESIRED STATE
                  │
                  ↓
            Kubernetes API
                  │
                  ↓
             CONTROLLERS
                  │
                  ↓
               ACTION
                  │
                  ↓
             ACTUAL STATE
                  │
                  ↓
               OBSERVE
                  │
                  └──────────→ RECONCILE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when something breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What do we want?

        ↓

What do we actually have?

        ↓

Where is the difference?

        ↓

Which controller/component owns that difference?

        ↓

Why can't it reconcile it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much more useful Kubernetes mental model than memorizing commands.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Kubernetes is obviously a platform for running workloads and containers.&lt;/p&gt;

&lt;p&gt;But I think that description hides the more important idea.&lt;/p&gt;

&lt;p&gt;The deeper abstraction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes continuously reconciles the actual state of a distributed system toward a declared desired state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you understand that, Deployments, ReplicaSets, scheduling, autoscaling, self-healing, and many debugging workflows become easier to reason about.&lt;/p&gt;

&lt;p&gt;And perhaps the biggest lesson is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When Kubernetes isn't doing what you expect, don't just ask “what command should I run?” Ask “what state does Kubernetes want, what state does it have, and why can't it reconcile the difference?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>keda</category>
      <category>automation</category>
    </item>
    <item>
      <title>When something breaks across 20 microservices, one question decides how fast you recover</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:15:15 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/when-something-breaks-across-20-microservices-one-question-decides-how-fast-you-recover-36bc</link>
      <guid>https://dev.to/sheryar_ahmed/when-something-breaks-across-20-microservices-one-question-decides-how-fast-you-recover-36bc</guid>
      <description>&lt;h1&gt;
  
  
  It's 2 A.M. and Your Dashboard Just Says "Something Is Wrong"
&lt;/h1&gt;

&lt;p&gt;A field guide to observability — logs, metrics, and traces — for everyone who has ever debugged twenty microservices with grep and prayer.&lt;/p&gt;




&lt;p&gt;It's 2:14 a.m. Your phone is doing the thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjtf59k7mkulgts2opa2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjtf59k7mkulgts2opa2.jpg" alt=" " width="736" height="414"&gt;&lt;/a&gt;&lt;br&gt;
                 Caption: "on-call, living the dream."&lt;/p&gt;

&lt;p&gt;Checkout is failing. Not for everyone, not all the time, but enough that support is drowning and someone has already tweeted a screenshot with the words "is this app down or is it just me." You open your laptop, one eye still asleep, and pull up the dashboard.&lt;/p&gt;

&lt;p&gt;A big red number is climbing. Error rate: up. Something is wrong.&lt;/p&gt;

&lt;p&gt;And that is &lt;em&gt;all it tells you.&lt;/em&gt; Something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41z4ou86nbmj0qxrdu4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41z4ou86nbmj0qxrdu4.jpeg" alt=" " width="736" height="558"&gt;&lt;/a&gt;&lt;br&gt;
Caption: "Dashboard: 'Something is wrong.' Me: great, love that for us, very specific."*&lt;/p&gt;

&lt;p&gt;Here's the trap you're standing in. Your system isn't one program anymore. It's twenty services, spread across a cluster, written by different people in different languages, calling each other in a chain you couldn't draw from memory if your life depended on it. One "place order" click quietly fans out into a dozen network hops. One of them is failing. Maybe two. Your dashboard knows the house is on fire. It has absolutely no idea which room.&lt;/p&gt;

&lt;p&gt;So you do what everyone does at 2 a.m. You start SSH-ing into boxes. You grep logs. You guess. "Probably the payment service." You read a thousand lines and find nothing. "The database?" Fine. The clock ticks, users keep failing to check out, and somewhere in that chain of twenty services the actual problem is sitting there, arms crossed, invisible, waiting for you to trip over it.&lt;/p&gt;

&lt;p&gt;This — this exact moment — is what observability is &lt;em&gt;for.&lt;/em&gt; Not to look impressive on a screen. To answer one question, fast: &lt;strong&gt;where is the problem, and why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let me actually teach you how it works, start to finish, so that nights like this stop being three-hour horror movies and start being ninety-second fixes. No prior knowledge needed. Just bring the caffeine.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, a distinction that everything else hangs on
&lt;/h2&gt;

&lt;p&gt;People throw around "monitoring" and "observability" like they mean the same thing. They don't, and the gap between them is the whole story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring is the smoke alarm.&lt;/strong&gt; You decided &lt;em&gt;in advance&lt;/em&gt; what to watch — error rate, CPU, latency — and built a dashboard for it. It's brilliant at one job: screaming when a known thing goes wrong. "There is smoke." But a smoke alarm cannot tell you that the wiring in the upstairs bathroom shorted out. It only knows: smoke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability is being able to walk through the burning house and ask questions you never planned for.&lt;/strong&gt; "Why are only Android users, only in one region, only after 6 p.m., only when paying by card, getting slow checkouts?" Nobody builds a dashboard that specific ahead of time. But if your system is constantly describing itself — emitting enough signal — you can start at the alarm and follow the smoke all the way to the one shorted wire, &lt;em&gt;after&lt;/em&gt; the fire starts, without shipping new code to investigate.&lt;/p&gt;

&lt;p&gt;Monitoring tells you the house is on fire. Observability tells you which room. You need both, but at 2 a.m., the alarm was never the hard part. Finding the room is.&lt;/p&gt;

&lt;p&gt;And to find the room, you need three different kinds of signal. Each answers a different question, and the entire skill is knowing which one to grab.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three signals (a.k.a. your detective kit)
&lt;/h2&gt;

&lt;p&gt;Treat your broken checkout like a crime scene. You've got three tools, and they are not interchangeable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Metrics — the vital-signs monitor
&lt;/h3&gt;

&lt;p&gt;Metrics are numbers over time: requests per second, error rate, latency. Cheap, always running, glanceable. Metrics are what spiked and woke you up. Their job is to answer &lt;em&gt;is something wrong, and how bad?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What metrics &lt;em&gt;cannot&lt;/em&gt; do is tell you &lt;em&gt;which&lt;/em&gt; patient or &lt;em&gt;why.&lt;/em&gt; A metric is an aggregate — the heart-rate line jumping — and it will never name the one request that died, because to stay cheap it blended that request into a million others. That's not a bug. That's the deal: you traded per-request detail for a signal that runs 24/7 and costs almost nothing.&lt;/p&gt;

&lt;p&gt;If you only ever watch a handful of metrics, watch these four — the industry calls them the &lt;strong&gt;Four Golden Signals&lt;/strong&gt;: &lt;strong&gt;latency&lt;/strong&gt; (how slow), &lt;strong&gt;traffic&lt;/strong&gt; (how much demand), &lt;strong&gt;errors&lt;/strong&gt; (how many are failing), and &lt;strong&gt;saturation&lt;/strong&gt; (how full your system is). Those four are the vital signs of basically any service. Get them on a dashboard and you have honest, always-on vitals.&lt;/p&gt;
&lt;h3&gt;
  
  
  Traces — the detective tailing the suspect through the building
&lt;/h3&gt;

&lt;p&gt;A trace is the story of &lt;em&gt;one single request&lt;/em&gt; as it travels across all your services, broken into timed steps called &lt;strong&gt;spans&lt;/strong&gt;. The gateway received it (span one). The gateway called orders (span two). Orders called payments (span three). Each span has a start, a duration, and a parent. Lay them out and you get a &lt;strong&gt;waterfall&lt;/strong&gt; — a visual timeline of exactly where that one request spent its time, and exactly where it fell over.&lt;/p&gt;

&lt;p&gt;When your metrics scream "checkout is slow," the trace is what walks you down the hallway, past the five services that were perfectly fine, and stops you at the door of the one that wasn't. Traces answer the question metrics can't: &lt;em&gt;where?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Logs — the security-camera footage
&lt;/h3&gt;

&lt;p&gt;When the trace stops you at the payment service's door, the &lt;strong&gt;log&lt;/strong&gt; tells you what actually happened inside the room: "payment processor declined: connection timed out after 5000ms." Logs are the detailed, event-level story of one specific thing. Richest signal you have, and the most expensive to keep — which is why they're only useful if they're written like &lt;em&gt;data&lt;/em&gt;, not diary entries.&lt;/p&gt;

&lt;p&gt;Here's the difference, because it matters. This is a log written like a diary entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-08-02 02:14  payment failed for bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useless. To find every failed payment you're back to grep and regex. Now here's the same event written like data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"level":"error","service":"payments","trace_id":"a1b2c3…","msg":"processor declined","reason":"timeout"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That you can &lt;em&gt;query&lt;/em&gt;: "give me every error from the payments service in the last hour." The rule for logs, then, is short: log JSON, one event per line, and put a trace ID in every single one. (Hold onto that trace ID. It's about to become the hero of the story.)&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;metrics&lt;/strong&gt; say &lt;em&gt;is it broken.&lt;/em&gt; &lt;strong&gt;Traces&lt;/strong&gt; say &lt;em&gt;where.&lt;/em&gt; &lt;strong&gt;Logs&lt;/strong&gt; say &lt;em&gt;what.&lt;/em&gt; Three tools, three questions, one investigation.&lt;/p&gt;

&lt;p&gt;Now for the part almost every tutorial skips — the three ideas that separate people who merely &lt;em&gt;own&lt;/em&gt; dashboards from people who can actually &lt;em&gt;use&lt;/em&gt; them. They're all a little counterintuitive, and once they click, you will never look at a system the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Truth #1: Averages are filthy liars
&lt;/h2&gt;

&lt;p&gt;Say your checkout handles a hundred requests. Ninety-nine come back in 100 milliseconds. One takes ten full seconds.&lt;/p&gt;

&lt;p&gt;The average? About 200 milliseconds. You glance at it and think: healthy. Ship it.&lt;/p&gt;

&lt;p&gt;But one in every hundred customers just waited &lt;em&gt;ten seconds&lt;/em&gt; for a page. At scale that's thousands of furious people a day, and your dashboard smiled the entire time. The average didn't lie by being wrong — it lied by being an &lt;em&gt;average.&lt;/em&gt; It smeared one catastrophe across ninety-nine good experiences until it vanished.&lt;/p&gt;

&lt;p&gt;There's an old line: a statistician drowned crossing a river that was, on average, three feet deep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kwxxxj9nkz3k7nl5wop.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3kwxxxj9nkz3k7nl5wop.jpeg" alt=" " width="736" height="596"&gt;&lt;/a&gt;&lt;br&gt;
Caption: "the average response time is fine, sir."&lt;/p&gt;

&lt;p&gt;This is why people who run big systems almost never look at averages. They look at &lt;strong&gt;percentiles.&lt;/strong&gt; The &lt;strong&gt;p50&lt;/strong&gt; (median) is your typical experience. The &lt;strong&gt;p95&lt;/strong&gt; is what &lt;em&gt;most&lt;/em&gt; users feel — only one in twenty is slower. The &lt;strong&gt;p99&lt;/strong&gt; is the painful tail, the one-in-a-hundred where the angry tweets are born. When an engineer says "our p99 blew up," they mean "the average looks fine, but our worst-served users are suffering, and I can &lt;em&gt;see&lt;/em&gt; it." That visibility is the whole difference between a system you &lt;em&gt;think&lt;/em&gt; is healthy and one you &lt;em&gt;know&lt;/em&gt; is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Truth #2: The thing that makes metrics cheap can also blow them to pieces
&lt;/h2&gt;

&lt;p&gt;Metrics are cheap because they're aggregated. But there's a landmine buried in that word, and it takes down real systems.&lt;/p&gt;

&lt;p&gt;Every metric can be sliced by &lt;strong&gt;labels&lt;/strong&gt;: error rate &lt;em&gt;by service&lt;/em&gt;, &lt;em&gt;by status code&lt;/em&gt;, &lt;em&gt;by region&lt;/em&gt;. Each unique combination of labels is a separate line the database must store and hold in memory. Slice by service and status? Maybe sixty lines. Fine. But the second someone adds a label like &lt;code&gt;user_id&lt;/code&gt;, and you have a million users, you've just asked your metrics database to track sixty &lt;em&gt;million&lt;/em&gt; lines. It falls over and dies. This has a name — &lt;strong&gt;cardinality explosion&lt;/strong&gt; — and it's one of the most popular ways teams accidentally nuke their own monitoring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdi05b4xrnwu1lpyx2f8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdi05b4xrnwu1lpyx2f8.jpeg" alt=" " width="320" height="240"&gt;&lt;/a&gt;&lt;br&gt;
Caption: "me, after adding user_id as a metric label."*&lt;/p&gt;

&lt;p&gt;The lesson is a rule that keeps you alive: labels are for things with a small, bounded set of values — service, status code, region. They are &lt;em&gt;never&lt;/em&gt; for unbounded things — user IDs, emails, request IDs. And this is exactly where the three signals snap together into a system: when you want per-user, per-request detail, you don't cram it into a metric. That's what logs and traces are &lt;em&gt;for.&lt;/em&gt; Every signal has a job. The whole art is not forcing one to do another's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Truth #3: The real magic is the thread that ties all three together
&lt;/h2&gt;

&lt;p&gt;Here's the idea that turns this from "three separate tools" into a superpower.&lt;/p&gt;

&lt;p&gt;Remember that trace ID I told you to keep in every log line? It also rides along on every metric and every span. One string, following one request across every service it touches. And when all three signals carry the same one, something genuinely magical happens.&lt;/p&gt;

&lt;p&gt;You're staring at a metric spike. You click it — and you're looking at an example &lt;em&gt;trace&lt;/em&gt; from that exact moment. You see the payment span glowing red. You click &lt;em&gt;that&lt;/em&gt; — and you're reading the exact &lt;em&gt;log&lt;/em&gt; lines that request produced: "processor timeout." You never opened a terminal. You never guessed. You followed one thread from "something's wrong" all the way to "here's the precise line and the precise reason," across a system written in three languages, in about ninety seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai0d750ht4vhdvslu6zn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai0d750ht4vhdvslu6zn.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
Caption: "…all carrying the same trace_id."*&lt;/p&gt;

&lt;p&gt;For that thread to survive, one thing has to work. When service A calls service B, it has to pass the trace ID along, tucked inside the request. There's an open standard for this — a header literally called &lt;code&gt;traceparent&lt;/code&gt; — and every service in the chain reads it and &lt;em&gt;continues the same story&lt;/em&gt; instead of starting a new one. Break that hand-off anywhere (an ancient library, a queue that drops the header) and your one beautiful trace shatters into disconnected fragments and the magic dies.&lt;/p&gt;

&lt;p&gt;The good news is you don't wire this by hand per vendor. &lt;strong&gt;OpenTelemetry&lt;/strong&gt; is the open, vendor-neutral standard for producing all three signals and passing that thread along. You instrument your code &lt;em&gt;once&lt;/em&gt; against it, and you can send the data to any backend you like without rewriting anything. Instrument once, send anywhere, never get locked in. So the entire discipline, in one breath: instrument everything with OpenTelemetry, and never drop the thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Truth #4 (bonus): Alert on what your &lt;em&gt;users&lt;/em&gt; feel, not what your &lt;em&gt;servers&lt;/em&gt; feel
&lt;/h2&gt;

&lt;p&gt;This one saves your sleep, so it earns a spot.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; alert on a thousand things. Most of them will wake you at 2 a.m. for absolutely nothing. The rule that rescues your sanity: alert on &lt;strong&gt;symptoms your users feel&lt;/strong&gt;, not on internal &lt;strong&gt;causes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;High error rate is a symptom — someone can't check out right now. Wake me. Slow p95 is a symptom — the app feels broken. Wake me. But CPU at 90%? That is &lt;em&gt;not&lt;/em&gt; a symptom. A server pinned at 90% while happily serving fast, successful responses is a server doing its literal job. Page a human for that and you've taught your team that alerts are noise — so the day a real one fires, everyone scrolls past it. CPU and memory are for &lt;em&gt;diagnosis,&lt;/em&gt; something you check &lt;em&gt;after&lt;/em&gt; a symptom already pulled you in. They should never be the thing that pulls you in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfga0qtkvqfscgyp1mpc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfga0qtkvqfscgyp1mpc.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And give every alert a little patience: "error rate above 5% &lt;em&gt;for two minutes&lt;/em&gt;," not "for one second." That short wait absorbs the meaningless blips so the only things that ever wake you are real. An alert that isn't worth acting on isn't an alert. It's a notification you'll learn to hate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like when it's all actually running
&lt;/h2&gt;

&lt;p&gt;Concepts are slippery until you watch them move, so let me paint the finished picture.&lt;/p&gt;

&lt;p&gt;Imagine a small but &lt;em&gt;real&lt;/em&gt; distributed system — three services, and on purpose written in two languages: a gateway in Node.js, an orders service in Python, and a payments service back in Node.js. A single request to the gateway fans out through all three, exactly like your checkout. Around them sits the modern stack: Prometheus collecting metrics, Loki holding logs, Tempo storing traces, an OpenTelemetry Collector routing all of it, and Grafana on top as one single pane of glass.&lt;/p&gt;

&lt;p&gt;Send one request, and you watch it become all three signals at once. It shows up as a spike on the metrics dashboard. It shows up as structured log lines. And it shows up as one distributed trace whose waterfall runs from Node, into Python, and back into Node — living proof that the thread survived the trip across languages. Click the trace, jump to its logs. Click a log line, jump back to its trace. That round trip, in one tool, is the entire idea made physical. Watching a single request stitch three languages into one story is the moment observability stops being a buzzword and becomes obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now rewind to 2 a.m.
&lt;/h2&gt;

&lt;p&gt;Same night. Same fire. But this time your system can explain itself.&lt;/p&gt;

&lt;p&gt;Your phone buzzes. Error rate climbing. You open the dashboard — but instead of shrugging at "something is wrong," you click into an example failing trace from that exact minute and watch the waterfall light up the payment span in angry red. You click it. The logs are right there: "payment processor declined: connection timed out." You check the processor's status page — they're having an incident. You flip a feature flag to fail over to your backup provider, checkout recovers, and you are back in bed before the coffee would have finished brewing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjjgrzaezljpnyh8bzt7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgjjgrzaezljpnyh8bzt7.jpeg" alt=" " width="500" height="700"&gt;&lt;/a&gt;&lt;br&gt;
Caption: "found it in 90 seconds. good night."&lt;/p&gt;

&lt;p&gt;Same fire. Same twenty services. The difference isn't that you got smarter at 2 a.m. It's that your system could finally tell you where it hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start, if you're starting from zero
&lt;/h2&gt;

&lt;p&gt;Don't try to boil the ocean. Do these three, in this order:&lt;/p&gt;

&lt;p&gt;First, put the &lt;strong&gt;Four Golden Signals&lt;/strong&gt; — latency, traffic, errors, saturation — on one dashboard, so you have honest vital signs. Second, make your logs &lt;strong&gt;structured&lt;/strong&gt;: JSON, one event per line, a trace ID in every one, so they're queryable instead of grep-bait. Third, add &lt;strong&gt;tracing with OpenTelemetry&lt;/strong&gt;, so you can follow one request across every service and jump between all three signals.&lt;/p&gt;

&lt;p&gt;Do that, and the next time something breaks across your services, you won't be the person SSH-ing into boxes and guessing at 2 a.m. You'll be the person who clicks three times, points at the exact failing service, says "found it," and goes back to sleep while everyone else is still reaching for the terminal.&lt;/p&gt;

&lt;p&gt;That, in the end, is all observability really is: giving your system a voice, so that when it's hurting, it can finally just &lt;em&gt;tell you where.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you'd like to see all of this running — the three services, the cross-language trace, every signal lighting up in Grafana — I built the whole thing as a hands-on demo and walk through it live. Even better, it runs with one command, so you can break it on purpose and watch it tell on itself. Link's in the comments.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>devops</category>
      <category>automation</category>
      <category>cloud</category>
    </item>
    <item>
      <title>"an AI-interview experience with zero AI and zero cost." Opens with the three bills you didn't pay (TTS, Whisper, proctoring vendor)</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Tue, 07 Jul 2026 20:57:07 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/an-ai-interview-experience-with-zero-ai-and-zero-cost-opens-with-the-three-bills-you-didnt-pay-1jmm</link>
      <guid>https://dev.to/sheryar_ahmed/an-ai-interview-experience-with-zero-ai-and-zero-cost-opens-with-the-three-bills-you-didnt-pay-1jmm</guid>
      <description>&lt;h1&gt;
  
  
  A mock interviewer that speaks, listens, and proctors with no AI bill
&lt;/h1&gt;

&lt;p&gt;Demo: &lt;a href="https://drive.google.com/file/d/13WEiJXqwq7O6XzouhUR0LpNfgz67znd9/view?usp=sharing" rel="noopener noreferrer"&gt;https://drive.google.com/file/d/13WEiJXqwq7O6XzouhUR0LpNfgz67znd9/view?usp=sharing&lt;/a&gt;&lt;br&gt;
Mentors on my platform wanted to run practice interviews with their mentees: a voice&lt;br&gt;
asks a question, the candidate answers out loud, it gets recorded, and — because it's&lt;br&gt;
practice for the real thing the session is proctored. Every off-the-shelf version of&lt;br&gt;
this hands you three metered bills: a &lt;strong&gt;TTS&lt;/strong&gt; service to read the question, an &lt;strong&gt;STT&lt;/strong&gt;&lt;br&gt;
service (usually Whisper) to transcribe the answer, and a proctoring vendor to watch the&lt;br&gt;
room. Three subscriptions for a feature people use in short bursts.&lt;/p&gt;

&lt;p&gt;The naive plan is to just pay them. Wire up a cloud TTS voice, POST each audio answer to&lt;br&gt;
a speech-to-text endpoint, embed a proctoring SDK, and eat the per-minute cost forever.&lt;br&gt;
It works, but every one of those is a recurring charge, a data-egress path, and a privacy&lt;br&gt;
footprint — for something that's fundamentally &lt;em&gt;the candidate's own browser talking to&lt;br&gt;
the candidate's own microphone&lt;/em&gt;. I didn't want a network round-trip sitting between a&lt;br&gt;
mentee and a practice question.&lt;/p&gt;
&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;I asked how much of the stack the browser already ships, and the answer was: nearly all&lt;br&gt;
of it. The whole "AI interviewer" experience is three native Web APIs in a trench coat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTS — the interviewer speaks.&lt;/strong&gt; &lt;code&gt;window.speechSynthesis&lt;/code&gt; renders the question on-device.&lt;br&gt;
The mentor even tunes the pitch/rate so the interviewer ("Aria") has a personality. No&lt;br&gt;
audio is generated on a server; nothing is downloaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STT — the answer transcribes itself.&lt;/strong&gt; &lt;code&gt;SpeechRecognition&lt;/code&gt; (&lt;code&gt;webkitSpeechRecognition&lt;/code&gt;&lt;br&gt;
in Chromium) does live, &lt;strong&gt;streaming&lt;/strong&gt; transcription on-device — &lt;code&gt;interimResults&lt;/code&gt; means&lt;br&gt;
words land on screen as they're spoken, the same engine behind your phone keyboard's mic&lt;br&gt;
button. No upload, no Whisper invoice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio streaming — the recording is the ground truth.&lt;/strong&gt; &lt;code&gt;MediaRecorder&lt;/code&gt; captures the mic&lt;br&gt;
to a chunked &lt;code&gt;audio/webm&lt;/code&gt; blob. That blob is the &lt;em&gt;only&lt;/em&gt; byte that touches my server, and&lt;br&gt;
it goes to storage I already pay for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TTS: the interviewer reads the prompt (mentor-tuned voice)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SpeechSynthesisUtterance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interviewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pitch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;speechSynthesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// never overlap two questions&lt;/span&gt;
&lt;span class="nx"&gt;speechSynthesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// STT: live, streaming transcript — no server in the loop&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SpeechRecognition&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitSpeechRecognition&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;
&lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;continuous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interimResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onresult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTranscript&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Audio streaming: MediaRecorder is the ground truth if STT is unsupported&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio/webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;mr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ondataavailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key judgment call is &lt;em&gt;layering&lt;/em&gt;, not just "use the free thing." STT support is uneven&lt;br&gt;
(Firefox notably lags), so transcription is never the source of truth — the &lt;strong&gt;recording&lt;/strong&gt;&lt;br&gt;
is. STT degrades to a nicety; the feature never breaks. And a green-room mic check runs a&lt;br&gt;
lightweight &lt;strong&gt;VAD&lt;/strong&gt;-style level meter off the Web Audio &lt;code&gt;AnalyserNode&lt;/code&gt; (RMS on the time-&lt;br&gt;
domain data) so a candidate can &lt;em&gt;see&lt;/em&gt; the mic is live before the clock starts — again,&lt;br&gt;
zero network, pure &lt;code&gt;getByteTimeDomainData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tally: TTS $0, STT $0, proctoring $0. The only recurring cost is the audio blob storage I&lt;br&gt;
already had.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotcha
&lt;/h2&gt;

&lt;p&gt;Proctoring is where "free browser signals" nearly bit me. The harness itself is cheap and&lt;br&gt;
honest — fullscreen lock, tab/blur focus-loss counting, copy/paste/right-click blocking,&lt;br&gt;
periodic webcam snapshots, all buffered client-side and flushed in batches. The trap was a&lt;br&gt;
kit option I was quietly proud of: &lt;strong&gt;"require camera."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I shipped it, then during testing found the guarantee was hollow. A candidate could allow&lt;br&gt;
the camera at the start, then &lt;strong&gt;turn it off mid-interview&lt;/strong&gt; from the browser's site&lt;br&gt;
controls. The session sailed on — and worse, my snapshot loop kept firing, dutifully&lt;br&gt;
uploading &lt;strong&gt;black frames&lt;/strong&gt;. On the surface everything looked compliant. "Require camera"&lt;br&gt;
that you can switch off after ten seconds isn't a requirement; it's decoration.&lt;/p&gt;

&lt;p&gt;The fix is one signal: a &lt;code&gt;MediaStreamTrack&lt;/code&gt;'s health. When a camera is turned off, covered,&lt;br&gt;
or unplugged, its track goes &lt;code&gt;muted&lt;/code&gt;/&lt;code&gt;ended&lt;/code&gt; and &lt;code&gt;readyState&lt;/code&gt; stops being &lt;code&gt;'live'&lt;/code&gt;. Poll&lt;br&gt;
that, and two behaviors fall out of the same check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MediaStream&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;getVideoTracks&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// (1) block instead of silently continuing&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;setCameraLive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;camera_off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// → full-screen "turn it back on" overlay&lt;/span&gt;

&lt;span class="c1"&gt;// (2) never upload a black frame from a dead camera&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;live&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;muted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// skip the snapshot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the camera drops, the runner throws a full-screen block — &lt;em&gt;"your camera is off,&lt;br&gt;
turn it back on to continue"&lt;/em&gt; with a one-click &lt;strong&gt;Retry camera&lt;/strong&gt; — and the candidate can't&lt;br&gt;
answer or advance until it's back. The clock keeps running and the off→on gap is logged,&lt;br&gt;
so turning the camera off to buy thinking time is a &lt;em&gt;recorded&lt;/em&gt; act, not a free one. And the&lt;br&gt;
mentor's proctor gallery gets a clean &lt;strong&gt;"Camera turned off"&lt;/strong&gt; flag instead of a wall of&lt;br&gt;
black rectangles. Same track state, both jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STT confidence + a manual-correction pass.&lt;/strong&gt; On-device recognition trails Whisper on
accents and noise. I'd surface a confidence score and let the candidate fix the
transcript, keeping the audio as the authority.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real VAD, not just a level meter.&lt;/strong&gt; The RMS reading is enough for a "mic is live" cue;
proper voice-activity detection could auto-trim dead air and auto-advance on silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest limits on proctoring.&lt;/strong&gt; Client-side signals raise the &lt;em&gt;cost&lt;/em&gt; of cheating; they
don't make it impossible (a second device still exists). The goal is deterrence and an
auditable timeline for a mentor — not a courtroom — and the UI should say so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meta-lesson: before you add an AI line-item, check what &lt;code&gt;window&lt;/code&gt; already does for free.&lt;br&gt;
The browser quietly grew a whole speech-and-media stack — TTS, streaming STT, &lt;code&gt;MediaRecorder&lt;/code&gt;,&lt;br&gt;
Web Audio VAD, &lt;code&gt;MediaStreamTrack&lt;/code&gt; health — and most of us keep reaching for an API instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the last feature you paid a vendor for that the platform could already do TTS,&lt;br&gt;
STT, or something else?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>I added dark mode by editing CSS variables not 100 components</title>
      <dc:creator>Sheryar Ahmed</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:21:06 +0000</pubDate>
      <link>https://dev.to/sheryar_ahmed/i-added-dark-mode-by-editing-css-variables-not-100-components-3n8o</link>
      <guid>https://dev.to/sheryar_ahmed/i-added-dark-mode-by-editing-css-variables-not-100-components-3n8o</guid>
      <description>&lt;h1&gt;
  
  
  I added dark mode by editing CSS variables - not 100 components
&lt;/h1&gt;

&lt;p&gt;My app had ~1,200 hardcoded &lt;code&gt;indigo-*&lt;/code&gt; and ~2,600 hardcoded &lt;code&gt;slate-*&lt;/code&gt; class usages across&lt;br&gt;
130-odd components. The brief was small to say and brutal to do: &lt;em&gt;"ship dark mode, and&lt;br&gt;
let me rebrand the accent color whenever I want."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The naive plan is a sweep: open every component, add &lt;code&gt;dark:&lt;/code&gt; variants, swap colors. That's&lt;br&gt;
weeks of work, and worse, it's &lt;em&gt;permanent&lt;/em&gt; work - every new component re-incurs it, and&lt;br&gt;
the design drifts the moment two people touch it. I wanted dark mode to be a property of&lt;br&gt;
the &lt;strong&gt;theme&lt;/strong&gt;, not of every component.&lt;/p&gt;
&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;Two ideas, both "edit the tokens, not the markup":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. One brand token.&lt;/strong&gt; Instead of components knowing the color &lt;code&gt;indigo&lt;/code&gt;, they reference a&lt;br&gt;
semantic &lt;code&gt;brand&lt;/code&gt; scale. I did a one-time codemod - &lt;code&gt;indigo- → brand-&lt;/code&gt; across &lt;code&gt;app/&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;components/&lt;/code&gt;, &lt;code&gt;lib/&lt;/code&gt; (~1,230 mechanical edits, no logic change) - and defined the brand&lt;br&gt;
scale once as CSS variables. Now recoloring the entire app, globally or per-user, is a&lt;br&gt;
variable swap. No component ever needs editing again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--brand-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#EEF4FF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* … */&lt;/span&gt; &lt;span class="py"&gt;--brand-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#0066FF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--brand-600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#0052D6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* … */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* Tailwind v4: expose them so bg-/text-/border-/ring-brand-* all resolve to the vars */&lt;/span&gt;
&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="nb"&gt;inline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--brand-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--brand-600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c"&gt;/* …the full scale… */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-user accent "vibes" then become trivial - each preset just re-defines that scale under&lt;br&gt;
an attribute selector, so picking one recolors the app instantly with zero component churn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-accent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"emerald"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--brand-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#059669&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--brand-600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;#047857&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Dark mode as a token remap, not a component sweep.&lt;/strong&gt; This is the part that surprised&lt;br&gt;
me.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mechanism (and the wrong turn I took first)
&lt;/h2&gt;

&lt;p&gt;My first attempt failed silently. I tried mapping Tailwind's slate scale through my own&lt;br&gt;
indirection in &lt;code&gt;@theme inline&lt;/code&gt; - and text stayed dark in dark mode. Tailwind's &lt;em&gt;default&lt;/em&gt;&lt;br&gt;
&lt;code&gt;slate-*&lt;/code&gt; utilities weren't picking up my override.&lt;/p&gt;

&lt;p&gt;Then I read the compiled CSS, and the trick fell out of it. Tailwind v4 emits its own&lt;br&gt;
palette as real CSS variables, and the utilities reference them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.text-slate-900&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-slate-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I don't need to touch a single utility. I just &lt;strong&gt;redefine the slate ramp inside the&lt;br&gt;
&lt;code&gt;.dark&lt;/code&gt; selector&lt;/strong&gt; - reversed, so "dark text" becomes light and "light background" becomes&lt;br&gt;
dark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-slate-900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f1f5f9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* was near-black → now near-white text */&lt;/span&gt;
  &lt;span class="py"&gt;--color-slate-700&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#cbd5e1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-slate-200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3a4659&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* was light border → now a dark border */&lt;/span&gt;
  &lt;span class="py"&gt;--color-slate-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;#273345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* was page-white → now an elevated surface */&lt;/span&gt;
  &lt;span class="py"&gt;color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c"&gt;/* native scrollbars/date pickers follow too */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;text-slate-900&lt;/code&gt;, &lt;code&gt;bg-slate-50&lt;/code&gt;, &lt;code&gt;border-slate-200&lt;/code&gt; in the codebase flips&lt;br&gt;
automatically. Thousands of usages, one block of CSS.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotcha
&lt;/h2&gt;

&lt;p&gt;A reversed ramp isn't a straight inversion - &lt;strong&gt;surface hierarchy&lt;/strong&gt; breaks if you're naive.&lt;br&gt;
My first ramp made &lt;code&gt;slate-50&lt;/code&gt; darker than my card color, so every "subtle gray" inner box&lt;br&gt;
(inside drawers and cards) suddenly looked like the page bleeding &lt;em&gt;through&lt;/em&gt; the card. The&lt;br&gt;
fix was to treat the low slate shades as &lt;strong&gt;elevated&lt;/strong&gt; surfaces that sit &lt;em&gt;above&lt;/em&gt; the card,&lt;br&gt;
not below it: &lt;code&gt;canvas (#0b1120) &amp;lt; card (#1e293b) &amp;lt; slate-50/100/200&lt;/code&gt;. Dark mode isn't&lt;br&gt;
"flip the colors" - it's "preserve the depth ordering with a dark palette." Miss that and&lt;br&gt;
everything looks flat and muddy.&lt;/p&gt;

&lt;p&gt;A second, smaller one: native &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; option lists. Components set a dark background on&lt;br&gt;
the control, but the option popup inherited the dark bg &lt;em&gt;without&lt;/em&gt; a light text color -&lt;br&gt;
dark-on-dark, invisible. One global rule fixed all of them at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--card-foreground&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A no-flash inline script in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; to set the theme before first paint (right now a
provider gates render, which avoids the flash but costs a frame).&lt;/li&gt;
&lt;li&gt;Audit the handful of &lt;em&gt;intentionally&lt;/em&gt; dark elements (a dark CTA, a tooltip) that I had to
pin to fixed colors so the remap couldn't invert them - those are the exceptions that
prove the rule.&lt;/li&gt;
&lt;li&gt;A contrast-ratio check in CI so a future accent preset can't ship an unreadable pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The meta-lesson: if changing a visual property means editing many files, the property is&lt;br&gt;
in the wrong layer. Push color and theme into tokens, and "rebrand the app" or "add dark&lt;br&gt;
mode" turns from a sprint into a diff.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's your dark-mode strategy - &lt;code&gt;dark:&lt;/code&gt; everywhere, CSS variables, or something else?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
