<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dev.ideaspot360.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dev.ideaspot360.com/" rel="alternate" type="text/html" /><updated>2026-11-03T01:19:20+00:00</updated><id>https://dev.ideaspot360.com/feed.xml</id><title type="html">IdeaSpot360 Dev</title><subtitle>In-depth troubleshooting guides, cloud infrastructure post-mortems, and production-ready software engineering solutions. No fluff, just working code.</subtitle><author><name>Justin (IdeaSpot360 Lead Engineer)</name></author><entry><title type="html">Resolving Docker &amp;amp; Kubernetes Exit Code 137 (OOMKilled): Root Cause &amp;amp; JVM Memory Tuning</title><link href="https://dev.ideaspot360.com/cloud%20&%20devops/2026/11/03/resolving-docker-exit-code-137-oomkilled.html" rel="alternate" type="text/html" title="Resolving Docker &amp;amp; Kubernetes Exit Code 137 (OOMKilled): Root Cause &amp;amp; JVM Memory Tuning" /><published>2026-11-03T01:00:00+00:00</published><updated>2026-11-03T01:00:00+00:00</updated><id>https://dev.ideaspot360.com/cloud%20&amp;%20devops/2026/11/03/resolving-docker-exit-code-137-oomkilled</id><content type="html" xml:base="https://dev.ideaspot360.com/cloud%20&amp;%20devops/2026/11/03/resolving-docker-exit-code-137-oomkilled.html"><![CDATA[<p>If your containerized application terminates abruptly without leaving an application-level stack trace in standard log outputs, inspecting the container state usually reveals:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>State:          Terminated
  Reason:       OOMKilled
  Exit Code:    137
  Started:      Mon, 03 Nov 2026 01:14:22 UTC
  Finished:     Mon, 03 Nov 2026 01:38:05 UTC
</code></pre></div></div>

<p>Exit code <code class="language-plaintext highlighter-rouge">137</code> is not arbitrary: in POSIX systems, fatal signals result in an exit code of <code class="language-plaintext highlighter-rouge">128 + n</code>. Signal 9 is <code class="language-plaintext highlighter-rouge">SIGKILL</code> ($128 + 9 = 137$). Because <code class="language-plaintext highlighter-rouge">SIGKILL</code> cannot be caught, blocked, or handled by the JVM process, shutdown hooks never execute, thread dumps are never generated, and your APM agent ceases transmitting telemetry mid-flight.</p>

<p>Here is the exact diagnostic workflow and configuration fix for production workloads.</p>

<hr />

<h2 id="1-verified-environment--reproduction-specs">1. Verified Environment &amp; Reproduction Specs</h2>

<p>The behavior documented below was verified against modern container runtimes and JVM environments:</p>

<ul>
  <li><strong>Host OS Kernel:</strong> Linux 6.6.x (Ubuntu 24.04 LTS / Amazon Linux 2023)</li>
  <li><strong>Control Group:</strong> <code class="language-plaintext highlighter-rouge">cgroup v2</code> enabled (<code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/cgroup.controllers</code>)</li>
  <li><strong>Container Runtime:</strong> containerd v1.7.x / Docker Engine 27.x</li>
  <li><strong>Orchestrator:</strong> Kubernetes v1.30.x</li>
  <li><strong>Runtime Stack:</strong> Eclipse Temurin OpenJDK 21 (LTS) running Spring Boot 3.3.x</li>
</ul>

<hr />

<h2 id="2-common-false-fix-why-bumping--xmx-accelerates-the-crash">2. Common False Fix: Why Bumping <code class="language-plaintext highlighter-rouge">-Xmx</code> Accelerates the Crash</h2>

<p>When engineers first observe an OOM event, the immediate reflex is often:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># WRONG FIX: Allocating 90% of container memory to Java Heap</span>
java <span class="nt">-Xms1800m</span> <span class="nt">-Xmx1800m</span> <span class="nt">-jar</span> app.jar
</code></pre></div></div>

<p>In a container constrained to <code class="language-plaintext highlighter-rouge">2048Mi</code> memory limit in Kubernetes:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Pod Spec (Vulnerable)</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="na">limits</span><span class="pi">:</span>
    <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
  <span class="na">requests</span><span class="pi">:</span>
    <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1Gi"</span>
</code></pre></div></div>

<h3 id="why-this-fails">Why this fails:</h3>
<p>The Linux kernel OOM killer (<code class="language-plaintext highlighter-rouge">mm/oom_kill.c</code>) tracks the <strong>total resident memory consumption (RSS + swap + page cache)</strong> of all processes in the container’s cgroup, <strong>not just the Java Heap</strong>.</p>

<p>A JVM’s total memory footprint ($M_{total}$) is calculated as:</p>

\[M_{total} = \text{Heap} + \text{Metaspace} + \text{Thread Stacks} + \text{Direct ByteBuffers (Netty/NIO)} + \text{CodeCache} + \text{GC Overhead} + \text{Native C/C++ Libraries}\]

<p>When you set <code class="language-plaintext highlighter-rouge">-Xmx</code> to <code class="language-plaintext highlighter-rouge">1800MB</code> inside a <code class="language-plaintext highlighter-rouge">2048MB</code> cgroup, you leave only <strong>248MB</strong> for:</p>
<ol>
  <li><strong>Thread Stacks:</strong> 200 concurrent HTTP threads $\times$ 1MB default stack = <strong>200MB</strong>.</li>
  <li><strong>Netty Direct Memory:</strong> Off-heap networking buffers for WebFlux or gRPC.</li>
  <li><strong>Metaspace:</strong> Spring Boot class metadata usually requires <strong>120MB–180MB</strong>.</li>
</ol>

<p>The moment network traffic spikes, off-heap allocations push total process RSS to <code class="language-plaintext highlighter-rouge">2050MB</code>. The kernel immediately invokes <code class="language-plaintext highlighter-rouge">oom_killer</code> and sends <code class="language-plaintext highlighter-rouge">SIGKILL (9)</code>.</p>

<hr />

<h2 id="3-kernel-diagnostic-confirming-the-oom-event">3. Kernel Diagnostic: Confirming the OOM Event</h2>

<p>To verify that the host kernel dispatched the kill signal, inspect <code class="language-plaintext highlighter-rouge">dmesg</code> or <code class="language-plaintext highlighter-rouge">/var/log/messages</code> on the worker node:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dmesg <span class="nt">-T</span> | <span class="nb">grep</span> <span class="nt">-E</span> <span class="nt">-i</span> <span class="s2">"oom_reaper|killed process"</span>
</code></pre></div></div>

<p>Expected kernel output:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Mon Nov  3 01:38:05 2026] Memory cgroup out of memory: Killed process 38421 (java) total-vm:4918204kB, anon-rss:2097152kB, file-rss:1248kB, shmem-rss:0kB, UID:1000 pgtables:8204kB oom_score_adj:988
[Mon Nov  3 01:38:05 2026] oom_reaper: reaped process 38421 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
</code></pre></div></div>

<p>Notice <code class="language-plaintext highlighter-rouge">anon-rss:2097152kB</code> ($= 2048\text{ MB}$). The kernel killed the process precisely when anonymous pages hit the cgroup ceiling.</p>

<hr />

<h2 id="4-the-production-fix-dynamic-percentage-tuning">4. The Production Fix: Dynamic Percentage Tuning</h2>

<p>Modern OpenJDK releases (Java 11+) are fully container-aware via <code class="language-plaintext highlighter-rouge">-XX:+UseContainerSupport</code> (enabled by default). Instead of hardcoding static <code class="language-plaintext highlighter-rouge">-Xmx</code> megabytes in your Dockerfile or startup scripts, leverage percentage-based sizing and restrict off-heap headroom.</p>

<h3 id="before-vs-after-configuration">Before vs After Configuration</h3>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">#</span> In your Dockerfile or K8s Deployment args:

- ENV JAVA_OPTS="-Xms1800m -Xmx1800m"
<span class="gi">+ ENV JAVA_OPTS="-XX:+UseContainerSupport \
+                -XX:MaxRAMPercentage=70.0 \
+                -XX:InitialRAMPercentage=50.0 \
+                -XX:MaxMetaspaceSize=256m \
+                -XX:ReservedCodeCacheSize=128m \
+                -XX:+ExitOnOutOfMemoryError"
</span></code></pre></div></div>

<h3 id="explanation-of-flags">Explanation of Flags:</h3>
<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">-XX:MaxRAMPercentage=70.0</code></strong>: Automatically allocates 70% of the container’s cgroup limit to Heap ($2048\text{ MB} \times 0.70 = 1433\text{ MB}$). This guarantees 30% ($615\text{ MB}$) is reserved for Metaspace, thread stacks, and native buffers.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-XX:MaxMetaspaceSize=256m</code></strong>: Prevents runaway classloader memory leaks from consuming the remaining cgroup margin.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-XX:+ExitOnOutOfMemoryError</code></strong>: If an application-level heap exhaustion occurs, this forces the JVM to exit immediately with an informative exit code and log line before the Linux kernel kills it silently.</li>
</ol>

<hr />

<h2 id="5-kubernetes-resource-specification-best-practice">5. Kubernetes Resource Specification Best Practice</h2>

<p>Align Kubernetes requests and limits to guarantee consistent Quality of Service (QoS):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">billing-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ideaspot360/billing-service:v1.4.2</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000m"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2000m"</span>
          <span class="na">env</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">JAVA_TOOL_OPTIONS</span>
              <span class="na">value</span><span class="pi">:</span> <span class="pi">&gt;-</span>
                <span class="s">-XX:MaxRAMPercentage=70.0</span>
                <span class="s">-XX:MaxMetaspaceSize=256m</span>
                <span class="s">-XX:+HeapDumpOnOutOfMemoryError</span>
                <span class="s">-XX:HeapDumpPath=/dumps/heapdump.hprof</span>
</code></pre></div></div>

<p>By providing equal <code class="language-plaintext highlighter-rouge">requests.memory</code> and <code class="language-plaintext highlighter-rouge">limits.memory</code>, Kubernetes assigns the pod a <strong>Guaranteed QoS class</strong>, minimizing the probability of node-pressure eviction under tight cluster resources.</p>]]></content><author><name>Justin (IdeaSpot360 Lead Engineer)</name></author><category term="Cloud &amp; DevOps" /><category term="docker" /><category term="kubernetes" /><category term="java" /><category term="devops" /><category term="troubleshooting" /><summary type="html"><![CDATA[When your production container abruptly terminates with exit code 137, increasing -Xmx usually makes it crash faster. Here is how Linux cgroups v2, off-heap buffers, and container memory limits actually interact.]]></summary></entry><entry><title type="html">Fixing Kubernetes Pod Evictions and CrashLoopBackOff: Ephemeral Storage Limits &amp;amp; EmptyDir Sizing</title><link href="https://dev.ideaspot360.com/cloud%20&%20devops/2026/11/03/kubernetes-pod-eviction-ephemeral-storage.html" rel="alternate" type="text/html" title="Fixing Kubernetes Pod Evictions and CrashLoopBackOff: Ephemeral Storage Limits &amp;amp; EmptyDir Sizing" /><published>2026-11-03T00:00:00+00:00</published><updated>2026-11-03T00:00:00+00:00</updated><id>https://dev.ideaspot360.com/cloud%20&amp;%20devops/2026/11/03/kubernetes-pod-eviction-ephemeral-storage</id><content type="html" xml:base="https://dev.ideaspot360.com/cloud%20&amp;%20devops/2026/11/03/kubernetes-pod-eviction-ephemeral-storage.html"><![CDATA[<p>One of the most elusive production failures in Kubernetes clusters occurs when a pod transitions from <code class="language-plaintext highlighter-rouge">Running</code> to <code class="language-plaintext highlighter-rouge">Evicted</code> without throwing memory OOM warnings or application-level stack traces:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Status:       Failed
Reason:       Evicted
Message:      The node was low on resource: ephemeral-storage. 
              Threshold quantity: 5368709120, available: 4194304000. 
              Container app was using 6291456000, requesting 1073741824.
</code></pre></div></div>

<p>Or subsequently:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Warning  Evicted    2m (x3 over 15m)   kubelet            The node was low on resource: ephemeral-storage.
  Normal   Killing    2m                 kubelet            Stopping container billing-worker
</code></pre></div></div>

<p>Unlike CPU throttling (which merely delays response times) or RAM exhaustion (which triggers <code class="language-plaintext highlighter-rouge">OOMKilled exit code 137</code>), <strong>ephemeral storage eviction immediately terminates the pod and schedules a reschedule</strong>. If multiple pods on the same worker node breach storage thresholds simultaneously, cascading node evictions can disrupt cluster stability.</p>

<p>Here is the exact diagnostic workflow to pinpoint runaway disk consumption and configure bulletproof ephemeral storage boundaries.</p>

<hr />

<h2 id="1-verified-production-specs">1. Verified Production Specs</h2>

<p>The diagnostic rules and remediation configs documented here were verified on:</p>

<ul>
  <li><strong>Kubernetes Control Plane &amp; Kubelet:</strong> v1.29.x &amp; v1.30.x</li>
  <li><strong>Container Runtime:</strong> <code class="language-plaintext highlighter-rouge">containerd</code> v1.7.15 (with overlayfs snapshotter)</li>
  <li><strong>Underlying Host OS:</strong> Amazon Linux 2023 / Ubuntu 22.04 LTS</li>
  <li><strong>Storage Driver:</strong> AWS EBS <code class="language-plaintext highlighter-rouge">gp3</code> (mounted on <code class="language-plaintext highlighter-rouge">/var/lib/kubelet</code> and <code class="language-plaintext highlighter-rouge">/var/lib/containerd</code>)</li>
</ul>

<hr />

<h2 id="2-common-false-fix-why-application-logging-limits-alone-fail">2. Common False Fix: Why Application Logging Limits Alone Fail</h2>

<p>A common reaction to storage eviction is tuning logging frameworks (e.g., Logback or Winston) to rotate logs faster:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># WRONG FIX: Assuming standard container logs are the sole culprit</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="na">requests</span><span class="pi">:</span>
    <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
    <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1Gi"</span>
  <span class="c1"># Omitting ephemeral-storage limits under the assumption that stdout log rotation handles it</span>
</code></pre></div></div>

<h3 id="why-this-fails">Why this fails:</h3>
<p>Kubelet calculates a container’s local ephemeral storage consumption ($S_{ephemeral}$) as the aggregate of three distinct layers:</p>

\[S_{ephemeral} = S_{\text{container writable layer (rootfs)}} + S_{\text{stdout/stderr logs}} + S_{\text{emptyDir volumes}}\]

<p>Even if your container runtime truncates <code class="language-plaintext highlighter-rouge">stdout</code> logs at <code class="language-plaintext highlighter-rouge">10MB</code>, applications that:</p>
<ol>
  <li>Generate uncompressed PDF exports or temporary CSV reports in <code class="language-plaintext highlighter-rouge">/tmp</code></li>
  <li>Download dynamic model weights or ML artifacts at runtime</li>
  <li>Retain unpruned Lucene/Elasticsearch index segments in local directories</li>
</ol>

<p>will rapidly bloat the container’s <strong>writable layer (<code class="language-plaintext highlighter-rouge">upperdir</code>)</strong>. Once this layer breaches the kubelet eviction threshold, the node drops the hammer regardless of CPU or memory margins.</p>

<hr />

<h2 id="3-diagnosing-disk-consumption-inside-the-pod">3. Diagnosing Disk Consumption Inside the Pod</h2>

<p>Before the pod is killed, identify which directory is ballooning in storage. Execute an interactive exec into a running replica:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec</span> <span class="nt">-it</span> &lt;pod-name&gt; <span class="nt">-n</span> production <span class="nt">--</span> <span class="nb">df</span> <span class="nt">-h</span>
</code></pre></div></div>

<p>Then identify disk hogs by depth:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nb">exec</span> <span class="nt">-it</span> &lt;pod-name&gt; <span class="nt">-n</span> production <span class="nt">--</span> <span class="nb">du</span> <span class="nt">-ah</span> / 2&gt;/dev/null | <span class="nb">sort</span> <span class="nt">-rh</span> | <span class="nb">head</span> <span class="nt">-n</span> 20
</code></pre></div></div>

<p>Typical output revealing the leak:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4.8G    /tmp/report_cache
1.2G    /var/log/application.log
320M    /app/node_modules
</code></pre></div></div>

<p>Notice <code class="language-plaintext highlighter-rouge">/tmp/report_cache</code> consumed nearly 5GB of the container’s root file system.</p>

<hr />

<h2 id="4-the-architectural-fix-decoupling-temp-storage-with-memory-backed-emptydir">4. The Architectural Fix: Decoupling Temp Storage with Memory-Backed <code class="language-plaintext highlighter-rouge">emptyDir</code></h2>

<p>To protect the container filesystem from unconstrained writes, explicitly route high-throughput temporary directories (<code class="language-plaintext highlighter-rouge">/tmp</code>, <code class="language-plaintext highlighter-rouge">/app/cache</code>) to an isolated <code class="language-plaintext highlighter-rouge">emptyDir</code> volume.</p>

<h3 id="implementation-ram-backed-temp-mount">Implementation: RAM-Backed Temp Mount</h3>

<p>For workloads generating ephemeral artifacts, mounting <code class="language-plaintext highlighter-rouge">emptyDir</code> backed by <code class="language-plaintext highlighter-rouge">medium: Memory</code> guarantees supercharged I/O speeds while preventing disk exhaustion on the host EBS root volume:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">billing-worker</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">production</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">billing-worker</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">ideaspot360/billing-worker:v2.1.0</span>
          <span class="na">resources</span><span class="pi">:</span>
            <span class="na">requests</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1000m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
              <span class="na">ephemeral-storage</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
            <span class="na">limits</span><span class="pi">:</span>
              <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2000m"</span>
              <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2Gi"</span>
              <span class="na">ephemeral-storage</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4Gi"</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">scratch-storage</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/tmp</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">scratch-storage</span>
          <span class="na">emptyDir</span><span class="pi">:</span>
            <span class="c1"># Memory medium uses tmpfs (consumes container RAM limit instead of host disk)</span>
            <span class="na">medium</span><span class="pi">:</span> <span class="s">Memory</span>
            <span class="na">sizeLimit</span><span class="pi">:</span> <span class="s2">"</span><span class="s">512Mi"</span>
</code></pre></div></div>

<h3 id="key-differences--benefits">Key Differences &amp; Benefits:</h3>
<ol>
  <li><strong>Explicit <code class="language-plaintext highlighter-rouge">ephemeral-storage</code> Limits</strong>: Guarantees that if disk writes exceed <code class="language-plaintext highlighter-rouge">4Gi</code>, kubelet evicts <em>only</em> this rogue pod before it exhausts the host node’s root filesystem and destabilizes neighboring services.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">medium: Memory</code> for <code class="language-plaintext highlighter-rouge">/tmp</code></strong>: Directs all temporary files into a fast <code class="language-plaintext highlighter-rouge">tmpfs</code> RAM slice. If the app forgets to clean up temporary exports, it will trip the <code class="language-plaintext highlighter-rouge">512Mi</code> limit cleanly without touching physical disk sectors.</li>
</ol>]]></content><author><name>Justin (IdeaSpot360 Lead Engineer)</name></author><category term="Cloud &amp; DevOps" /><category term="kubernetes" /><category term="devops" /><category term="docker" /><category term="containers" /><category term="troubleshooting" /><summary type="html"><![CDATA[When your Kubernetes pods suddenly enter an Evicted or CrashLoopBackOff state due to 'ephemeral-storage exceeded', increasing CPU or RAM limits will not prevent node drain. Here is how k8s local storage accounting actually works.]]></summary></entry><entry><title type="html">Debugging PostgreSQL Deadlocks: Root Cause, Row-Level Lock Queues &amp;amp; Advisory Locks</title><link href="https://dev.ideaspot360.com/database%20&%20backend/2026/11/03/postgres-deadlock-troubleshooting.html" rel="alternate" type="text/html" title="Debugging PostgreSQL Deadlocks: Root Cause, Row-Level Lock Queues &amp;amp; Advisory Locks" /><published>2026-11-03T00:00:00+00:00</published><updated>2026-11-03T00:00:00+00:00</updated><id>https://dev.ideaspot360.com/database%20&amp;%20backend/2026/11/03/postgres-deadlock-troubleshooting</id><content type="html" xml:base="https://dev.ideaspot360.com/database%20&amp;%20backend/2026/11/03/postgres-deadlock-troubleshooting.html"><![CDATA[<p>When concurrent database transactions attempt to acquire conflicting row-level or table-level locks in cyclic dependencies, the PostgreSQL lock manager throws:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR:  deadlock detected
DETAIL:  Process 21402 waits for ShareLock on transaction 849201; blocked by process 21408.
Process 21408 waits for ExclusiveLock on tuple (42, 18) of relation "orders" of database 16384; blocked by process 21402.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (42, 18) in relation "orders"
</code></pre></div></div>

<p>A common anti-pattern is wrapping queries in exponential backoff retry loops. While retrying mitigates transient collisions, it drastically increases query latency tail percentiles ($P_{99}$) and conceals architectural race conditions under peak traffic.</p>

<p>Here is the exact diagnostic workflow to pinpoint deadlocking queries and restructure locking order.</p>

<hr />

<h2 id="1-verified-production-environment">1. Verified Production Environment</h2>

<ul>
  <li><strong>Database Engine:</strong> PostgreSQL 16.3 (x86_64 Linux on AWS RDS / Self-hosted)</li>
  <li><strong>Transaction Isolation Level:</strong> <code class="language-plaintext highlighter-rouge">READ COMMITTED</code> (default)</li>
  <li><strong>Connection Pooler:</strong> PgBouncer 1.22.x in transaction pooling mode</li>
  <li><strong>Application Framework:</strong> Spring Boot 3.3.x (HikariCP 5.1.0) / Node.js <code class="language-plaintext highlighter-rouge">pg-promise</code></li>
</ul>

<hr />

<h2 id="2-common-false-fix-why-application-level-retries-fail">2. Common False Fix: Why Application-Level Retries Fail</h2>

<p>Consider two microservice endpoints updating orders and inventory balances concurrently:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># WRONG FIX: Blind retry loops without lock order synchronization
</span><span class="o">@</span><span class="n">retry</span><span class="p">(</span><span class="n">exceptions</span><span class="o">=</span><span class="n">DeadlockDetected</span><span class="p">,</span> <span class="n">max_attempts</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">backoff</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">execute_order_settlement</span><span class="p">(</span><span class="n">order_id</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">):</span>
    <span class="k">with</span> <span class="n">db</span><span class="p">.</span><span class="n">transaction</span><span class="p">():</span>
        <span class="c1"># Service A: Locks user balance first, then updates order status
</span>        <span class="n">update_user_balance</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
        <span class="n">update_order_status</span><span class="p">(</span><span class="n">order_id</span><span class="p">,</span> <span class="s">'COMPLETED'</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Concurrently executed by automated invoice reconciliation job:
</span><span class="k">with</span> <span class="n">db</span><span class="p">.</span><span class="n">transaction</span><span class="p">():</span>
    <span class="c1"># Service B: Locks order status first, then audits user balance
</span>    <span class="n">update_order_status</span><span class="p">(</span><span class="n">order_id</span><span class="p">,</span> <span class="s">'PROCESSING'</span><span class="p">)</span>
    <span class="n">audit_user_balance</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</code></pre></div></div>

<p>If Service A locks <code class="language-plaintext highlighter-rouge">user_balance</code> while Service B locks <code class="language-plaintext highlighter-rouge">orders</code>, both workers block each other. After <code class="language-plaintext highlighter-rouge">deadlock_timeout</code> (default <code class="language-plaintext highlighter-rouge">1s</code>), PostgreSQL aborts one transaction. Retrying merely repeats the contention cycle under high throughput, creating database CPU spikes.</p>

<hr />

<h2 id="3-investigating-lock-contention-with-pg_locks">3. Investigating Lock Contention with <code class="language-plaintext highlighter-rouge">pg_locks</code></h2>

<p>To inspect live lock wait queues before the deadlock detector terminates transactions, execute:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
    <span class="n">blocked_locks</span><span class="p">.</span><span class="n">pid</span>     <span class="k">AS</span> <span class="n">blocked_pid</span><span class="p">,</span>
    <span class="n">blocked_activity</span><span class="p">.</span><span class="n">usename</span>  <span class="k">AS</span> <span class="n">blocked_user</span><span class="p">,</span>
    <span class="n">blocking_locks</span><span class="p">.</span><span class="n">pid</span>    <span class="k">AS</span> <span class="n">blocking_pid</span><span class="p">,</span>
    <span class="n">blocking_activity</span><span class="p">.</span><span class="n">usename</span> <span class="k">AS</span> <span class="n">blocking_user</span><span class="p">,</span>
    <span class="n">blocked_activity</span><span class="p">.</span><span class="n">query</span>    <span class="k">AS</span> <span class="n">blocked_statement</span><span class="p">,</span>
    <span class="n">blocking_activity</span><span class="p">.</span><span class="n">query</span>   <span class="k">AS</span> <span class="n">blocking_statement</span>
<span class="k">FROM</span>  <span class="n">pg_catalog</span><span class="p">.</span><span class="n">pg_locks</span>         <span class="n">blocked_locks</span>
<span class="k">JOIN</span> <span class="n">pg_catalog</span><span class="p">.</span><span class="n">pg_stat_activity</span> <span class="n">blocked_activity</span> <span class="k">ON</span> <span class="n">blocked_activity</span><span class="p">.</span><span class="n">pid</span> <span class="o">=</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">pid</span>
<span class="k">JOIN</span> <span class="n">pg_catalog</span><span class="p">.</span><span class="n">pg_locks</span>         <span class="n">blocking_locks</span> 
    <span class="k">ON</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">locktype</span> <span class="o">=</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">locktype</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="k">database</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="k">database</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">relation</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">relation</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">page</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">page</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">tuple</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">tuple</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">virtualxid</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">virtualxid</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">transactionid</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">transactionid</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">classid</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">classid</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">objid</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">objid</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">objsubid</span> <span class="k">IS</span> <span class="k">NOT</span> <span class="k">DISTINCT</span> <span class="k">FROM</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">objsubid</span>
    <span class="k">AND</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">pid</span> <span class="o">!=</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="n">pid</span>
<span class="k">JOIN</span> <span class="n">pg_catalog</span><span class="p">.</span><span class="n">pg_stat_activity</span> <span class="n">blocking_activity</span> <span class="k">ON</span> <span class="n">blocking_activity</span><span class="p">.</span><span class="n">pid</span> <span class="o">=</span> <span class="n">blocking_locks</span><span class="p">.</span><span class="n">pid</span>
<span class="k">WHERE</span> <span class="k">NOT</span> <span class="n">blocked_locks</span><span class="p">.</span><span class="k">granted</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="4-architectural-solutions">4. Architectural Solutions</h2>

<h3 id="solution-a-strict-deterministic-ordering-alphabetical--id-sort">Solution A: Strict Deterministic Ordering (Alphabetical / ID Sort)</h3>
<p>Whenever updating multiple rows in batch, <strong>always sort primary keys in ascending order</strong> prior to issuing SQL statements:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- UPDATE accounts SET balance = balance - 100 WHERE id IN (85, 12, 44);
</span><span class="gi">+ -- Sort IDs beforehand in application memory: [12, 44, 85]
+ UPDATE accounts SET balance = balance - 100 WHERE id = 12;
+ UPDATE accounts SET balance = balance - 100 WHERE id = 44;
+ UPDATE accounts SET balance = balance - 100 WHERE id = 85;
</span></code></pre></div></div>

<p>When every worker acquires row locks in identical numerical sequence ($12 \to 44 \to 85$), cyclic deadlocks become mathematically impossible.</p>

<h3 id="solution-b-application-level-advisory-locks">Solution B: Application-Level Advisory Locks</h3>
<p>For complex workflows spanning multiple tables where row sorting is impractical, use PostgreSQL advisory locks:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Acquire transactional advisory lock based on user_id hash</span>
<span class="k">SELECT</span> <span class="n">pg_advisory_xact_lock</span><span class="p">(</span><span class="n">hashtext</span><span class="p">(</span><span class="s1">'user_account_'</span> <span class="o">||</span> <span class="p">:</span><span class="n">userId</span><span class="p">));</span>

<span class="c1">-- Proceed with safe multi-table operations</span>
<span class="k">UPDATE</span> <span class="n">accounts</span> <span class="k">SET</span> <span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span> <span class="o">-</span> <span class="p">:</span><span class="n">amount</span> <span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="p">:</span><span class="n">userId</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="n">orders</span> <span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'PAID'</span> <span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="p">:</span><span class="n">userId</span><span class="p">;</span>

<span class="c1">-- Lock automatically releases upon COMMIT or ROLLBACK</span>
</code></pre></div></div>]]></content><author><name>Justin (IdeaSpot360 Lead Engineer)</name></author><category term="Database &amp; Backend" /><category term="postgresql" /><category term="database" /><category term="backend" /><category term="sql" /><category term="troubleshooting" /><summary type="html"><![CDATA[When PostgreSQL throws 'deadlock detected: Process 4120 waits for ShareLock on transaction', simply retrying hides race conditions. Here is how row-level lock trees and foreign key validation actually behave.]]></summary></entry></feed>