<?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: Amaresh Pelleti</title>
    <description>The latest articles on DEV Community by Amaresh Pelleti (@amareswer).</description>
    <link>https://dev.to/amareswer</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%2F3978481%2Fbef1aa2c-c07a-414a-bb88-ea788ca39ba2.jpg</url>
      <title>DEV Community: Amaresh Pelleti</title>
      <link>https://dev.to/amareswer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amareswer"/>
    <language>en</language>
    <item>
      <title>Redis 8.6: What Changed in Performance and Event Streams</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:42:55 +0000</pubDate>
      <link>https://dev.to/amareswer/redis-86-what-changed-in-performance-and-event-streams-392a</link>
      <guid>https://dev.to/amareswer/redis-86-what-changed-in-performance-and-event-streams-392a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/redis-8-6-whats-new/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Redis 8.6 went GA in February 2026 and the latest patch, 8.6.7, shipped September 17 with security fixes only — the feature set has been stable for months. The headline feature isn't a speed number. It's a real fix for a problem every Streams-as-a-queue user has hit: what happens when your producer times out waiting for an &lt;code&gt;XADD&lt;/code&gt; reply and doesn't know if the message landed. Here's Redis 8.6 explained with the commands that actually change how you'd build on it, verified against &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.6-release-notes/" rel="noopener noreferrer"&gt;Redis's own release notes&lt;/a&gt; and &lt;a href="https://redis.io/docs/latest/develop/data-types/streams/idempotency/" rel="noopener noreferrer"&gt;idempotency docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis 8.6 Stream Idempotency: What XADD IDMP Actually Fixes
&lt;/h2&gt;

&lt;p&gt;If you've built a queue on &lt;a href="https://devtoolhub.com/database-types-guide/" rel="noopener noreferrer"&gt;Redis Streams&lt;/a&gt;, you've hit this: a producer calls &lt;code&gt;XADD&lt;/code&gt;, the connection drops before the reply arrives, and the producer has no way to know whether the message was written. Retry, and you risk a duplicate. Don't retry, and you risk losing the message. Per Redis's own docs, this is exactly the gap Redis 8.6 closes with &lt;strong&gt;idempotent message processing&lt;/strong&gt;, new in this release.&lt;/p&gt;

&lt;p&gt;Two modes, both added to &lt;code&gt;XADD&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manual mode — you supply both producer ID and message ID&lt;/span&gt;
XADD mystream IDMP producer-1 iid-1 &lt;span class="k"&gt;*&lt;/span&gt; field value

&lt;span class="c"&gt;# Automatic mode — Redis derives the message ID from the field/value content&lt;/span&gt;
XADD mystream IDMPAUTO producer-1 &lt;span class="k"&gt;*&lt;/span&gt; field value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resend the exact same &lt;code&gt;(producer-id, iid)&lt;/code&gt; pair — whether because the producer retried after a timeout or crashed and restarted — and Redis returns the ID of the &lt;em&gt;original&lt;/em&gt; entry instead of writing a duplicate. &lt;code&gt;IDMPAUTO&lt;/code&gt; skips supplying your own ID and lets Redis hash the field/value pairs instead, so identical content gets deduplicated automatically; the tradeoff is the hash calculation, which Redis's own docs note makes &lt;code&gt;IDMPAUTO&lt;/code&gt; "slightly slower" than manual &lt;code&gt;IDMP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Retention is tunable per stream via &lt;code&gt;XCFGSET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;XCFGSET mystream IDMP-DURATION 300 IDMP-MAXSIZE 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IDMP-DURATION&lt;/code&gt; (1–86,400 seconds, default 100) controls how long Redis remembers an ID; &lt;code&gt;IDMP-MAXSIZE&lt;/code&gt; (1–10,000, default 100) caps how many IDs it tracks per producer, and is the harder limit of the two — Redis evicts the oldest tracked IDs once &lt;code&gt;IDMP-MAXSIZE&lt;/code&gt; is hit, regardless of how much time is left on &lt;code&gt;IDMP-DURATION&lt;/code&gt;. Redis's own sizing guidance: set &lt;code&gt;IDMP-DURATION&lt;/code&gt; to roughly how long your producer takes to recover from a crash and resume sending, and size &lt;code&gt;IDMP-MAXSIZE&lt;/code&gt; as &lt;code&gt;mark-delay (ms) × messages/ms + margin&lt;/code&gt; — their own worked example is 1K msgs/sec at an 80ms mark-delay needing &lt;code&gt;IDMP-MAXSIZE&lt;/code&gt; around 100.&lt;/p&gt;

&lt;p&gt;This isn't free, and Redis is specific about the cost: &lt;strong&gt;2–5% lower throughput&lt;/strong&gt; on &lt;code&gt;XADD&lt;/code&gt; compared to the non-idempotent path, with under 1.5% additional memory overhead. For a queue where a duplicate message means double-charging a customer or double-sending a notification, that's a cheap trade. For a high-throughput logging stream where an occasional duplicate is harmless, it's not worth turning on. &lt;code&gt;XINFO STREAM&lt;/code&gt; now reports &lt;code&gt;iids-duplicates&lt;/code&gt; so you can actually measure how often it's catching something, rather than guessing.&lt;/p&gt;

&lt;p&gt;One real caveat from the release notes, not a hypothetical: avoid &lt;code&gt;IDMP&lt;/code&gt;/&lt;code&gt;IDMPAUTO&lt;/code&gt; if you're running &lt;code&gt;appendonly yes&lt;/code&gt; with the non-default &lt;code&gt;aof-use-rdb-preamble no&lt;/code&gt; — Redis flags this combination as a known limitation to be fixed in a later patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  New Eviction Policies: volatile-lrm and allkeys-lrm
&lt;/h2&gt;

&lt;p&gt;Redis 8.6 adds two eviction policies — &lt;code&gt;volatile-lrm&lt;/code&gt; (keys with a TTL) and &lt;code&gt;allkeys-lrm&lt;/code&gt; (any key) — that evict based on &lt;strong&gt;least recently modified&lt;/strong&gt;, not least recently used. The distinction matters for read-heavy caches: standard LRU treats a read as "recently used" and protects the key from eviction, while LRM only resets the clock on a write. That means a key you read constantly but never update is exactly as evictable as one nobody's touched, which is the behavior you actually want if "modified recently" is a better proxy for relevance than "read recently" in your workload (a cached computed result, for instance, versus a hot but static config value you're not trying to protect from eviction just because it gets read a lot).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hot Key Detection: the HOTKEYS Command
&lt;/h2&gt;

&lt;p&gt;Redis 8.6 also ships a new &lt;code&gt;HOTKEYS&lt;/code&gt; command for real-time detection and reporting of frequently accessed keys — useful for finding the one key that's skewing load on a cluster shard before it becomes an incident, rather than reconstructing it after the fact from slow-log guesswork.&lt;/p&gt;

&lt;p&gt;Like our recent look at &lt;a href="https://devtoolhub.com/postgresql-19-whats-shipping-whats-cut/" rel="noopener noreferrer"&gt;PostgreSQL 19&lt;/a&gt;, this is a release where the real-world impact comes down to one or two specific changes, not the full feature list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Redis Doesn't Quantify
&lt;/h2&gt;

&lt;p&gt;Redis's own materials describe "substantial memory reduction for hashes and sorted sets" (from unifying field/value and score/value into single structs) and "substantial performance improvements" across more than 20 optimizations — but neither the official 8.6 release notes nor the "what's new" page attaches a specific percentage to either claim. Worth knowing if you're deciding whether to benchmark your own workload before upgrading: Redis isn't hiding a number here, there just isn't one published. The one area where Redis did publish a number is the idempotency overhead above (2–5% throughput, &amp;lt;1.5% memory) — because that's a cost, and costs get measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Upgrade to Redis 8.6?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running Streams as a queue or event log where duplicates are costly&lt;/strong&gt;: yes, specifically for &lt;code&gt;IDMP&lt;/code&gt;/&lt;code&gt;IDMPAUTO&lt;/code&gt;. This is the first Redis-native answer to a problem people have previously solved with external dedup tables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read-heavy caching workload with a mix of hot-but-static and actively-updated keys&lt;/strong&gt;: worth testing &lt;code&gt;allkeys-lrm&lt;/code&gt; against your current &lt;code&gt;allkeys-lru&lt;/code&gt; policy — it's a config change, not a migration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running Redis via Docker Compose&lt;/strong&gt;: a version bump is a good moment to double-check your compose file doesn't &lt;a href="https://devtoolhub.com/docker-compose-recreates-containers/" rel="noopener noreferrer"&gt;recreate the container unnecessarily&lt;/a&gt; on every &lt;code&gt;up&lt;/code&gt; — unrelated to 8.6 itself, but worth catching while you're already touching the service definition.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running &lt;code&gt;appendonly yes&lt;/code&gt; with &lt;code&gt;aof-use-rdb-preamble no&lt;/code&gt;&lt;/strong&gt;: hold off on &lt;code&gt;IDMP&lt;/code&gt;/&lt;code&gt;IDMPAUTO&lt;/code&gt; specifically until the known limitation is patched; the rest of 8.6 is unaffected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Just want the performance improvements&lt;/strong&gt;: they're real per Redis's own notes, just not independently quantified — benchmark your own workload rather than expecting a specific percentage.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Redis 8.6 guarantee exactly-once delivery for Streams?&lt;/strong&gt;&lt;br&gt;
A: No — it guarantees at-most-once &lt;em&gt;production&lt;/em&gt; (no duplicate entries from a retried &lt;code&gt;XADD&lt;/code&gt;). Consumer-side processing guarantees are a separate concern handled by consumer groups and acknowledgments, which idempotent production doesn't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the performance cost of enabling IDMP?&lt;/strong&gt;&lt;br&gt;
A: Per Redis's own docs: 2-5% lower &lt;code&gt;XADD&lt;/code&gt; throughput, under 1.5% additional memory, with &lt;code&gt;IDMPAUTO&lt;/code&gt; slightly slower than manual &lt;code&gt;IDMP&lt;/code&gt; due to its hash calculation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is allkeys-lrm a replacement for allkeys-lru?&lt;/strong&gt;&lt;br&gt;
A: Not a direct replacement — a different eviction signal. LRU tracks reads, LRM tracks writes. Pick LRM when "recently modified" better predicts what's still relevant in your workload than "recently accessed" does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the latest Redis 8.6 patch?&lt;/strong&gt;&lt;br&gt;
A: 8.6.7, released September 17, 2026 — security fixes only (an unauthenticated-cluster-bus-port issue and a few crash fixes). The feature set has been unchanged since the 8.6.0 GA in February 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis 8.6 (GA February 2026, latest patch 8.6.7) adds idempotent Streams production via &lt;code&gt;XADD IDMP&lt;/code&gt;/&lt;code&gt;IDMPAUTO&lt;/code&gt; — real duplicate-message prevention for queue-style use cases&lt;/li&gt;
&lt;li&gt;Cost is measured and specific: 2-5% lower &lt;code&gt;XADD&lt;/code&gt; throughput, &amp;lt;1.5% extra memory; tune retention with &lt;code&gt;XCFGSET&lt;/code&gt;'s &lt;code&gt;IDMP-DURATION&lt;/code&gt;/&lt;code&gt;IDMP-MAXSIZE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;volatile-lrm&lt;/code&gt;/&lt;code&gt;allkeys-lrm&lt;/code&gt; eviction policies evict by last-modified time instead of last-accessed time&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;HOTKEYS&lt;/code&gt; command surfaces frequently-accessed keys in real time&lt;/li&gt;
&lt;li&gt;"Substantial" memory/performance gains are real per Redis's own notes but not independently quantified — benchmark your own workload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running Streams as a real queue, &lt;code&gt;XADD IDMP&lt;/code&gt; is the first Redis-native fix for the retry-duplicate problem — worth adopting even if you skip everything else in this release.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>database</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Cilium mTLS ztunnel: Sidecar-Free Encryption Explained</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:37:20 +0000</pubDate>
      <link>https://dev.to/amareswer/cilium-mtls-ztunnel-sidecar-free-encryption-explained-nke</link>
      <guid>https://dev.to/amareswer/cilium-mtls-ztunnel-sidecar-free-encryption-explained-nke</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/cilium-native-mtls-ztunnel/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cilium mTLS ztunnel doesn't need a SPIRE server — and that's actually the second-biggest news here. The bigger story: Cilium's older, SPIRE-based "Mutual Authentication" feature is now officially deprecated, with removal targeted for v1.21. If you adopted it, you're migrating whether you planned to or not. I fetched the real docs and the maintainer's own deprecation issue to find out what that migration actually involves — and the real gotcha isn't SPIRE at all. (New to Cilium or Kubernetes networking generally? Start with our &lt;a href="https://devtoolhub.com/kubernetes-complete-guide/" rel="noopener noreferrer"&gt;Kubernetes complete guide&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed: SPIRE-Based Mutual Auth Is Being Removed
&lt;/h2&gt;

&lt;p&gt;Cilium shipped a SPIRE-based "Mutual Authentication" feature back in 2023 (as a Beta), requiring a SPIRE server deployed alongside Cilium (&lt;code&gt;authentication.mutual.spire.enabled=true&lt;/code&gt;) to issue and verify identity certificates. Per the &lt;a href="https://github.com/cilium/cilium/issues/47132" rel="noopener noreferrer"&gt;maintainer's own deprecation issue&lt;/a&gt;, opened by a Cilium committer: "Given the lack of progress on Mutual Auth, and the presence and at least equal stability of the new feature, as of Cilium v1.20, Cilium committers are marking the Mutual Auth feature as deprecated, and will remove the feature in a later version, most likely v1.21." The &lt;a href="https://github.com/cilium/cilium/pull/48647" rel="noopener noreferrer"&gt;agent-side removal PR&lt;/a&gt; is already merged.&lt;/p&gt;

&lt;p&gt;The replacement is &lt;strong&gt;ztunnel Transparent Encryption&lt;/strong&gt; — also Beta, but actively developed, and it doesn't ask you to run SPIRE at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cilium mTLS ztunnel: How It Actually Works (No SPIRE Needed)
&lt;/h2&gt;

&lt;p&gt;Per &lt;a href="https://docs.cilium.io/en/stable/security/network/encryption-ztunnel/" rel="noopener noreferrer"&gt;Cilium's ztunnel documentation&lt;/a&gt;, ztunnel is "a purpose-built per-node proxy that provides transparent Layer 4 mTLS encryption and authentication for pod-to-pod communication." Instead of a SPIRE server, it needs a set of Kubernetes secrets generated with a short OpenSSL script the docs provide — a bootstrap key pair (secures the connection between ztunnel and Cilium's certificate server) and a CA key pair (the root for ephemeral, in-memory client certificates ztunnel issues on request):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; bootstrap-private.key 2048
&lt;span class="c"&gt;# ... (bootstrap cert generation, see docs)&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; ca-private.key 2048
&lt;span class="c"&gt;# ... (CA cert generation, see docs)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system create secret generic cilium-ztunnel-secrets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bootstrap-private.key&lt;span class="o"&gt;=&lt;/span&gt;bootstrap-private.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bootstrap-root.crt&lt;span class="o"&gt;=&lt;/span&gt;bootstrap-root.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca-private.key&lt;span class="o"&gt;=&lt;/span&gt;ca-private.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ca-root.crt&lt;span class="o"&gt;=&lt;/span&gt;ca-root.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enable it at install time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cilium &lt;span class="nb"&gt;install &lt;/span&gt;1.20.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; encryption.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; encryption.type&lt;span class="o"&gt;=&lt;/span&gt;ztunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One hard precondition the docs call out: &lt;strong&gt;Cluster Mesh must be disabled first&lt;/strong&gt; — ztunnel "is currently not compatible with Cluster Mesh," and enabling both together throws a validation error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enrolling Namespaces (Not Pods)
&lt;/h2&gt;

&lt;p&gt;Once ztunnel is running, nothing is encrypted until you opt namespaces in. Per the docs, enrollment is a namespace label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl label namespace &amp;lt;namespace-name&amp;gt; io.cilium/mtls-enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every existing pod in that namespace gets enrolled (ztunnel pods excepted), iptables rules get installed in each pod's network namespace to redirect traffic to the local ztunnel proxy, and every future pod created in the namespace is enrolled automatically. Disenrolling is the same label, removed: &lt;code&gt;kubectl label namespace &amp;lt;namespace-name&amp;gt; io.cilium/mtls-enabled-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That namespace-level granularity sounds convenient for a gradual rollout. It's also where the real migration risk is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Migration Gotcha: Partial Enrollment Breaks Traffic
&lt;/h2&gt;

&lt;p&gt;The docs are explicit about this, in one line easy to skim past: &lt;strong&gt;"Traffic between workloads is only supported when both the source and destination endpoints are enrolled in ztunnel. Communication between an enrolled workload and a non-enrolled workload is not supported."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice: if you enroll &lt;code&gt;payments&lt;/code&gt; but haven't gotten to &lt;code&gt;orders&lt;/code&gt; yet, and &lt;code&gt;payments&lt;/code&gt; calls &lt;code&gt;orders&lt;/code&gt;, that call breaks — not silently degrades to unencrypted, just doesn't work. A namespace-by-namespace rollout plan has to account for every namespace a given workload talks to, not just the one you're migrating. For anything beyond a small cluster, map your service-to-service dependencies before you label the first namespace, not after something stops responding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Gotcha: It Can Silently Break L4 NetworkPolicy
&lt;/h2&gt;

&lt;p&gt;This is the one that catches people who already did their homework on the namespace-enrollment rule. Per the docs: &lt;strong&gt;"ztunnel interferes with Cilium network policy as traffic is encrypted before it leaves the pod, meaning L4 policies won't work except for directly targeting the ztunnel HBONE port (15008)."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're running &lt;a href="https://devtoolhub.com/kubernetes-network-policies/" rel="noopener noreferrer"&gt;Kubernetes NetworkPolicies&lt;/a&gt; or Cilium-native L4 policies to restrict which pods can talk to which ports, enabling ztunnel on a namespace can quietly stop those policies from doing their job — because by the time your policy would evaluate the traffic, it's already encrypted and tunneled through port 15008. This doesn't throw an error. It just means an L4 rule you're relying on may no longer enforce what you think it does. Re-verify your NetworkPolicy behavior after enrolling a namespace, not just your mTLS connectivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cilium mTLS ztunnel: Other Real Limitations (Confirmed in the Docs)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TCP only.&lt;/strong&gt; UDP and other protocols aren't redirected through ztunnel — if a service depends on UDP, it isn't covered by this encryption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace-level enrollment only.&lt;/strong&gt; No per-pod opt-in; everything in an enrolled namespace is in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host-networked pods are skipped.&lt;/strong&gt; No network namespace path means no enrollment — they won't error, they'll just stay unencrypted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requires iptables support in the kernel.&lt;/strong&gt; Minimal container runtimes without iptables can't use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;mTLS secures what's in transit between pods; it doesn't tell you what a compromised pod does once it's running. Pair it with runtime visibility — see our &lt;a href="https://devtoolhub.com/falco-kubernetes-runtime-security/" rel="noopener noreferrer"&gt;Falco on Kubernetes guide&lt;/a&gt; for the detection side of that equation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need to run a SPIRE server for Cilium's native mTLS?&lt;/strong&gt;&lt;br&gt;
A: No. ztunnel generates and manages its own certificates from a bootstrap/CA key pair you create once via a provided OpenSSL script — no external PKI or SPIRE deployment required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Cilium's SPIRE-based Mutual Authentication feature going away?&lt;/strong&gt;&lt;br&gt;
A: Yes. Per the Cilium maintainers' own deprecation issue, it was marked deprecated as of v1.20, with removal "most likely" in v1.21. The agent-side removal code is already merged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I migrate one namespace at a time?&lt;/strong&gt;&lt;br&gt;
A: Technically yes via the namespace label, but only if every namespace that workload talks to is migrated together — traffic between an enrolled and a non-enrolled workload isn't supported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Will my existing NetworkPolicies still work after enabling ztunnel?&lt;/strong&gt;&lt;br&gt;
A: Not necessarily. L4 policies stop enforcing as expected once ztunnel encrypts traffic before it leaves the pod, except for rules that directly target ztunnel's HBONE port (15008). Re-test policy behavior after enrollment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What Cilium version is this in?&lt;/strong&gt;&lt;br&gt;
A: ztunnel Transparent Encryption is Beta as of Cilium 1.20.2, the current stable release at time of writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cilium's older SPIRE-based Mutual Authentication is deprecated as of v1.20 and slated for removal in v1.21 — this isn't optional for long&lt;/li&gt;
&lt;li&gt;The replacement, ztunnel Transparent Encryption, needs no SPIRE server — it generates its own bootstrap/CA certs via a provided script&lt;/li&gt;
&lt;li&gt;Enrollment is per-namespace (&lt;code&gt;io.cilium/mtls-enabled=true&lt;/code&gt;), not per-pod&lt;/li&gt;
&lt;li&gt;Real gotcha #1: traffic between an enrolled and a non-enrolled workload isn't supported — map dependencies before migrating&lt;/li&gt;
&lt;li&gt;Real gotcha #2: L4 NetworkPolicy can silently stop enforcing once ztunnel encrypts traffic, except for rules targeting port 15008&lt;/li&gt;
&lt;li&gt;Cluster Mesh is incompatible with ztunnel and must be disabled first; only TCP is covered, not UDP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cilium mTLS ztunnel is where this migration ends up either way: if you're still running the SPIRE-based mutual auth, the removal timeline makes it a when, not an if — plan the namespace rollout around your actual service dependency graph, and re-test NetworkPolicy behavior before you call it done.&lt;/p&gt;

</description>
      <category>cilium</category>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Grafana Tempo 3.1: Query-Based Trace Redaction Explained</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:20:15 +0000</pubDate>
      <link>https://dev.to/amareswer/grafana-tempo-31-query-based-trace-redaction-explained-2c03</link>
      <guid>https://dev.to/amareswer/grafana-tempo-31-query-based-trace-redaction-explained-2c03</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/grafana-tempo-3-1-whats-new/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Grafana Tempo 3.1 shipped on 2026-09-30 with a feature that matters the moment something goes wrong, not when everything's fine: redaction by query. Before this release, scrubbing a leaked secret out of your traces meant enumerating trace IDs one at a time. Now you can hand Tempo a TraceQL query and it finds and removes every matching trace. Here's the exact command, verified against Grafana's own docs, plus the rolling-upgrade gotcha that can make a "successful" redaction silently incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually New in Grafana Tempo 3.1
&lt;/h2&gt;

&lt;p&gt;Per &lt;a href="https://grafana.com/docs/tempo/latest/release-notes/v3-1/" rel="noopener noreferrer"&gt;Grafana's release notes&lt;/a&gt;, Tempo 3.1's headline changes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redaction by query&lt;/strong&gt; — remove traces matching a TraceQL query instead of enumerating trace IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka ingestion improvements&lt;/strong&gt; — authenticated, TLS-encrypted Kafka connections (SASL &lt;code&gt;SCRAM-SHA-256&lt;/code&gt;, &lt;code&gt;SCRAM-SHA-512&lt;/code&gt;, &lt;code&gt;OAUTHBEARER&lt;/code&gt;, &lt;code&gt;AWS_MSK_IAM&lt;/code&gt;), rack-aware fetching via &lt;code&gt;client_rack&lt;/code&gt; to cut cross-zone transfer costs, and &lt;code&gt;ingest.kafka.producer_compression&lt;/code&gt; for Azure Event Hubs' required gzip compression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TraceQL metrics extrapolation and arithmetic&lt;/strong&gt; — extrapolate rates from head-sampled traces so metrics reflect real traffic volume, and combine aggregations with arithmetic to compute ratios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vParquet5 becomes the default block format.&lt;/strong&gt; Existing vParquet4 blocks stay readable with no migration needed; you can pin the old format with &lt;code&gt;storage.trace.block.version: vParquet4&lt;/code&gt; if you need to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article focuses on redaction, since it's the feature most teams will actually need to reach for under pressure — and the one where getting the details wrong has real consequences. If you're building out observability more broadly, see our guide to &lt;a href="https://devtoolhub.com/ai-log-monitoring-azure/" rel="noopener noreferrer"&gt;real-time log monitoring&lt;/a&gt; for the logs side of the same problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Incident: A Secret Leaked Into a Span Attribute
&lt;/h2&gt;

&lt;p&gt;The scenario this feature exists for: an API key, a password, or a customer PII field ends up in a span attribute — logged by an overly verbose instrumentation library, a debug flag left on, or a bug in a custom span processor. It's now sitting in your trace storage, queryable by anyone with Explore access, until you do something about it.&lt;/p&gt;

&lt;p&gt;Before Tempo 3.1, your options were either live with it until the retention period expired, or find every offending trace ID manually and redact them one by one. Query-based redaction skips that — useful whether you're running Tempo standalone or as part of a full &lt;a href="https://devtoolhub.com/kubernetes-complete-guide/" rel="noopener noreferrer"&gt;Kubernetes observability stack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana Tempo 3.1 Redaction: The Exact Command
&lt;/h2&gt;

&lt;p&gt;Per &lt;a href="https://grafana.com/docs/tempo/latest/operations/tempo_cli/" rel="noopener noreferrer"&gt;Tempo's &lt;code&gt;tempo-cli&lt;/code&gt; documentation&lt;/a&gt;, the redact command has two modes. Trace-ID mode (the old way, still supported, up to 1000 IDs per request):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tempo-cli redact &lt;span class="nt"&gt;--tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TENANT_ID&amp;gt; &lt;span class="nt"&gt;--trace-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TRACE_ID&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;--trace-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TRACE_ID&amp;gt; ...] &amp;lt;scheduler-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query mode (the new way):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tempo-cli redact &lt;span class="nt"&gt;--tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TENANT_ID&amp;gt; &lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;TRACEQL_QUERY&amp;gt; &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;START&amp;gt; &lt;span class="nt"&gt;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;END&amp;gt; &amp;lt;scheduler-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the leaked-API-key scenario, assuming the key landed in a span attribute called &lt;code&gt;http.request.header.authorization&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tempo-cli redact &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{ span.http.request.header.authorization = "sk-live-abc123" }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now-7d &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  tempo-scheduler.prod.svc:9095
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;--dry-run&lt;/code&gt; first — it evaluates the query and reports a match count without rewriting anything. Once the count looks right, drop &lt;code&gt;--dry-run&lt;/code&gt; to actually redact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real constraint worth knowing before you write the query:&lt;/strong&gt; Tempo's docs specify the TraceQL support here is narrower than a normal TraceQL search — redaction "supports only single spanset filters with &lt;code&gt;=&lt;/code&gt; comparisons on &lt;code&gt;resource.*&lt;/code&gt; or &lt;code&gt;span.*&lt;/code&gt; attributes, joined by &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt;." You can't use the full TraceQL grammar (no duration comparisons, no nested spanset logic) — just flat equality checks on resource or span attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Redaction Actually Does — and Doesn't Do
&lt;/h2&gt;

&lt;p&gt;This is the part generic release-note summaries skip: redaction doesn't strip one field out of a trace. Per the docs, the command "rewrites all blocks for a tenant that contain specific trace IDs. The traces are dropped from the new blocks." &lt;strong&gt;The entire matching trace is removed from object storage, not just the offending attribute.&lt;/strong&gt; If you need to keep the rest of the trace and only scrub one field, this feature doesn't do that — it's all-or-nothing at the trace level.&lt;/p&gt;

&lt;p&gt;Tempo's own safety warnings are worth repeating verbatim because they change how you should actually run this in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Redaction rewrites blocks in object storage and can't be undone."&lt;/strong&gt; There's no soft-delete, no recovery window.&lt;/li&gt;
&lt;li&gt;Stop ingesting the sensitive data at its source first — fix the instrumentation bug before you clean up its output, or you're redacting a moving target.&lt;/li&gt;
&lt;li&gt;Wait roughly 10 minutes for current blocks to flush to storage before running the redaction, so you're not racing data still in memory.&lt;/li&gt;
&lt;li&gt;For query-based redaction, verify your match set in Grafana Explore first, then confirm with &lt;code&gt;--dry-run&lt;/code&gt; before dropping it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single pass is never fully complete on a live system.&lt;/strong&gt; Traces still held by block-builders (not yet flushed) aren't covered by a redaction run — you may need a second pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Rolling-Upgrade Gotcha
&lt;/h2&gt;

&lt;p&gt;This is the finding that doesn't show up if you only read the headline feature list. Per Grafana's own 3.1 release notes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Don't submit a windowed redaction until every scheduler and worker is on Tempo 3.1. An older worker ignores the window and redacts every query match in the block, with no error."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translate that: if you're mid-rollout — some Tempo workers upgraded to 3.1, some still on 3.0 — and you submit a redaction with &lt;code&gt;--start&lt;/code&gt;/&lt;code&gt;--end&lt;/code&gt; to limit it to a narrow time window, any worker still running the old version &lt;strong&gt;ignores your time window entirely&lt;/strong&gt; and redacts every trace matching the query across the whole block, with no error or warning telling you it happened. You could intend to scrub one afternoon's worth of traces and instead silently wipe weeks of data, discovering it only when someone asks where their traces went.&lt;/p&gt;

&lt;p&gt;The practical rule: finish the Tempo 3.1 rollout across every scheduler and worker before you run your first windowed redaction. If you're not certain every component is upgraded, run trace-ID-based redaction instead — it doesn't have this failure mode. It's the same discipline covered in our broader list of &lt;a href="https://devtoolhub.com/devops-tools/" rel="noopener noreferrer"&gt;DevOps tools&lt;/a&gt; worth having in your incident-response toolkit before you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Tempo 3.1's redaction remove just the sensitive field, or the whole trace?&lt;/strong&gt;&lt;br&gt;
A: The whole trace. Per Tempo's docs, matching traces are dropped entirely from the rewritten blocks — there's no partial-field redaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I undo a redaction if I made a mistake?&lt;/strong&gt;&lt;br&gt;
A: No. Tempo's docs state explicitly that redaction "rewrites blocks in object storage and can't be undone." Always run &lt;code&gt;--dry-run&lt;/code&gt; first and verify the match count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What TraceQL syntax does the &lt;code&gt;--query&lt;/code&gt; flag actually support?&lt;/strong&gt;&lt;br&gt;
A: A narrower subset than normal TraceQL search: single spanset filters using &lt;code&gt;=&lt;/code&gt; comparisons on &lt;code&gt;resource.*&lt;/code&gt; or &lt;code&gt;span.*&lt;/code&gt; attributes, combined with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt; only. No duration filters, no nested spansets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is it safe to use &lt;code&gt;--start&lt;/code&gt;/&lt;code&gt;--end&lt;/code&gt; windowed redaction during a rolling upgrade?&lt;/strong&gt;&lt;br&gt;
A: No — per Grafana's own warning, a worker still on an older Tempo version ignores the time window and redacts every match in the block with no error. Wait until every scheduler and worker is on 3.1 first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tempo 3.1 (GA'd 2026-09-30) adds redaction by TraceQL query, alongside Kafka auth/TLS improvements, TraceQL metrics arithmetic, and vParquet5 as the new default block format&lt;/li&gt;
&lt;li&gt;The redact command: &lt;code&gt;tempo-cli redact --tenant=&amp;lt;id&amp;gt; --query=&amp;lt;traceql&amp;gt; --start=&amp;lt;t&amp;gt; --end=&amp;lt;t&amp;gt; --dry-run &amp;lt;scheduler-address&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Redaction removes the entire matching trace from storage, not just one attribute, and can't be undone&lt;/li&gt;
&lt;li&gt;Query syntax is limited to flat &lt;code&gt;=&lt;/code&gt; comparisons on &lt;code&gt;resource.*&lt;/code&gt;/&lt;code&gt;span.*&lt;/code&gt; attributes joined by &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;/&lt;code&gt;||&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Don't run windowed (&lt;code&gt;--start&lt;/code&gt;/&lt;code&gt;--end&lt;/code&gt;) redaction mid-rollout — an old worker silently ignores the window and redacts the whole block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grafana Tempo 3.1's redaction feature is worth having ready before you need it — check your Tempo version across every scheduler and worker now, not during the incident where you actually need to use it.&lt;/p&gt;

</description>
      <category>grafana</category>
      <category>observability</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>YAML vs JSON for LLMs: Real Token Counts, Real Gotchas</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:46:16 +0000</pubDate>
      <link>https://dev.to/amareswer/yaml-vs-json-for-llms-real-token-counts-real-gotchas-41o1</link>
      <guid>https://dev.to/amareswer/yaml-vs-json-for-llms-real-token-counts-real-gotchas-41o1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/yaml-vs-json-for-llms/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;YAML vs JSON for LLMs isn't just a style preference — it changes how many tokens you pay for on every request. I ran both formats through a real tokenizer on identical data instead of repeating the "20-ish percent" figures floating around online, and the gap is bigger than you'd guess, with one real parsing bug that can silently corrupt an LLM's output if you're not careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML vs JSON for LLMs: The Real Numbers, Measured
&lt;/h2&gt;

&lt;p&gt;Most "YAML saves tokens" claims cite a number without showing their work. So I built two test cases — a nested config object and a 10-row uniform table — and ran both formats through OpenAI's &lt;code&gt;cl100k_base&lt;/code&gt; tokenizer (the GPT-4-era encoding, a standard reference point; exact counts shift slightly on Claude's or Gemini's own tokenizers, but the ranking between formats doesn't).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nested config (not a list — a single object with child objects):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;322&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;223&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;YAML cut &lt;strong&gt;33.3%&lt;/strong&gt; of the tokens off the same config, just by dropping braces, commas, and quoted keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uniform table (10 rows, 4 fields each — the shape most AI agent data actually takes: user lists, logs, tool outputs):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Characters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;323&lt;/td&gt;
&lt;td&gt;963&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;590&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOON&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;235&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here the picture changes. YAML still beats JSON by &lt;strong&gt;31.6%&lt;/strong&gt;, but &lt;a href="https://devtoolhub.com/toon-vs-json-token-efficient-ai-format/" rel="noopener noreferrer"&gt;TOON&lt;/a&gt; — a format built specifically for this case — beats JSON by &lt;strong&gt;70%&lt;/strong&gt; and beats YAML by &lt;strong&gt;56.1%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason is structural, not cosmetic. YAML's &lt;code&gt;- id: 1\n  name: Alice&lt;/code&gt; syntax repeats every field name on every row. TOON declares the field list once (&lt;code&gt;users[10]{id,name,role,active}:&lt;/code&gt;) and streams bare values after it. For a one-off config file that repetition barely matters. For a 500-row tool result going into an agent's context window on every turn, it adds up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML vs JSON vs TOON: Which Should You Use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested, non-uniform data&lt;/strong&gt; (app config, a single object with child objects): YAML and TOON are nearly identical here — TOON's spec explicitly borrows YAML's indentation syntax for non-tabular data, so there's no real token advantage to switching formats for this case. Pick YAML; it's the one more tools already read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniform arrays of objects&lt;/strong&gt; (user lists, log lines, query results, tool outputs): TOON is the clear winner. This is specifically the case it was designed to compress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything hitting an API, a database, or existing software&lt;/strong&gt;: JSON. It's universal, strict, and every library expects it. Don't reach for YAML or TOON here — convert at the LLM boundary, not before it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical pattern: keep your actual system in JSON, and only convert to YAML or TOON for the text you hand to the model — then have the model hand JSON back for anything that needs to go back into code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The YAML Gotcha Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;YAML's compactness comes from a looser grammar than JSON's, and that looseness has a well-documented failure mode: the &lt;strong&gt;Norway problem&lt;/strong&gt;. In YAML 1.1 (still what many libraries implement, including older PyYAML), bare words like &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, and &lt;code&gt;off&lt;/code&gt; parse as booleans, not strings. A country-code list like:&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;geoblock_regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dk&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;fi&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;is&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;se&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;silently becomes &lt;code&gt;["dk", "fi", "is", false, "se"]&lt;/code&gt; — Norway (&lt;code&gt;no&lt;/code&gt;) turns into &lt;code&gt;False&lt;/code&gt;. Version-looking strings have the same problem: &lt;code&gt;10.23&lt;/code&gt; parses as the float &lt;code&gt;10.23&lt;/code&gt;, not the string &lt;code&gt;"10.23"&lt;/code&gt;, which breaks version-comparison logic that expected text.&lt;/p&gt;

&lt;p&gt;This isn't theoretical for this site's audience specifically. GitHub Actions workflow files use &lt;code&gt;on:&lt;/code&gt; as a top-level key (&lt;code&gt;on: push&lt;/code&gt;). Per a &lt;a href="https://github.com/go-yaml/yaml/issues/523" rel="noopener noreferrer"&gt;real bug report against the Go YAML library&lt;/a&gt;, a generic YAML 1.1 parser reads that key as the boolean &lt;code&gt;true&lt;/code&gt;, not the string &lt;code&gt;"on"&lt;/code&gt; — so any tooling that parses workflow YAML with a non-GitHub-aware library can silently drop or mishandle the trigger config. GitHub's own runner handles &lt;code&gt;on:&lt;/code&gt; correctly because it's YAML-1.2-aware, but if you're writing your own workflow linter, generator, or an LLM-based tool that reads &lt;code&gt;.github/workflows/*.yml&lt;/code&gt;, this is exactly the kind of bug that passes code review and fails in production. See our &lt;a href="https://devtoolhub.com/github-actions-workflow-yaml-guide/" rel="noopener noreferrer"&gt;GitHub Actions YAML guide&lt;/a&gt; for the rest of the syntax.&lt;/p&gt;

&lt;p&gt;The fix, per that same thread, is one of two things: quote the key (&lt;code&gt;"on": push&lt;/code&gt;) or use a YAML 1.2-compliant parser. &lt;a href="https://yaml.org/spec/1.2.2/" rel="noopener noreferrer"&gt;YAML 1.2.2&lt;/a&gt; — the current spec, revised 2021-10-01 — dropped the bare &lt;code&gt;on&lt;/code&gt;/&lt;code&gt;off&lt;/code&gt;/&lt;code&gt;yes&lt;/code&gt;/&lt;code&gt;no&lt;/code&gt; boolean shortcuts specifically because of bugs like this one. If you're feeding YAML into an LLM and expect it to reliably reason about or regenerate that YAML, this gotcha matters doubly: the model has almost certainly also been trained on YAML 1.1-style parsing assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML vs JSON for LLMs: Why Token Count Matters More for Agents
&lt;/h2&gt;

&lt;p&gt;A single chat message where YAML saves 60 tokens over JSON is immaterial. It compounds somewhere specific: &lt;a href="https://devtoolhub.com/ai-agents-explained/" rel="noopener noreferrer"&gt;AI agents&lt;/a&gt; that re-send accumulated context — conversation history, tool outputs, retrieved documents — on every turn. If an agent calls a tool 20 times in a session and each tool result is a JSON array of rows, you're paying the JSON tax 20 times over, not once. That's the same economics covered in our &lt;a href="https://devtoolhub.com/ai-prompt-compression-guide/" rel="noopener noreferrer"&gt;prompt compression guide&lt;/a&gt;: the lever that matters is the one you pull on every request, not the one you pull once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is YAML actually faster for an LLM to parse, or just fewer tokens?&lt;/strong&gt;&lt;br&gt;
A: This article only measured token count, which is the cost and context-window lever. Parsing speed is a separate question and depends on the runtime, not the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does TOON replace YAML?&lt;/strong&gt;&lt;br&gt;
A: No — they overlap only on uniform tabular data. For nested, irregular structures (most real config files), TOON's own spec uses YAML-style indentation, so there's no efficiency gain from switching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does "no" become false in YAML?&lt;/strong&gt;&lt;br&gt;
A: YAML 1.1 treats a set of bare words (&lt;code&gt;y&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;) as boolean literals when unquoted. YAML 1.2.2 removed this behavior from the spec, but many parsers (older PyYAML, Go's &lt;code&gt;go-yaml&lt;/code&gt; v2) still implement 1.1 rules for compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I quote every YAML value to avoid this?&lt;/strong&gt;&lt;br&gt;
A: Quote any value that could be misread as a different type — country codes, version strings, and anything resembling &lt;code&gt;yes&lt;/code&gt;/&lt;code&gt;no&lt;/code&gt;/&lt;code&gt;on&lt;/code&gt;/&lt;code&gt;off&lt;/code&gt;. For a config key like GitHub Actions' &lt;code&gt;on:&lt;/code&gt;, quote the key itself: &lt;code&gt;"on": push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measured with a real tokenizer: YAML saves ~30-33% of tokens vs JSON on both nested config and tabular data&lt;/li&gt;
&lt;li&gt;TOON pulls further ahead specifically on uniform tabular data — 70% fewer tokens than JSON, 56% fewer than YAML, in this test&lt;/li&gt;
&lt;li&gt;For nested, non-uniform data, YAML and TOON are about the same size — pick YAML for compatibility&lt;/li&gt;
&lt;li&gt;The Norway problem (&lt;code&gt;no&lt;/code&gt; → &lt;code&gt;false&lt;/code&gt;) is real and has broken actual tooling, including GitHub Actions' &lt;code&gt;on:&lt;/code&gt; key in non-GitHub-aware parsers&lt;/li&gt;
&lt;li&gt;JSON still wins for anything touching an API or database — convert formats only at the LLM boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're weighing YAML vs JSON for LLMs in your own pipeline, the honest answer is "it depends on the shape of the data," not "YAML always wins" — measure your own payloads with a tokenizer before committing, and watch for unquoted values that look like booleans.&lt;/p&gt;

</description>
      <category>yaml</category>
      <category>json</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Terraform State Explained: Locking, Backups, and Recovery</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:05:19 +0000</pubDate>
      <link>https://dev.to/amareswer/terraform-state-explained-locking-backups-and-recovery-8h3</link>
      <guid>https://dev.to/amareswer/terraform-state-explained-locking-backups-and-recovery-8h3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/terraform-state-explained/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Terraform state causes more real incidents than almost anything else in the tool — a stuck lock, a corrupted file, a &lt;code&gt;state rm&lt;/code&gt; that touched the wrong resource. Most of the anxiety is unnecessary once you know two things most tutorials skip: Terraform backs up state automatically before every change, and it can't be turned off. Here's terraform state explained with the commands that actually matter, verified against HashiCorp's own docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform State Backups: The One You Didn't Know You Had
&lt;/h2&gt;

&lt;p&gt;Per &lt;a href="https://developer.hashicorp.com/terraform/language/state" rel="noopener noreferrer"&gt;Terraform's state documentation&lt;/a&gt;, Terraform "stores each workspace's state in a local file named &lt;code&gt;terraform.tfstate&lt;/code&gt;, and a backup of the previous state in &lt;code&gt;terraform.tfstate.backup&lt;/code&gt;." That backup isn't optional. Per the &lt;a href="https://developer.hashicorp.com/terraform/cli/commands/state" rel="noopener noreferrer"&gt;state command reference&lt;/a&gt;, every command that modifies state "write[s] backup files," and backups for state modification "can not be disabled" — HashiCorp's own words, because of how sensitive state content is.&lt;/p&gt;

&lt;p&gt;Practically: before you panic about a bad &lt;code&gt;terraform state rm&lt;/code&gt; or &lt;code&gt;state mv&lt;/code&gt;, check for &lt;code&gt;terraform.tfstate.backup&lt;/code&gt; first. It's sitting right next to your state file, one command edit behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform State Commands That Actually Matter
&lt;/h2&gt;

&lt;p&gt;Six subcommands live under &lt;code&gt;terraform state&lt;/code&gt;, and you'll use maybe three of them regularly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists every resource currently tracked in state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state show &amp;lt;resource&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows the full recorded attributes for one resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state mv &amp;lt;src&amp;gt; &amp;lt;dst&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Moves a resource to a new address in state, without touching real infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state rm &amp;lt;resource&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes a resource from state tracking, without destroying it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state pull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieves and prints the raw remote state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform state replace-provider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates which provider a resource is associated with&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; and &lt;code&gt;rm&lt;/code&gt; are the two people reach for under pressure — renaming a resource in code without triggering a destroy/recreate, or un-tracking something Terraform shouldn't manage anymore. Both are state-only operations: nothing in your real infrastructure changes. That's also exactly why the automatic backup exists — a bad &lt;code&gt;mv&lt;/code&gt; or &lt;code&gt;rm&lt;/code&gt; is a state-file mistake, and the backup is how you undo it.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Locking: The DynamoDB Pattern Is Deprecated
&lt;/h2&gt;

&lt;p&gt;If you learned Terraform's S3 backend from an older tutorial, it told you to provision a DynamoDB table for state locking. That guidance is now out of date. Per the &lt;a href="https://developer.hashicorp.com/terraform/language/backend/s3" rel="noopener noreferrer"&gt;S3 backend docs&lt;/a&gt;, DynamoDB-based locking is explicitly deprecated: "Locking can be enabled via S3 or DynamoDB. However, DynamoDB-based locking is deprecated and will be removed in a future minor version."&lt;/p&gt;

&lt;p&gt;The current approach is native S3 locking, enabled with one config key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
    &lt;span class="nx"&gt;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# native S3 locking — no DynamoDB table needed&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;The lock lives alongside your state as a file with a &lt;code&gt;.tflock&lt;/code&gt; suffix. If you're still running a DynamoDB table purely for Terraform locking, it's no longer required — &lt;code&gt;use_lockfile = true&lt;/code&gt; replaces the &lt;code&gt;dynamodb_table&lt;/code&gt; setting, and one less piece of infrastructure to maintain and pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Lock Won't Release: force-unlock
&lt;/h2&gt;

&lt;p&gt;A crashed &lt;code&gt;apply&lt;/code&gt;, a killed CI job, or a network drop mid-run can leave a lock in place with nothing actually running. Per the &lt;a href="https://developer.hashicorp.com/terraform/cli/commands/force-unlock" rel="noopener noreferrer"&gt;force-unlock reference&lt;/a&gt;, the command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform force-unlock [options] LOCK_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important line in HashiCorp's own docs: "This command does not modify your infrastructure." It only removes the lock record — it doesn't touch state content or real resources, which is why it's safe to run once you've confirmed nothing else is actually mid-apply. One real caveat worth knowing before you reach for it: "Local state files cannot be unlocked by another process" — &lt;code&gt;force-unlock&lt;/code&gt; is a remote-backend feature. If you're on purely local state, a stuck lock means finding and clearing the lock file yourself, not running this command.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Recovery Workflow
&lt;/h2&gt;

&lt;p&gt;Put together, here's the actual sequence for the two most common state incidents:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I ran &lt;code&gt;terraform state rm&lt;/code&gt; on the wrong resource."&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't run &lt;code&gt;terraform apply&lt;/code&gt; yet — that's what would actually recreate or destroy something based on the now-wrong state.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;terraform.tfstate.backup&lt;/code&gt; — it has the pre-&lt;code&gt;rm&lt;/code&gt; version.&lt;/li&gt;
&lt;li&gt;Restore the specific resource block from the backup, or use &lt;code&gt;terraform state push&lt;/code&gt; with a corrected copy if you're on a remote backend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;"A lock won't release and nothing is actually running."&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm no CI job or teammate has an apply genuinely in progress — check your CI dashboard, not just your own terminal.&lt;/li&gt;
&lt;li&gt;Get the lock ID from the error message Terraform printed when the lock conflict occurred.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform force-unlock LOCK_ID&lt;/code&gt;. Remember this doesn't work for purely local state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither of these requires touching real infrastructure. That's the point of how Terraform separates state operations from apply operations — it's worth knowing before the 2am incident, not during it.&lt;/p&gt;

&lt;p&gt;For the tool that automates a lot of this multi-environment state and backend setup, our &lt;a href="https://devtoolhub.com/terragrunt-vs-terraform/" rel="noopener noreferrer"&gt;Terragrunt vs Terraform guide&lt;/a&gt; covers &lt;code&gt;remote_state&lt;/code&gt; blocks that auto-generate backend config like the one above. If you're deciding between engines entirely, our &lt;a href="https://devtoolhub.com/opentofu-vs-terraform/" rel="noopener noreferrer"&gt;OpenTofu vs Terraform guide&lt;/a&gt; and our look at &lt;a href="https://devtoolhub.com/opentofu-1-13-whats-new/" rel="noopener noreferrer"&gt;what's new in OpenTofu 1.13&lt;/a&gt; cover the state-encryption feature Terraform doesn't have natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Terraform back up state automatically?&lt;/strong&gt;&lt;br&gt;
A: Yes. Per HashiCorp's own docs, every state-modifying command writes a backup to &lt;code&gt;terraform.tfstate.backup&lt;/code&gt;, and this cannot be disabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I still need a DynamoDB table for Terraform state locking?&lt;/strong&gt;&lt;br&gt;
A: No, not for new setups. DynamoDB-based S3 locking is deprecated. Use &lt;code&gt;use_lockfile = true&lt;/code&gt; in your S3 backend config for native locking instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does terraform force-unlock destroy anything?&lt;/strong&gt;&lt;br&gt;
A: No. Per HashiCorp's docs, it explicitly does not modify your infrastructure — it only removes the lock record. It also doesn't work on local (non-remote) state files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the difference between terraform state rm and terraform destroy?&lt;/strong&gt;&lt;br&gt;
A: &lt;code&gt;state rm&lt;/code&gt; only removes a resource from Terraform's tracking — the real infrastructure is untouched. &lt;code&gt;destroy&lt;/code&gt; actually deletes the real resource. They're easy to confuse in a panic; &lt;code&gt;state rm&lt;/code&gt; is the safe one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Where is Terraform state stored by default?&lt;/strong&gt;&lt;br&gt;
A: Locally in &lt;code&gt;terraform.tfstate&lt;/code&gt; unless a remote backend (S3, HCP Terraform, etc.) is configured. HashiCorp recommends a remote backend for anything beyond solo experimentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform state modification commands (&lt;code&gt;state mv&lt;/code&gt;, &lt;code&gt;state rm&lt;/code&gt;) always write an undeletable backup to &lt;code&gt;terraform.tfstate.backup&lt;/code&gt; first&lt;/li&gt;
&lt;li&gt;DynamoDB-based S3 state locking is deprecated — use &lt;code&gt;use_lockfile = true&lt;/code&gt; for native S3 locking instead&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform force-unlock LOCK_ID&lt;/code&gt; removes a stuck lock without touching infrastructure, but doesn't work on local state&lt;/li&gt;
&lt;li&gt;The safe recovery path for a bad state edit is the &lt;code&gt;.tfstate.backup&lt;/code&gt; file, not &lt;code&gt;terraform apply&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state rm&lt;/code&gt; and &lt;code&gt;terraform destroy&lt;/code&gt; are not the same thing — only one of them touches real infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check &lt;code&gt;terraform state list&lt;/code&gt; before any bulk state operation — confirming what's actually tracked takes ten seconds and prevents the kind of terraform state mistake that needs a recovery workflow in the first place.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>iac</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Terragrunt vs Terraform: What's Actually Different</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:11:46 +0000</pubDate>
      <link>https://dev.to/amareswer/terragrunt-vs-terraform-whats-actually-different-can</link>
      <guid>https://dev.to/amareswer/terragrunt-vs-terraform-whats-actually-different-can</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/terragrunt-vs-terraform/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Terragrunt isn't a Terraform competitor. It's a thin wrapper that runs on top of OpenTofu or Terraform, and the confusion about what it actually adds is real enough that "terragrunt vs terraform workspaces" and "terragrunt vs terraform modules" are both real searches. Here's what Terragrunt actually does, verified against its current docs and a real CLI run — including one command-syntax change that will break a tutorial written more than a year ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terragrunt vs Terraform: What Terragrunt Actually Adds
&lt;/h2&gt;

&lt;p&gt;Per &lt;a href="https://docs.terragrunt.com/getting-started/overview/" rel="noopener noreferrer"&gt;Terragrunt's own overview&lt;/a&gt;, its job is solving code duplication across multiple environments and regions. A team running the same infrastructure in dev, staging, and prod normally either copies the same &lt;code&gt;.tf&lt;/code&gt; files three times or reaches for workspaces. Terragrunt's answer is four features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DRY configuration.&lt;/strong&gt; An &lt;code&gt;include&lt;/code&gt; block lets every environment inherit shared provider and backend settings from one root config, instead of repeating them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote state generation.&lt;/strong&gt; A &lt;code&gt;remote_state&lt;/code&gt; block auto-generates the &lt;code&gt;backend.tf&lt;/code&gt; file before every run, so nobody hand-writes backend config per environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module dependencies.&lt;/strong&gt; A &lt;code&gt;dependency&lt;/code&gt; block lets one unit read another unit's outputs — Terragrunt runs &lt;code&gt;terragrunt output&lt;/code&gt; on the dependency first, then feeds those values in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency-ordered &lt;code&gt;run --all&lt;/code&gt;.&lt;/strong&gt; Terragrunt builds a directed acyclic graph from your &lt;code&gt;dependency&lt;/code&gt; blocks and runs commands across every unit in the correct order automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this replaces OpenTofu or Terraform. Terragrunt still shells out to one of them for every actual plan/apply — it's an orchestration layer, not a second IaC engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command That Changed: run-all Is Gone
&lt;/h2&gt;

&lt;p&gt;If you learned Terragrunt from an older tutorial, this will break on you. Terragrunt went through a &lt;a href="https://docs.terragrunt.com/migrate/cli-redesign/" rel="noopener noreferrer"&gt;CLI redesign&lt;/a&gt; that folded the old &lt;code&gt;run-all&lt;/code&gt; and &lt;code&gt;graph&lt;/code&gt; commands into a single &lt;code&gt;run&lt;/code&gt; command with flags. We tested this directly on Terragrunt 1.1.6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ terragrunt run --all plan --non-interactive
INFO  The following units will be run, starting with dependencies and then their dependents:
.
╰── .
[.] tofu: No changes. Your infrastructure matches the configuration.

❯❯ Run Summary  1 units  564ms
   Succeeded    1
&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;$ terragrunt run-all plan --non-interactive
ERROR unknown command: "run-all". Terragrunt no longer forwards unknown commands by
default. Use 'terragrunt run -- run-all ...' or a supported shortcut.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a deprecation warning — on a current install, the old syntax is a hard error. The fix is mechanical: &lt;code&gt;terragrunt run-all apply&lt;/code&gt; becomes &lt;code&gt;terragrunt run --all apply&lt;/code&gt;, and every other &lt;code&gt;run-all &amp;lt;command&amp;gt;&lt;/code&gt; invocation follows the same pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terragrunt vs Terraform vs OpenTofu: Which Engine Runs
&lt;/h2&gt;

&lt;p&gt;Terragrunt doesn't force a choice between OpenTofu and Terraform. Per its &lt;a href="https://docs.terragrunt.com/features/units/engine/" rel="noopener noreferrer"&gt;engine documentation&lt;/a&gt;, without the experimental Engine feature enabled, "Terragrunt will determine how IaC updates will be performed by doing things like invoking the &lt;code&gt;tofu&lt;/code&gt;/&lt;code&gt;terraform&lt;/code&gt; binary directly" — it uses whichever binary it finds. We confirmed this: with only &lt;code&gt;tofu&lt;/code&gt; installed and no &lt;code&gt;terraform&lt;/code&gt; binary on the test machine, &lt;code&gt;terragrunt init&lt;/code&gt; correctly invoked &lt;code&gt;tofu&lt;/code&gt; for every step, and Terragrunt's own &lt;code&gt;--help&lt;/code&gt; output labels its command shortcuts "OpenTofu shortcuts" first.&lt;/p&gt;

&lt;p&gt;If you need to pin a specific engine explicitly rather than relying on autodetection, that's what the experimental &lt;code&gt;engine&lt;/code&gt; block is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# requires: export TG_EXPERIMENTAL_ENGINE=1&lt;/span&gt;
&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/gruntwork-io/terragrunt-engine-opentofu"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v0.1.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most setups, autodetection is enough — install whichever binary you want (&lt;code&gt;tofu&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt;), and Terragrunt uses it without extra configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal DRY Setup
&lt;/h2&gt;

&lt;p&gt;This is the actual pattern the DRY features exist for — one root config, environment-specific units that inherit from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;terragrunt&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hcl&lt;/span&gt;          &lt;span class="c1"&gt;# root: shared provider + remote_state config&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;terragrunt&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hcl&lt;/span&gt;      &lt;span class="c1"&gt;# include { path = find_in_parent_folders() }&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;staging&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;terragrunt&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hcl&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;
    &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;terragrunt&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hcl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each environment's &lt;code&gt;terragrunt.hcl&lt;/code&gt; is a few lines — an &lt;code&gt;include&lt;/code&gt; block plus whatever's actually different about that environment (instance size, region, a different &lt;code&gt;dependency&lt;/code&gt; output). The provider block, backend config, and remote state setup live once, in the root file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Terragrunt Doesn't Solve
&lt;/h2&gt;

&lt;p&gt;Terragrunt adds a real dependency to your toolchain and a second config language layer (HCL generating HCL) on top of the one you already have. Two things worth knowing before adopting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's still Gruntwork's tool.&lt;/strong&gt; Per its docs, Terragrunt is maintained by Gruntwork, not a vendor-neutral foundation the way OpenTofu is under the Linux Foundation. That's not a red flag — it's a mature, 9.9k-star project with paid enterprise support available — but it's a different governance model than the engine it wraps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Engine feature (explicit OpenTofu/Terraform pinning) is still experimental&lt;/strong&gt;, gated behind &lt;code&gt;TG_EXPERIMENTAL_ENGINE=1&lt;/code&gt;. Autodetection covers the common case; don't build a workflow around the pinning syntax expecting it to be stable yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your actual problem is "I have one root module and want per-environment variable values," Terraform/OpenTofu's built-in &lt;code&gt;.tfvars&lt;/code&gt; files or workspaces solve that without adding a new tool. Terragrunt earns its place once you have multiple modules with real dependencies between them, or enough environments that copy-pasted backend blocks have become their own maintenance problem.&lt;/p&gt;

&lt;p&gt;If you haven't settled the engine question yet, our &lt;a href="https://devtoolhub.com/opentofu-vs-terraform/" rel="noopener noreferrer"&gt;OpenTofu vs Terraform guide&lt;/a&gt; covers licensing and compatibility in more depth — Terragrunt works the same way on top of either. If you're documenting whichever module structure you land on, &lt;a href="https://devtoolhub.com/terraform-docs-github-action/" rel="noopener noreferrer"&gt;our terraform-docs GitHub Action guide&lt;/a&gt; covers keeping that in sync in CI, and our &lt;a href="https://devtoolhub.com/terraform-helm-provider-migration/" rel="noopener noreferrer"&gt;Terraform Helm provider migration guide&lt;/a&gt; covers a real breaking-change migration if Helm is part of your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Terragrunt a replacement for Terraform or OpenTofu?&lt;/strong&gt;&lt;br&gt;
A: No. Terragrunt is an orchestration wrapper — it still invokes the &lt;code&gt;tofu&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt; binary for every actual infrastructure change. It doesn't have its own execution engine outside the experimental Engine feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does terragrunt run-all still work?&lt;/strong&gt;&lt;br&gt;
A: No, not on current versions. We tested it directly on Terragrunt 1.1.6 and got &lt;code&gt;ERROR unknown command: "run-all"&lt;/code&gt;. Use &lt;code&gt;terragrunt run --all &amp;lt;command&amp;gt;&lt;/code&gt; instead — the CLI redesign folded &lt;code&gt;run-all&lt;/code&gt; into &lt;code&gt;run&lt;/code&gt; as a flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Terragrunt vs Terraform workspaces — what's the actual difference?&lt;/strong&gt;&lt;br&gt;
A: Workspaces let one set of &lt;code&gt;.tf&lt;/code&gt; files manage multiple named state instances from the same config. Terragrunt instead uses separate directories (units) per environment, each with its own &lt;code&gt;remote_state&lt;/code&gt; block and its own real backend, inheriting shared settings via &lt;code&gt;include&lt;/code&gt;. Workspaces are simpler for small setups; Terragrunt's directory-per-environment model scales better once environments need genuinely different configuration, not just different variable values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Terragrunt work with OpenTofu?&lt;/strong&gt;&lt;br&gt;
A: Yes, and its CLI help labels the primary command shortcuts "OpenTofu shortcuts." Without the experimental Engine feature, Terragrunt auto-detects and invokes whichever of &lt;code&gt;tofu&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt; is on your system PATH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need Terragrunt if I'm already using OpenTofu?&lt;/strong&gt;&lt;br&gt;
A: Only if you're managing multiple environments or modules with real dependencies between them. OpenTofu alone is enough for a single module with &lt;code&gt;.tfvars&lt;/code&gt;-based environment differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terragrunt is an orchestration wrapper, not an alternative to OpenTofu/Terraform — it still shells out to one of them for every actual change&lt;/li&gt;
&lt;li&gt;Core features: DRY config via &lt;code&gt;include&lt;/code&gt;, auto-generated remote state, &lt;code&gt;dependency&lt;/code&gt; blocks for cross-module outputs, and dependency-ordered &lt;code&gt;run --all&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terragrunt run-all&lt;/code&gt; is gone on current versions — confirmed via direct test, it's now a hard error, not a warning. Use &lt;code&gt;terragrunt run --all &amp;lt;command&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Terragrunt auto-detects &lt;code&gt;tofu&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt; on PATH; explicit engine pinning is still an experimental feature&lt;/li&gt;
&lt;li&gt;Worth adopting once you have multiple environments or modules with real dependencies; skip it for a single module that just needs different variable values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check &lt;code&gt;docs.terragrunt.com&lt;/code&gt; before pinning a Terragrunt version in CI — the CLI redesign is still an active migration, and more &lt;code&gt;run-all&lt;/code&gt;-style syntax changes could land before it's fully settled.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>opentofu</category>
      <category>devops</category>
      <category>iac</category>
    </item>
    <item>
      <title>OpenTofu 1.13: What's New and What Terraform Still Doesn't Have</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:43:15 +0000</pubDate>
      <link>https://dev.to/amareswer/opentofu-113-whats-new-and-what-terraform-still-doesnt-have-4g49</link>
      <guid>https://dev.to/amareswer/opentofu-113-whats-new-and-what-terraform-still-doesnt-have-4g49</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/opentofu-1-13-whats-new/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenTofu 1.13.0 shipped on September 30, 2026. Officially-supported Windows on ARM64, a set of new functions, and upgrades to the state-encryption system it shipped ahead of Terraform. Here's what actually changed, verified against the official release changelog, plus a close look at state encryption itself — still the single biggest feature gap between OpenTofu and Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually New in OpenTofu 1.13
&lt;/h2&gt;

&lt;p&gt;Per the &lt;a href="https://github.com/opentofu/opentofu/blob/v1.13/CHANGELOG.md" rel="noopener noreferrer"&gt;official v1.13 changelog&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows ARM64 is now officially supported.&lt;/strong&gt; Previously unofficial or community-built; OpenTofu now ships official binaries for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New &lt;code&gt;convert&lt;/code&gt; function&lt;/strong&gt; for type conversion, plus a set of functions with an &lt;code&gt;assume...&lt;/code&gt; prefix that "give OpenTofu additional hints about what's expected as the final result of an unknown value" — useful for reducing unknown-value noise in &lt;code&gt;plan&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linting support (experimental)&lt;/strong&gt;, enabled with a &lt;code&gt;-lint&lt;/code&gt; flag on supported commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbol libraries (experimental)&lt;/strong&gt; — a new capability to define reusable functions and types in hcl-lang based libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unicode 17&lt;/strong&gt; for all string processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan files now include the provider schemas needed to render the plan&lt;/strong&gt;, which matters if you consume &lt;code&gt;.tfplan&lt;/code&gt; files outside the CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Breaks in OpenTofu 1.13: Upgrade Notes
&lt;/h2&gt;

&lt;p&gt;Three things worth checking before you upgrade a CI pipeline to 1.13:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;winrm&lt;/code&gt; provisioner connection type is removed.&lt;/strong&gt; If anything in your config still uses WinRM for Windows provisioning, it's gone — OpenSSH is the supported path on modern Windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;base64gzip&lt;/code&gt; output changed.&lt;/strong&gt; Results are "equivalent to but not equal to" previous output, due to optimized DEFLATE compression — don't diff old and new &lt;code&gt;base64gzip&lt;/code&gt; output expecting a byte match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS 13 Ventura is now the minimum.&lt;/strong&gt; And 1.13 is explicitly called out as the &lt;em&gt;last&lt;/em&gt; release series shipping official 32-bit CPU builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic — they're the kind of thing that silently breaks a pipeline that hasn't been touched in a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feature Terraform Still Doesn't Have: State Encryption
&lt;/h2&gt;

&lt;p&gt;OpenTofu's state and plan encryption isn't new to 1.13 — it shipped in an earlier release, and 1.13 extends it — but it remains the clearest structural difference between OpenTofu and Terraform. Per &lt;a href="https://opentofu.org/docs/language/state/encryption/" rel="noopener noreferrer"&gt;OpenTofu's encryption docs&lt;/a&gt;, it's stable, GA functionality, not experimental: "If an attacker were to gain access to your state file, they should not be able to read it and use the sensitive values (e.g. access keys) contained in the state file."&lt;/p&gt;

&lt;p&gt;The config lives inside the &lt;code&gt;terraform&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;encryption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key_provider&lt;/span&gt; &lt;span class="s2"&gt;"pbkdf2"&lt;/span&gt; &lt;span class="s2"&gt;"mykey"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;passphrase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state_passphrase&lt;/span&gt;  &lt;span class="c1"&gt;# minimum 16 characters&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="s2"&gt;"aes_gcm"&lt;/span&gt; &lt;span class="s2"&gt;"mymethod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;key_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pbkdf2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mykey&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_gcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mymethod&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_gcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mymethod&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported key providers go beyond a passphrase: AWS KMS, GCP KMS, Azure Vault, OpenBao's Transit Secret Engine (compatible with HashiCorp Vault 1.14), and a generic external-program option for anything else. 1.13 specifically extended this: GCP KMS gained &lt;code&gt;additional_authenticated_data&lt;/code&gt; support, AWS KMS gained an &lt;code&gt;encryption_context&lt;/code&gt; field, and OpenBao gained an &lt;code&gt;associated_data&lt;/code&gt; argument — all ways to bind the encryption to additional context so a stolen key alone isn't enough to decrypt a state file from a different environment.&lt;/p&gt;

&lt;p&gt;Terraform has no equivalent built-in feature. State-at-rest protection in Terraform means encrypting the storage backend itself (S3 bucket encryption, for instance) — the state &lt;em&gt;content&lt;/em&gt; isn't encrypted by Terraform's own logic the way OpenTofu's &lt;code&gt;encryption&lt;/code&gt; block does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Quiet But Real Bug Fixes
&lt;/h2&gt;

&lt;p&gt;Two fixes in 1.13 are worth knowing even if they don't affect your specific setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connection.script_path&lt;/code&gt; escaping now prevents command injection.&lt;/strong&gt; If a provisioner script path was ever built from a variable, this closes a real injection vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH proxy connection errors now report properly&lt;/strong&gt; instead of crashing the run — previously a connection failure could crash rather than surface a readable error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should You Upgrade Now?
&lt;/h2&gt;

&lt;p&gt;If you're not using WinRM provisioners and you're on macOS 13+ already, there's no real reason to hold back — the breaking changes are narrow and well-documented. If you're on an older macOS or a 32-bit build target, plan the migration now: 1.13 is the last release series supporting 32-bit builds, and there won't be a grace period on the next major bump.&lt;/p&gt;

&lt;p&gt;For a broader look at whether OpenTofu is the right call over Terraform for your team at all — licensing, provider registry compatibility, migration steps — see our &lt;a href="https://devtoolhub.com/opentofu-vs-terraform/" rel="noopener noreferrer"&gt;OpenTofu vs Terraform guide&lt;/a&gt;. And if Terragrunt is part of your stack, it works the same way on top of either engine — see our &lt;a href="https://devtoolhub.com/terragrunt-vs-terraform/" rel="noopener noreferrer"&gt;Terragrunt vs Terraform guide&lt;/a&gt; for what it actually adds. If you're keeping module docs in sync as part of that CI pipeline, our &lt;a href="https://devtoolhub.com/terraform-docs-github-action/" rel="noopener noreferrer"&gt;terraform-docs GitHub Action guide&lt;/a&gt; covers automating that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: When did OpenTofu 1.13 release?&lt;/strong&gt;&lt;br&gt;
A: September 30, 2026, per the official GitHub releases page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does OpenTofu 1.13 support Windows on ARM64?&lt;/strong&gt;&lt;br&gt;
A: Yes, officially for the first time in this release, per the changelog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is OpenTofu's state encryption a new feature in 1.13?&lt;/strong&gt;&lt;br&gt;
A: No — it's an existing, stable feature that 1.13 extended with additional key-provider options (GCP KMS authenticated data, AWS KMS encryption context, OpenBao associated data). The core encryption capability predates this release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Terraform have state encryption like OpenTofu?&lt;/strong&gt;&lt;br&gt;
A: Not as a built-in feature. Terraform relies on backend-level encryption (like S3 bucket encryption) rather than encrypting state file contents itself the way OpenTofu's &lt;code&gt;encryption&lt;/code&gt; block does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What breaks when upgrading to OpenTofu 1.13?&lt;/strong&gt;&lt;br&gt;
A: The &lt;code&gt;winrm&lt;/code&gt; provisioner connection type is removed, &lt;code&gt;base64gzip&lt;/code&gt; output changed (not byte-identical to older output), and the minimum macOS version is now 13 Ventura.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenTofu 1.13.0 released September 30, 2026: official Windows ARM64 support, new &lt;code&gt;convert&lt;/code&gt;/&lt;code&gt;assume...&lt;/code&gt; functions, experimental linting and symbol libraries&lt;/li&gt;
&lt;li&gt;Breaking on upgrade: WinRM provisioner removed, &lt;code&gt;base64gzip&lt;/code&gt; output changed, macOS 13+ required, last release with 32-bit builds&lt;/li&gt;
&lt;li&gt;State and plan encryption remains OpenTofu's biggest structural advantage over Terraform — stable, GA, supports AWS/GCP/Azure KMS, OpenBao, and passphrase-based keys&lt;/li&gt;
&lt;li&gt;1.13 extended encryption with additional-authenticated-data options for GCP KMS, AWS KMS, and OpenBao&lt;/li&gt;
&lt;li&gt;Two security-relevant bug fixes: a &lt;code&gt;connection.script_path&lt;/code&gt; injection vector closed, SSH proxy errors now report instead of crashing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the official OpenTofu changelog before pinning 1.13 in CI if you're running WinRM provisioners or 32-bit builds — those are the two changes actually capable of breaking a pipeline.&lt;/p&gt;

</description>
      <category>opentofu</category>
      <category>terraform</category>
      <category>devops</category>
      <category>iac</category>
    </item>
    <item>
      <title>Switching Cloud Accounts: AWS, GCP and Azure Fast</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:34:06 +0000</pubDate>
      <link>https://dev.to/amareswer/switching-cloud-accounts-aws-gcp-and-azure-fast-2gfb</link>
      <guid>https://dev.to/amareswer/switching-cloud-accounts-aws-gcp-and-azure-fast-2gfb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/switching-multiple-cloud-accounts-profiles/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Switching cloud accounts across AWS, GCP and Azure is three unrelated problems wearing one trenchcoat. AWS calls them named profiles. GCP calls them configurations. Azure barely gives you a name for it at all — you're switching subscriptions inside one login, or isolating state directories if you actually need two separate identities. Nobody remembers three different mechanisms, so most people end up running &lt;code&gt;aws sts get-caller-identity&lt;/code&gt; before every deploy just to check they're not about to hit the wrong account.&lt;/p&gt;

&lt;p&gt;This is the actual mechanism behind each one, verified on the CLI, plus a shell function that switches all three at once so you stop needing to remember any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching Cloud Accounts on AWS: Named Profiles
&lt;/h2&gt;

&lt;p&gt;AWS profiles, &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html" rel="noopener noreferrer"&gt;documented here&lt;/a&gt;, live in two files: &lt;code&gt;~/.aws/config&lt;/code&gt; (settings) and &lt;code&gt;~/.aws/credentials&lt;/code&gt; (keys), both INI-formatted with &lt;code&gt;[profile name]&lt;/code&gt; sections (the &lt;code&gt;default&lt;/code&gt; profile skips the &lt;code&gt;profile&lt;/code&gt; prefix in &lt;code&gt;config&lt;/code&gt;, but not in &lt;code&gt;credentials&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.aws/config
&lt;/span&gt;&lt;span class="nn"&gt;[default]&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;

&lt;span class="nn"&gt;[profile work]&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;span class="py"&gt;sso_start_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://example.awsapps.com/start&lt;/span&gt;
&lt;span class="py"&gt;sso_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;span class="py"&gt;sso_account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;111122223333&lt;/span&gt;
&lt;span class="py"&gt;sso_role_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;DeveloperAccess&lt;/span&gt;

&lt;span class="nn"&gt;[profile personal]&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List what's actually configured, and check what a given profile resolves to, before you trust it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws configure list-profiles
&lt;span class="go"&gt;default
work
personal

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws configure list &lt;span class="nt"&gt;--profile&lt;/span&gt; personal
&lt;span class="go"&gt;NAME       VALUE                    TYPE                    LOCATION
profile    personal                 manual                  --profile
access_key ****************EST2     shared-credentials-file
secret_key ****************EST2     shared-credentials-file
region     us-west-2                config-file             ~/.aws/config
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every AWS CLI command takes &lt;code&gt;--profile name&lt;/code&gt;, but typing that on every command gets old fast. Set &lt;code&gt;AWS_PROFILE&lt;/code&gt; instead and every subsequent command in that shell uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal
&lt;span class="nv"&gt;$ &lt;/span&gt;aws configure list
NAME       VALUE      TYPE  LOCATION
profile    personal   &lt;span class="nb"&gt;env   &lt;/span&gt;AWS_PROFILE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AWS_PROFILE&lt;/code&gt; beats whatever's in the config file, which is exactly what you want for a per-terminal override.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The SSO gotcha:&lt;/strong&gt; an SSO-based profile with no cached session fails with &lt;code&gt;Error loading SSO Token: Token for &amp;lt;url&amp;gt; does not exist&lt;/code&gt; — not a permissions error, not a typo, just an expired or missing session. Run &lt;code&gt;aws sso login --profile work&lt;/code&gt; to fix it, not &lt;code&gt;aws configure&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching Cloud Accounts on GCP: Named Configurations
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/sdk/gcloud/reference/config/configurations" rel="noopener noreferrer"&gt;gcloud's configurations&lt;/a&gt; are the equivalent — a full bundle of account, project and defaults, switchable by name. :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud config configurations create work
&lt;span class="go"&gt;Created [work].

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud config configurations create personal
&lt;span class="go"&gt;Created [personal].

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcloud config configurations list
&lt;span class="go"&gt;NAME      IS_ACTIVE  ACCOUNT  PROJECT  COMPUTE_DEFAULT_ZONE  COMPUTE_DEFAULT_REGION
default   True
personal  False
work      False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set values inside a specific configuration without activating it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project work-project-id &lt;span class="nt"&gt;--configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;work
Updated property &lt;span class="o"&gt;[&lt;/span&gt;core/project].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch which one is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gcloud config configurations activate personal
Activated &lt;span class="o"&gt;[&lt;/span&gt;personal].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the same environment-variable escape hatch as AWS exists here too — &lt;code&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/code&gt; overrides the active configuration for that shell without touching the persisted state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;work gcloud config get-value project
Your active configuration is: &lt;span class="o"&gt;[&lt;/span&gt;work]
work-project-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for scripts and CI: you can run a one-off command against a different configuration without permanently activating it and without a &lt;code&gt;--configuration&lt;/code&gt; flag on every gcloud subcommand that supports one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching Cloud Accounts on Azure: Subscriptions, Not Profiles
&lt;/h2&gt;

&lt;p&gt;Azure doesn't really do named profiles. &lt;code&gt;az login&lt;/code&gt; authenticates one identity, and that identity can see every subscription its account has access to. Switching "accounts" day to day usually means switching &lt;strong&gt;subscriptions&lt;/strong&gt; within that one login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;az account list &lt;span class="nt"&gt;--output&lt;/span&gt; table
Name          SubscriptionId    State    IsDefault
Work-Prod     11111111-...      Enabled  True
Work-Staging  22222222-...      Enabled  False

&lt;span class="nv"&gt;$ &lt;/span&gt;az account &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--subscription&lt;/span&gt; &lt;span class="s2"&gt;"Work-Staging"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers most people. But if you genuinely need two separate identities — a work tenant and a personal tenant, logged in simultaneously without one &lt;code&gt;az login&lt;/code&gt; clobbering the other — Azure CLI reads its entire state from a directory controlled by &lt;code&gt;AZURE_CONFIG_DIR&lt;/code&gt;, documented at &lt;a href="https://learn.microsoft.com/en-us/cli/azure/azure-cli-configuration" rel="noopener noreferrer"&gt;learn.microsoft.com&lt;/a&gt; (default &lt;code&gt;~/.azure&lt;/code&gt;). Point it somewhere else and you get a fully isolated Azure CLI, tokens and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ AZURE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.azure-work az login
&lt;span class="nv"&gt;$ AZURE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.azure-personal az login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one logs in and caches its session independently. Set &lt;code&gt;AZURE_CONFIG_DIR&lt;/code&gt; before any &lt;code&gt;az&lt;/code&gt; command and that command runs against that identity's isolated state, the same shape as &lt;code&gt;AWS_PROFILE&lt;/code&gt; and &lt;code&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Nobody Automates: Switching All Three at Once
&lt;/h2&gt;

&lt;p&gt;Here's the actual fix for "the problem is remembering them": one shell function that sets all three environment variables together, so a single word switches your entire terminal's cloud identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.bashrc or ~/.zshrc&lt;/span&gt;
cloudenv&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;work&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;work
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;work
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AZURE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.azure-work"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    personal&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal
      &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AZURE_CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.azure-personal"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"usage: cloudenv work|personal"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="k"&gt;return &lt;/span&gt;1
      &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cloudenv -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cloudenv work
&lt;span class="gp"&gt;cloudenv -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;work
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cloudenv personal
&lt;span class="gp"&gt;cloudenv -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;personal
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cloudenv bogus
&lt;span class="go"&gt;usage: cloudenv work|personal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add more cases as you add accounts. This is a plain shell function, not a plugin or a tool to install — it works in bash and zsh as-is, and every AWS, gcloud and az command you run after calling it picks up the right identity automatically, because all three tools read these same environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Active Account Visible in Your Prompt
&lt;/h2&gt;

&lt;p&gt;The environment-variable approach has one real failure mode: you switch, forget you switched, and run something against the wrong account an hour later. Fix it by putting the active profile in your prompt so it's always visible, not something you have to remember to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.bashrc&lt;/span&gt;
&lt;span class="nv"&gt;PS1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[\${AWS_PROFILE:-none}] \w \$ '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That renders as &lt;code&gt;[work] ~/project $&lt;/code&gt; — updates automatically every time &lt;code&gt;cloudenv&lt;/code&gt; changes &lt;code&gt;AWS_PROFILE&lt;/code&gt;, no extra step required. Extend the same pattern to show &lt;code&gt;$CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/code&gt; if you're switching GCP configs just as often.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Doesn't Solve
&lt;/h2&gt;

&lt;p&gt;This pattern switches &lt;em&gt;default&lt;/em&gt; identity for a shell. It doesn't stop you from overriding it per-command with &lt;code&gt;--profile&lt;/code&gt; or &lt;code&gt;--subscription&lt;/code&gt;, and it doesn't replace credential rotation or least-privilege IAM — a &lt;code&gt;cloudenv work&lt;/code&gt; that quietly has admin access everywhere is still a &lt;code&gt;cloudenv work&lt;/code&gt; you should fix at the IAM layer, not just the shell layer.&lt;/p&gt;

&lt;p&gt;If you're also juggling Kubernetes clusters alongside these cloud accounts, that's a separate switching problem with its own tool — see our &lt;a href="https://devtoolhub.com/kubectx-kubernetes-context-switching/" rel="noopener noreferrer"&gt;kubectx guide&lt;/a&gt; for the equivalent pattern with &lt;code&gt;kubectl&lt;/code&gt; contexts. And if &lt;code&gt;.bashrc&lt;/code&gt; itself has become the thing you're afraid to touch, our &lt;a href="https://devtoolhub.com/linux-file-permissions-explained/" rel="noopener noreferrer"&gt;Linux file permissions guide&lt;/a&gt; and &lt;a href="https://devtoolhub.com/find-whats-using-a-port-on-linux/" rel="noopener noreferrer"&gt;finding what's using a port&lt;/a&gt; cover two other everyday shell headaches the same way — real commands, real gotchas, not just theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What's the difference between AWS_PROFILE and --profile?&lt;/strong&gt;&lt;br&gt;
A: &lt;code&gt;--profile name&lt;/code&gt; sets it for one command. &lt;code&gt;export AWS_PROFILE=name&lt;/code&gt; sets it for every command in that shell session until you change or unset it. Both read from the same &lt;code&gt;~/.aws/config&lt;/code&gt; and &lt;code&gt;~/.aws/credentials&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does gcloud have something like AWS_PROFILE?&lt;/strong&gt;&lt;br&gt;
A: Yes — &lt;code&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/code&gt;. Set it as an environment variable and it overrides whichever configuration is currently active via &lt;code&gt;gcloud config configurations activate&lt;/code&gt;, without changing the persisted state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I run two Azure identities at once without logging out?&lt;/strong&gt;&lt;br&gt;
A: Point &lt;code&gt;AZURE_CONFIG_DIR&lt;/code&gt; at two different directories and run &lt;code&gt;az login&lt;/code&gt; in each. Azure CLI reads its entire state — tokens included — from that directory, so each one stays fully isolated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my AWS SSO profile fail with a token error?&lt;/strong&gt;&lt;br&gt;
A: The cached SSO session expired or was never created. Run &lt;code&gt;aws sso login --profile &amp;lt;name&amp;gt;&lt;/code&gt; to fix it — &lt;code&gt;aws configure&lt;/code&gt; won't help here, since the profile's settings are already correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is a shell function safer than a tool like direnv for this?&lt;/strong&gt;&lt;br&gt;
A: A manual &lt;code&gt;cloudenv work&lt;/code&gt; is explicit — you know exactly when your identity changed. Directory-based auto-switching (direnv, etc.) is convenient but can silently activate the wrong account just by &lt;code&gt;cd&lt;/code&gt;-ing into a folder. For anything touching production, explicit beats automatic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS: named profiles in &lt;code&gt;~/.aws/config&lt;/code&gt;/&lt;code&gt;credentials&lt;/code&gt;, switch with &lt;code&gt;--profile&lt;/code&gt; or &lt;code&gt;export AWS_PROFILE=name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GCP: named configurations via &lt;code&gt;gcloud config configurations&lt;/code&gt;, override per-shell with &lt;code&gt;CLOUDSDK_ACTIVE_CONFIG_NAME&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Azure: subscriptions within one login via &lt;code&gt;az account set&lt;/code&gt;, or fully isolated identities via &lt;code&gt;AZURE_CONFIG_DIR&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All three read from environment variables, so one shell function (&lt;code&gt;cloudenv&lt;/code&gt;) can switch all three together&lt;/li&gt;
&lt;li&gt;Put the active profile in &lt;code&gt;PS1&lt;/code&gt; so you see which account you're in instead of having to remember or re-check it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop running &lt;code&gt;aws sts get-caller-identity&lt;/code&gt; as a paranoia check before every command. Switching cloud accounts shouldn't require remembering three different mechanisms — put the account name in your prompt, and you'll see it before you need to ask.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>gcp</category>
      <category>azure</category>
      <category>devops</category>
    </item>
    <item>
      <title>Kubernetes OOMKilled (Exit Code 137): Causes and Fixes</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:31:49 +0000</pubDate>
      <link>https://dev.to/amareswer/kubernetes-oomkilled-exit-code-137-causes-and-fixes-4fc</link>
      <guid>https://dev.to/amareswer/kubernetes-oomkilled-exit-code-137-causes-and-fixes-4fc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/kubernetes-oomkilled-exit-code-137/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes OOMKilled with exit code 137 means the Linux kernel killed your container's process for using more memory than it was allowed. Sometimes that's the container's own memory limit. Sometimes the whole node ran out of memory and your container lost the lottery. The status looks the same, but the fix is completely different, so the first job is working out which one you hit.&lt;/p&gt;

&lt;p&gt;This guide shows how to confirm an OOMKilled container, tell a limit kill from a node kill, and fix the common causes, including the JVM default that trips up most Java workloads. If you need a refresher on how requests and limits work, see our &lt;a href="https://devtoolhub.com/kubernetes-resource-requests-and-limits-full-guide/" rel="noopener noreferrer"&gt;Kubernetes resource requests and limits guide&lt;/a&gt;, or start from the &lt;a href="https://devtoolhub.com/kubernetes-complete-guide/" rel="noopener noreferrer"&gt;complete Kubernetes guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does OOMKilled Exit Code 137 Mean in Kubernetes?
&lt;/h2&gt;

&lt;p&gt;Exit code 137 is 128 + 9. Per the &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html" rel="noopener noreferrer"&gt;Bash manual&lt;/a&gt;, when a process dies from a fatal signal N, the exit status is 128+N. Signal 9 is &lt;code&gt;SIGKILL&lt;/code&gt;, which a process can't catch or clean up after.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OOMKilled&lt;/code&gt; is the reason Kubernetes records when that &lt;code&gt;SIGKILL&lt;/code&gt; came from the kernel's out-of-memory killer. The &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/" rel="noopener noreferrer"&gt;Kubernetes memory docs&lt;/a&gt; show exactly what you'll see in the pod status:&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;lastState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terminated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;exitCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;137&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OOMKilled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Note:&lt;/strong&gt; Exit code 137 on its own doesn't prove an OOM kill. Anything that sends &lt;code&gt;SIGKILL&lt;/code&gt; produces 137, including a pod that ignored &lt;code&gt;SIGTERM&lt;/code&gt; for longer than its grace period (see our &lt;a href="https://devtoolhub.com/kubernetes-graceful-shutdown-a-guide/" rel="noopener noreferrer"&gt;Kubernetes graceful shutdown guide&lt;/a&gt;). Only trust it as memory-related when the reason says &lt;code&gt;OOMKilled&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Confirm a Kubernetes OOMKilled Container
&lt;/h2&gt;

&lt;p&gt;Start with the pod. A container that keeps getting OOMKilled usually shows a climbing restart count and eventually &lt;code&gt;CrashLoopBackOff&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod
kubectl describe pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;describe&lt;/code&gt; output, look under the container's &lt;code&gt;Last State&lt;/code&gt;. You want &lt;code&gt;Terminated&lt;/code&gt;, &lt;code&gt;Reason: OOMKilled&lt;/code&gt;, &lt;code&gt;Exit Code: 137&lt;/code&gt;. To pull just that field across all containers in the pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check how much memory the container actually uses against its limit. This needs metrics-server installed in the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod &lt;span class="nt"&gt;--containers&lt;/span&gt;
kubectl get pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.containers[*].resources}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If usage sits close to the limit just before each restart, you've found your answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container Limit Kill vs Node Out-of-Memory Kill
&lt;/h2&gt;

&lt;p&gt;There are two different ways a container ends up OOMKilled, plus one lookalike that isn't an OOM kill at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;What triggers it&lt;/th&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container limit kill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container uses more than its own &lt;code&gt;limits.memory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OOMKilled&lt;/code&gt;, 137, container restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node OOM kill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The whole node runs out of memory before the kubelet can react&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OOMKilled&lt;/code&gt;, 137, often a container that was under its limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Eviction&lt;/strong&gt; (not an OOM kill)&lt;/td&gt;
&lt;td&gt;Node memory drops below the kubelet's eviction threshold&lt;/td&gt;
&lt;td&gt;Pod status &lt;code&gt;Evicted&lt;/code&gt;, pod rescheduled elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Container limit kills&lt;/strong&gt; are the common case. The kernel enforces &lt;code&gt;limits.memory&lt;/code&gt; through the container's cgroup. The &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/" rel="noopener noreferrer"&gt;resource management docs&lt;/a&gt; point out a subtlety: memory limits are enforced reactively. A container can briefly exceed its limit, and the kill happens when the kernel detects memory pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node OOM kills&lt;/strong&gt; happen when the node itself runs out of memory before the kubelet can evict anything. The kernel's OOM killer then picks a victim across the whole node, and the kubelet biases that choice by Quality of Service class through &lt;code&gt;oom_score_adj&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;QoS class&lt;/th&gt;
&lt;th&gt;&lt;code&gt;oom_score_adj&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Killed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;td&gt;-997&lt;/td&gt;
&lt;td&gt;Last&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burstable&lt;/td&gt;
&lt;td&gt;Between 3 and 999, based on the memory request&lt;/td&gt;
&lt;td&gt;In between&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BestEffort&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is why a pod with no memory requests or limits (BestEffort) gets killed first on a busy node, even if it's small. To check the node side, look for kernel OOM events. The Kubernetes docs example shows them as &lt;code&gt;OOMKilling&lt;/code&gt; warnings on the node, though whether they appear as events depends on your cluster's node monitoring:&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;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; oom
kubectl get events &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OOMKilling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Evictions&lt;/strong&gt; look different. The pod status says &lt;code&gt;Evicted&lt;/code&gt;, not &lt;code&gt;OOMKilled&lt;/code&gt;, and the pod is replaced rather than restarted in place. If that's what you see, the fix is node capacity and requests, not container limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Java Containers Get OOMKilled (and Why They Sometimes Don't Use Their Memory)
&lt;/h2&gt;

&lt;p&gt;Java is the most common OOMKilled search for a reason. A modern JVM sizes its heap from the container's memory limit, but by default it only takes a quarter of it. The &lt;a href="https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html" rel="noopener noreferrer"&gt;Java documentation&lt;/a&gt; lists the &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; default as 25 percent.&lt;/p&gt;

&lt;p&gt;That causes two opposite problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heap too small.&lt;/strong&gt; With a 2 GiB limit, the default max heap is about 512 MiB. The app throws &lt;code&gt;java.lang.OutOfMemoryError&lt;/code&gt; while the container still has plenty of memory. That's a JVM error, not an OOMKilled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap too big.&lt;/strong&gt; Someone sets &lt;code&gt;-Xmx&lt;/code&gt; or &lt;code&gt;MaxRAMPercentage=100&lt;/code&gt; to "use all the memory." But the JVM also needs memory outside the heap: metaspace, thread stacks, the JIT code cache, direct buffers. Heap plus everything else exceeds the limit, and the kernel kills the container with 137.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common middle ground is to leave headroom for non-heap memory:&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;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JAVA_TOOL_OPTIONS&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-XX:MaxRAMPercentage=75"&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2Gi"&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tune the percentage from real measurements. Apps with many threads or large direct buffers need more non-heap headroom than 25 percent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Causes of Kubernetes OOMKilled Errors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory-backed &lt;code&gt;emptyDir&lt;/code&gt; volumes.&lt;/strong&gt; If you mount an &lt;code&gt;emptyDir&lt;/code&gt; with &lt;code&gt;medium: Memory&lt;/code&gt;, files written there count against the container's memory limit. The docs note the kubelet tracks tmpfs &lt;code&gt;emptyDir&lt;/code&gt; volumes as container memory use, not local storage. A job that writes a large temp file to a RAM-backed volume gets OOMKilled even though the app itself is small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real memory leak.&lt;/strong&gt; If memory climbs steadily until each kill, raising the limit only delays the next restart. Graph the container's memory over a few restarts. A sawtooth that always climbs to the limit is a leak, and the fix is in the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A limit copied from another service.&lt;/strong&gt; Limits often get set once from a template and never measured. Check actual peak usage under load before assuming the app is misbehaving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidecars.&lt;/strong&gt; Each container has its own limit. An Envoy or logging sidecar can be the one getting OOMKilled while your app container is fine. The jsonpath command above shows which container it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix a Kubernetes OOMKilled Container
&lt;/h2&gt;

&lt;p&gt;Match the fix to the cause:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Limit too low for real usage:&lt;/strong&gt; raise &lt;code&gt;limits.memory&lt;/code&gt; in the Deployment, based on measured peak usage plus headroom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node OOM kills:&lt;/strong&gt; set memory requests that reflect real usage, so the scheduler doesn't overpack nodes. For critical workloads, set requests equal to limits so the pod is Guaranteed and killed last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java:&lt;/strong&gt; set &lt;code&gt;MaxRAMPercentage&lt;/code&gt; deliberately instead of relying on the 25 percent default or &lt;code&gt;-Xmx&lt;/code&gt; guesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leak:&lt;/strong&gt; fix the code. Raising the limit buys time, not a solution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Raising Memory Without Recreating the Pod
&lt;/h3&gt;

&lt;p&gt;In-place pod resize has been stable since Kubernetes v1.35, according to the &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/" rel="noopener noreferrer"&gt;resize documentation&lt;/a&gt;. You can raise a running container's memory through the pod's &lt;code&gt;resize&lt;/code&gt; subresource. It requires kubectl v1.32 or newer.&lt;/p&gt;

&lt;p&gt;This changes one running pod only. It is useful for buying time during an incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch pod my-app-7d9f8 &lt;span class="nt"&gt;-n&lt;/span&gt; prod &lt;span class="nt"&gt;--subresource&lt;/span&gt; resize &lt;span class="nt"&gt;--patch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"spec":{"containers":[{"name":"app","resources":{"requests":{"memory":"1Gi"},"limits":{"memory":"1Gi"}}}]}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; For pods managed by a Deployment, this patch is lost the next time the pod is replaced, because new pods come from the Deployment template. Update the Deployment too, or the OOMKilled restarts come back on the next rollout.&lt;/p&gt;

&lt;p&gt;Keep a few resize rules in mind. The pod's QoS class can't change through a resize, so a Guaranteed pod must keep requests equal to limits. Lowering a memory limit is best-effort: if current usage is above the new limit, the kubelet skips the resize and leaves it "In Progress." Windows pods don't support in-place resize.&lt;/p&gt;

&lt;p&gt;Right-sizing memory also affects your bill. Our &lt;a href="https://devtoolhub.com/kubernetes-cost-optimization/" rel="noopener noreferrer"&gt;Kubernetes cost optimization guide&lt;/a&gt; covers finding over-provisioned workloads without starving them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: What does exit code 137 mean in Kubernetes?&lt;/strong&gt;&lt;br&gt;
A: Exit code 137 means the process was killed by signal 9 (&lt;code&gt;SIGKILL&lt;/code&gt;), because the exit status for a fatal signal is 128 plus the signal number. When the reason is &lt;code&gt;OOMKilled&lt;/code&gt;, the kernel's out-of-memory killer sent it. Without that reason, 137 can also come from a pod that ignored &lt;code&gt;SIGTERM&lt;/code&gt; past its grace period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I check why a Kubernetes pod was OOMKilled?&lt;/strong&gt;&lt;br&gt;
A: Run &lt;code&gt;kubectl describe pod &amp;lt;name&amp;gt;&lt;/code&gt; and look at the container's &lt;code&gt;Last State&lt;/code&gt; for &lt;code&gt;Reason: OOMKilled&lt;/code&gt; and &lt;code&gt;Exit Code: 137&lt;/code&gt;. Then compare &lt;code&gt;kubectl top pod &amp;lt;name&amp;gt; --containers&lt;/code&gt; with the container's memory limit. If usage was under the limit, check the node for kernel OOM events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is OOMKilled the same as a pod being evicted?&lt;/strong&gt;&lt;br&gt;
A: No. OOMKilled means the kernel killed a container, and the kubelet restarts it in the same pod. Eviction means the kubelet removed the whole pod because the node was short on memory, and the pod status shows &lt;code&gt;Evicted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I increase a pod's memory limit without restarting it?&lt;/strong&gt;&lt;br&gt;
A: Yes, on Kubernetes v1.35 or later, with kubectl v1.32 or later. Patch the pod's &lt;code&gt;resize&lt;/code&gt; subresource. But for Deployment-managed pods, also update the Deployment, or the change is lost when the pod is replaced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my Java app get OOMKilled when the heap is smaller than the limit?&lt;/strong&gt;&lt;br&gt;
A: The JVM uses memory outside the heap for metaspace, thread stacks, the code cache and direct buffers. Heap plus non-heap can exceed the container limit. Leave headroom, for example &lt;code&gt;-XX:MaxRAMPercentage=75&lt;/code&gt; instead of 100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exit code 137 is 128 + 9 (&lt;code&gt;SIGKILL&lt;/code&gt;); only the &lt;code&gt;OOMKilled&lt;/code&gt; reason ties it to memory&lt;/li&gt;
&lt;li&gt;A container limit kill, a node OOM kill and an eviction look similar but need different fixes&lt;/li&gt;
&lt;li&gt;On node OOM, BestEffort pods die first (&lt;code&gt;oom_score_adj&lt;/code&gt; 1000) and Guaranteed pods last (-997)&lt;/li&gt;
&lt;li&gt;The JVM defaults to a max heap of 25 percent of the container limit, and non-heap memory still counts&lt;/li&gt;
&lt;li&gt;In-place resize (stable since v1.35) raises memory on a running pod, but update the Deployment too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time a pod shows Kubernetes OOMKilled, run &lt;code&gt;kubectl describe pod&lt;/code&gt; and &lt;code&gt;kubectl top pod --containers&lt;/code&gt; side by side before touching any limits. Those two outputs tell you which of the three cases you're in.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>sre</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>GitHub Actions Workflow Not Triggering? Every Cause and Fix</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:06:08 +0000</pubDate>
      <link>https://dev.to/amareswer/github-actions-workflow-not-triggering-every-cause-and-fix-5070</link>
      <guid>https://dev.to/amareswer/github-actions-workflow-not-triggering-every-cause-and-fix-5070</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/github-actions-workflow-not-triggering/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A GitHub Actions workflow not triggering almost never means GitHub is broken. It means one of about a dozen rules quietly filtered your event out: the workflow file is on the wrong branch, a filter didn't match, the commit was pushed with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, or the schedule got disabled. None of these produce an error. The run simply never appears.&lt;/p&gt;

&lt;p&gt;This guide covers every reason for a GitHub Actions workflow not triggering, in the order worth checking. Each one comes with the exact rule from GitHub's docs and the fix. If you're still learning workflow syntax, read &lt;a href="https://devtoolhub.com/github-actions-workflow-yaml-guide/" rel="noopener noreferrer"&gt;how GitHub Actions workflow files are structured&lt;/a&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions Workflow Not Triggering? Quick Diagnosis
&lt;/h2&gt;

&lt;p&gt;Match your symptom to the likely cause, then jump to that section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No "Run workflow" button&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;workflow_dispatch&lt;/code&gt; file isn't on the default branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled workflow never runs&lt;/td&gt;
&lt;td&gt;File not on default branch, or auto-disabled after 60 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled run is late or missing&lt;/td&gt;
&lt;td&gt;Queue delay at the top of the hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push from another workflow triggers nothing&lt;/td&gt;
&lt;td&gt;Push used &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs on branches, never on tags&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;branches&lt;/code&gt; filter defined without &lt;code&gt;tags&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pushed a change, nothing ran&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;paths&lt;/code&gt; filter didn't match, or &lt;code&gt;[skip ci]&lt;/code&gt; in the message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fork PR shows "approval required"&lt;/td&gt;
&lt;td&gt;First-time contributor approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow missing from the Actions tab&lt;/td&gt;
&lt;td&gt;Invalid YAML, disabled workflow, or Actions turned off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before anything else, check whether GitHub created a run at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List recent runs for one workflow, including disabled ones&lt;/span&gt;
gh run list &lt;span class="nt"&gt;--workflow&lt;/span&gt; ci.yml &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 10

&lt;span class="c"&gt;# Filter by the event you expected&lt;/span&gt;
gh run list &lt;span class="nt"&gt;--workflow&lt;/span&gt; ci.yml &lt;span class="nt"&gt;--event&lt;/span&gt; push &lt;span class="nt"&gt;--branch&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a run exists with status &lt;code&gt;skipped&lt;/code&gt; or &lt;code&gt;action_required&lt;/code&gt;, the trigger worked. Something later stopped it. But if there's no run at all, a rule filtered the event out first. That distinction cuts the list below in half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow Not Triggering Because It's Not on the Default Branch
&lt;/h2&gt;

&lt;p&gt;Many events only fire if the workflow file exists on the &lt;strong&gt;default branch&lt;/strong&gt;. That includes &lt;code&gt;workflow_dispatch&lt;/code&gt;, &lt;code&gt;schedule&lt;/code&gt;, &lt;code&gt;workflow_run&lt;/code&gt;, &lt;code&gt;issues&lt;/code&gt;, &lt;code&gt;issue_comment&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, and most other non-push events. The &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows" rel="noopener noreferrer"&gt;events reference&lt;/a&gt; repeats the same note on each: "This event will only trigger a workflow run if the workflow file exists on the default branch."&lt;/p&gt;

&lt;p&gt;This catches people constantly. You add a new &lt;code&gt;workflow_dispatch&lt;/code&gt; workflow on a feature branch. Then you open the Actions tab, and there's no "Run workflow" button. The workflow is fine. It just isn't on &lt;code&gt;main&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; merge the workflow file to the default branch first. After that, you can dispatch it against any branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh workflow run deploy.yml &lt;span class="nt"&gt;--ref&lt;/span&gt; my-feature-branch &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's docs spell out the order: the button appears once the file is on the default branch, and once the workflow has run at least once, you can dispatch it against any branch or tag via the API or &lt;code&gt;gh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;pull_request&lt;/code&gt; are the exceptions. They run from the workflow file on the branch you pushed, which is why a CI workflow can work on a feature branch while a new manual workflow can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduled GitHub Actions Workflow Not Triggering
&lt;/h2&gt;

&lt;p&gt;Scheduled workflows have four separate failure modes, and none of them show an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The file isn't on the default branch.&lt;/strong&gt; Scheduled workflows only run on the default branch, using its latest commit. A cron on any other branch never fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It was disabled after 60 days.&lt;/strong&gt; In a public repository, GitHub automatically disables scheduled workflows when there's been no repository activity for 60 days. Forks of public repos also start with scheduled workflows disabled. Re-enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh workflow list &lt;span class="nt"&gt;--all&lt;/span&gt;          &lt;span class="c"&gt;# disabled workflows are hidden without --all&lt;/span&gt;
gh workflow &lt;span class="nb"&gt;enable &lt;/span&gt;nightly.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. It ran, just late.&lt;/strong&gt; The docs are blunt about this: schedule events "can be delayed during periods of high loads," the start of every hour is a high-load time, and under enough load "some queued jobs may be dropped." Everyone writes &lt;code&gt;0 * * * *&lt;/code&gt;. Don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The time is off by hours.&lt;/strong&gt; Cron runs in UTC unless you say otherwise. GitHub now supports an IANA &lt;code&gt;timezone&lt;/code&gt; per schedule, so you can stop converting by hand:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1-5'&lt;/span&gt;   &lt;span class="c1"&gt;# 5:17 AM, weekdays, off the top of the hour&lt;/span&gt;
      &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a daylight-saving timezone, a run that falls in the skipped spring-forward hour moves to the next valid time. For example, a 2:30 AM schedule runs at 3:00 AM that day. The shortest interval GitHub allows is every 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does a Push From Another Workflow Not Trigger Anything?
&lt;/h2&gt;

&lt;p&gt;Events created with the repository's &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; don't start new workflow runs. GitHub does this on purpose to prevent infinite loops. Otherwise, a workflow that pushes a commit would trigger itself forever. The &lt;a href="https://docs.github.com/en/actions/concepts/security/github_token" rel="noopener noreferrer"&gt;GITHUB_TOKEN docs&lt;/a&gt; list the exceptions.&lt;/p&gt;

&lt;p&gt;So say your release workflow commits a version bump and pushes it with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. Your &lt;code&gt;on: push&lt;/code&gt; workflow won't run for that commit. There are two exceptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;workflow_dispatch&lt;/code&gt; and &lt;code&gt;repository_dispatch&lt;/code&gt; always create runs, even when sent with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When a workflow opens or updates a pull request with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, the &lt;code&gt;opened&lt;/code&gt;, &lt;code&gt;synchronize&lt;/code&gt; and &lt;code&gt;reopened&lt;/code&gt; events create runs in an &lt;strong&gt;approval-required&lt;/strong&gt; state. Someone with write access has to click "Approve workflows to run."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; trigger the next workflow explicitly with &lt;code&gt;workflow_dispatch&lt;/code&gt;, or use a GitHub App token or a fine-grained personal access token for the push. The App token is the better choice for shared repos because it isn't tied to a person's account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch, Tag and Path Filters That Silently Skip Runs
&lt;/h2&gt;

&lt;p&gt;Filters are the biggest single reason for a workflow not triggering. Three rules from the &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax" rel="noopener noreferrer"&gt;workflow syntax reference&lt;/a&gt; cause most of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defining &lt;code&gt;branches&lt;/code&gt; alone disables tags.&lt;/strong&gt; If you define only &lt;code&gt;branches&lt;/code&gt;/&lt;code&gt;branches-ignore&lt;/code&gt;, the workflow won't run for tag pushes, and vice versa. Define neither and it runs for both. So this workflow never runs on &lt;code&gt;v1.2.0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# add this to also run on release tags:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;branches&lt;/code&gt; and &lt;code&gt;paths&lt;/code&gt; must both match.&lt;/strong&gt; When you set both, the workflow runs only when a push matches the branch &lt;strong&gt;and&lt;/strong&gt; changes a matching file. A docs-only commit to &lt;code&gt;main&lt;/code&gt; with &lt;code&gt;paths: ['src/**']&lt;/code&gt; does nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path filters have diff limits.&lt;/strong&gt; GitHub builds the changed-file list with a two-dot diff for pushes and a three-dot diff for pull requests. So if the diff has more than 3,000 files and your matches aren't in the first 3,000, the workflow won't run. On the other hand, a push with more than 1,000 commits always runs. Large merges and monorepo refactors hit both limits.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; A workflow skipped by a branch or path filter leaves its required checks stuck in &lt;strong&gt;Pending&lt;/strong&gt;, which blocks the PR from merging. If you require a check that's path-filtered, the usual fix is a small always-running job that reports success when the real job is skipped.&lt;/p&gt;

&lt;p&gt;Also note that GitHub won't create push events for tags when more than three tags are pushed at once. Pushing a batch of tags with &lt;code&gt;git push --tags&lt;/code&gt; can trigger nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip Keywords in the Commit Message
&lt;/h2&gt;

&lt;p&gt;Any of these in the commit message (or the HEAD commit of a PR) stops &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;pull_request&lt;/code&gt; workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[skip ci]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[ci skip]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[no ci]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[skip actions]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[actions skip]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;skip-checks: true&lt;/code&gt; trailer does the same. This bites teams whose release tooling adds &lt;code&gt;[skip ci]&lt;/code&gt; to automated commits. Then someone squash-merges a PR, and the combined message still contains it.&lt;/p&gt;

&lt;p&gt;Skip keywords only apply to &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;pull_request&lt;/code&gt;. They don't stop &lt;code&gt;pull_request_target&lt;/code&gt;, &lt;code&gt;schedule&lt;/code&gt; or &lt;code&gt;workflow_dispatch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests From Forks Waiting for Approval
&lt;/h2&gt;

&lt;p&gt;When a first-time contributor opens a pull request on a public repository, a maintainer with write access may need to approve the workflow run before it starts. The run exists, with status &lt;code&gt;action_required&lt;/code&gt;, but nothing executes until someone approves it on the PR page.&lt;/p&gt;

&lt;p&gt;Separately, fork PRs get no secrets except &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, and that token is read-only. That doesn't stop the workflow from triggering, but a deploy step that needs a secret will fail, which is easy to mistake for a trigger problem. Our &lt;a href="https://devtoolhub.com/github-actions-security-best-practices/" rel="noopener noreferrer"&gt;GitHub Actions security guide&lt;/a&gt; covers why &lt;code&gt;pull_request_target&lt;/code&gt; is risky as a workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  workflow_run Chains That Stop Partway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;workflow_run&lt;/code&gt; lets one workflow start after another finishes. Two limits cause silent gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only fires if the listening workflow is on the default branch (same rule as above).&lt;/li&gt;
&lt;li&gt;You can't chain more than three levels. In GitHub's example, A → B → C → D → E → F runs A through D, and E and F never run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, the &lt;code&gt;requested&lt;/code&gt; activity type doesn't fire on re-runs. If you re-run the upstream workflow, a downstream workflow filtered to &lt;code&gt;types: [requested]&lt;/code&gt; won't start again. Use &lt;code&gt;completed&lt;/code&gt; for most chains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow Doesn't Show Up at All
&lt;/h2&gt;

&lt;p&gt;If the workflow is missing from the Actions tab entirely, check these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Location and extension.&lt;/strong&gt; The file must be in &lt;code&gt;.github/workflows/&lt;/code&gt; with a &lt;code&gt;.yml&lt;/code&gt; or &lt;code&gt;.yaml&lt;/code&gt; extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid YAML.&lt;/strong&gt; A syntax error means GitHub can't parse the triggers. The Actions tab shows the error on the workflow page after you push. Catch it earlier by running &lt;code&gt;actionlint&lt;/code&gt; locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The workflow is disabled.&lt;/strong&gt; &lt;code&gt;gh workflow list&lt;/code&gt; hides disabled workflows. Use &lt;code&gt;gh workflow list --all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions is disabled for the repo or org.&lt;/strong&gt; Check &lt;strong&gt;Settings → Actions → General&lt;/strong&gt;. If you see "GitHub Actions is currently disabled for this repository," GitHub's docs note that changing these settings may not restore access. The account can be in a GitHub-controlled disabled state, which needs support.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  GitHub Actions Workflow Not Triggering: The Full Checklist
&lt;/h2&gt;

&lt;p&gt;When you have a GitHub Actions workflow not triggering, work through this in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does &lt;code&gt;gh run list --workflow &amp;lt;file&amp;gt; --all&lt;/code&gt; show a run? If yes, check its status instead.&lt;/li&gt;
&lt;li&gt;Is the workflow file on the default branch? Required for &lt;code&gt;schedule&lt;/code&gt;, &lt;code&gt;workflow_dispatch&lt;/code&gt;, and &lt;code&gt;workflow_run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Is the workflow enabled? &lt;code&gt;gh workflow list --all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do &lt;code&gt;branches&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;paths&lt;/code&gt; all match what you pushed?&lt;/li&gt;
&lt;li&gt;Did the commit message contain a skip keyword?&lt;/li&gt;
&lt;li&gt;Was the event created with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is it a fork PR waiting for approval?&lt;/li&gt;
&lt;li&gt;For schedules: UTC vs your timezone, top-of-the-hour delays, 60-day auto-disable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once it runs, make it a required check so it can't be skipped silently. Our &lt;a href="https://devtoolhub.com/github-actions-testing-unit-tests/" rel="noopener noreferrer"&gt;unit testing with GitHub Actions guide&lt;/a&gt; shows the setup, and the &lt;a href="https://devtoolhub.com/git-github-actions-guide/" rel="noopener noreferrer"&gt;complete Git and GitHub Actions guide&lt;/a&gt; ties the whole workflow together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why doesn't my workflow_dispatch workflow show a Run workflow button?&lt;/strong&gt;&lt;br&gt;
A: The workflow file isn't on the default branch yet. GitHub only shows the "Run workflow" button when the file exists on the default branch. Merge it to &lt;code&gt;main&lt;/code&gt;, then you can dispatch it against any branch with &lt;code&gt;gh workflow run &amp;lt;file&amp;gt; --ref &amp;lt;branch&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why is my GitHub Actions cron schedule not running?&lt;/strong&gt;&lt;br&gt;
A: Scheduled workflows only run from the default branch, run in UTC unless you set a &lt;code&gt;timezone&lt;/code&gt;, can be delayed or dropped at the top of the hour, and are auto-disabled in public repos after 60 days without activity. Check &lt;code&gt;gh workflow list --all&lt;/code&gt; to see if it's disabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why doesn't a commit pushed by a workflow trigger other workflows?&lt;/strong&gt;&lt;br&gt;
A: Events created with the repository's &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; don't start new workflow runs, to prevent recursive loops. Use &lt;code&gt;workflow_dispatch&lt;/code&gt; or &lt;code&gt;repository_dispatch&lt;/code&gt; to chain workflows, or push with a GitHub App token instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my workflow run on branches but not on tags?&lt;/strong&gt;&lt;br&gt;
A: If you define only a &lt;code&gt;branches&lt;/code&gt; filter under &lt;code&gt;push&lt;/code&gt;, GitHub won't run the workflow for tag pushes. Add a &lt;code&gt;tags&lt;/code&gt; filter, such as &lt;code&gt;tags: ['v*']&lt;/code&gt;, to cover both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why are my required checks stuck in Pending?&lt;/strong&gt;&lt;br&gt;
A: A workflow skipped by a path filter, branch filter or skip keyword leaves its checks pending, which blocks merging. Add a job that always runs and reports success, or avoid path-filtering required checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;schedule&lt;/code&gt;, &lt;code&gt;workflow_dispatch&lt;/code&gt; and &lt;code&gt;workflow_run&lt;/code&gt; only fire when the workflow file is on the default branch&lt;/li&gt;
&lt;li&gt;Pushes made with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; don't start new runs, except &lt;code&gt;workflow_dispatch&lt;/code&gt; and &lt;code&gt;repository_dispatch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;branches&lt;/code&gt;-only filter silently ignores tag pushes, and &lt;code&gt;branches&lt;/code&gt; + &lt;code&gt;paths&lt;/code&gt; must both match&lt;/li&gt;
&lt;li&gt;Public-repo schedules auto-disable after 60 days of inactivity and can be delayed at the top of the hour&lt;/li&gt;
&lt;li&gt;Scheduled workflows accept an IANA &lt;code&gt;timezone&lt;/code&gt;, so you no longer have to convert cron times to UTC by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time you're stuck with a GitHub Actions workflow not triggering, start with &lt;code&gt;gh run list --workflow &amp;lt;file&amp;gt; --all&lt;/code&gt;. Whether a run exists tells you which half of this list to check.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>cicd</category>
      <category>github</category>
    </item>
    <item>
      <title>Ollama vs vLLM vs llama.cpp: Which Local LLM Engine?</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:20:51 +0000</pubDate>
      <link>https://dev.to/amareswer/ollama-vs-vllm-vs-llamacpp-which-local-llm-engine-58h6</link>
      <guid>https://dev.to/amareswer/ollama-vs-vllm-vs-llamacpp-which-local-llm-engine-58h6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/ollama-vs-vllm-vs-llama-cpp/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ollama vs vLLM vs llama.cpp comes down to one question: how many people will hit the model at once? For one developer on a laptop, pick Ollama or llama.cpp. For a GPU server with many concurrent users, pick vLLM. Below, I explain why. I also benchmarked Ollama and llama.cpp on the same model file, and the defaults changed the result more than the engine did.&lt;/p&gt;

&lt;p&gt;If you haven't installed any of these yet, start with our &lt;a href="https://devtoolhub.com/install-lm-studio-ollama/" rel="noopener noreferrer"&gt;LM Studio and Ollama setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama vs vLLM vs llama.cpp at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ollama&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;Model manager and local server&lt;/td&gt;
&lt;td&gt;C/C++ inference engine, CLI and server&lt;/td&gt;
&lt;td&gt;High-throughput serving engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;Easiest local setup&lt;/td&gt;
&lt;td&gt;Running anywhere, fine control&lt;/td&gt;
&lt;td&gt;Many concurrent requests on GPUs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model format&lt;/td&gt;
&lt;td&gt;Its own library, or import GGUF / Safetensors&lt;/td&gt;
&lt;td&gt;GGUF&lt;/td&gt;
&lt;td&gt;Hugging Face weights; GGUF "highly experimental" via a plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;macOS, Windows, Linux; CPU, NVIDIA, AMD, Apple Silicon&lt;/td&gt;
&lt;td&gt;CPU, Metal, CUDA, HIP, Vulkan, SYCL and more&lt;/td&gt;
&lt;td&gt;NVIDIA, AMD and Intel GPUs, TPUs, CPUs; experimental on Apple Silicon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Native API plus OpenAI-compatible&lt;/td&gt;
&lt;td&gt;OpenAI-compatible (&lt;code&gt;llama serve&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;OpenAI-compatible, Anthropic Messages API, gRPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency default&lt;/td&gt;
&lt;td&gt;1 request per model&lt;/td&gt;
&lt;td&gt;Parallel slots, auto-sized&lt;/td&gt;
&lt;td&gt;Continuous batching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup effort&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Each Engine Actually Is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;llama.cpp&lt;/strong&gt; is the engine underneath much of the local-LLM world. It's a plain C/C++ implementation with no dependencies. Per its &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;README&lt;/a&gt;, it targets "a wide range of hardware." It runs GGUF files and supports 1.5-bit to 8-bit quantization. As of v0.5.0 it ships a single &lt;code&gt;llama&lt;/code&gt; command, so the old &lt;code&gt;llama-server&lt;/code&gt; workflow is now &lt;code&gt;llama serve&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Chat with a model straight from Hugging Face&lt;/span&gt;
llama cli &lt;span class="nt"&gt;-hf&lt;/span&gt; ggml-org/Qwen3-4B-GGUF

&lt;span class="c"&gt;# OpenAI-compatible server on port 8080&lt;/span&gt;
llama serve &lt;span class="nt"&gt;-m&lt;/span&gt; ./Qwen3-4B-Q4_K_M.gguf &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ollama&lt;/strong&gt; wraps local inference in a model manager: &lt;code&gt;ollama pull&lt;/code&gt;, &lt;code&gt;ollama run&lt;/code&gt;, a model library, and a background service. It exposes both its own API and an OpenAI-compatible one on port &lt;code&gt;11434&lt;/code&gt;. It trades some control for convenience, which is why most people start here. Our &lt;a href="https://devtoolhub.com/ollama-api-guide/" rel="noopener noreferrer"&gt;Ollama API guide&lt;/a&gt; covers the endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vLLM&lt;/strong&gt; is a serving engine built for throughput. Its headline features are PagedAttention for KV-cache memory, continuous batching, chunked prefill and prefix caching. It also splits models across GPUs with tensor, pipeline and expert parallelism. So it's what you run behind an internal API that dozens of users or agents call at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv pip &lt;span class="nb"&gt;install &lt;/span&gt;vllm
vllm serve Qwen/Qwen3-4B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ollama vs llama.cpp: A Real Benchmark on the Same Model File
&lt;/h2&gt;

&lt;p&gt;To compare Ollama and llama.cpp fairly, I loaded the exact same GGUF file into both: Qwen3-4B at Q4_K_M, 2.5 GB. The machine was an Apple M1 with 16 GB of RAM. I ran Ollama 0.34.4 and llama.cpp 0.5.0 with default settings. Each engine generated 256 tokens for the same prompt. First I sent one request at a time, then four at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Single request&lt;/th&gt;
&lt;th&gt;4 concurrent (aggregate)&lt;/th&gt;
&lt;th&gt;Wall time for 4 requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama, default (&lt;code&gt;OLLAMA_NUM_PARALLEL=1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;18.7 tok/s&lt;/td&gt;
&lt;td&gt;18.9 tok/s&lt;/td&gt;
&lt;td&gt;54 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp &lt;code&gt;llama serve&lt;/code&gt;, default (4 slots)&lt;/td&gt;
&lt;td&gt;13.7 tok/s&lt;/td&gt;
&lt;td&gt;28.9 tok/s&lt;/td&gt;
&lt;td&gt;35 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama with &lt;code&gt;OLLAMA_NUM_PARALLEL=4&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;17.8 tok/s&lt;/td&gt;
&lt;td&gt;35.0 tok/s&lt;/td&gt;
&lt;td&gt;29 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For one user, Ollama was faster.&lt;/strong&gt; It generated about 18.7 tokens per second against 13.7 for llama.cpp on the same file. The startup logs show different defaults: Ollama picked a 4,096-token context from available VRAM, while &lt;code&gt;llama serve&lt;/code&gt; auto-configured 4 slots with a 40,960-token context each. Same weights, different setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under load, Ollama's default queues.&lt;/strong&gt; With four requests at once, default Ollama served them one after another. Aggregate throughput stayed at about 19 tokens per second, and the last user waited nearly a minute. llama.cpp's parallel slots split the work, so each request ran slower (8 tokens per second) but all four finished in 35 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One setting flips the result.&lt;/strong&gt; With &lt;code&gt;OLLAMA_NUM_PARALLEL=4&lt;/code&gt;, Ollama handled the same four requests in 29 seconds at 35 tokens per second aggregate, the best result in the test.&lt;/p&gt;

&lt;p&gt;I also threw away an earlier run. It showed llama.cpp far ahead, but the Mac was busy (load average around 9), and the numbers didn't hold up on a rerun. Benchmark on an idle machine, and run each test more than once.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note:&lt;/strong&gt; These are small-model numbers on one laptop. They show the shape of the difference, not a universal ranking. Run the same test on your own hardware before choosing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama vs vLLM vs llama.cpp Under Concurrent Load
&lt;/h2&gt;

&lt;p&gt;For a single user, the gap between engines comes mostly from configuration: context size, slot count and quantization. Model size and memory bandwidth set the ceiling, and Ollama and llama.cpp share much of the same low-level code.&lt;/p&gt;

&lt;p&gt;Concurrency is where they split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; processes one request per model at a time by default. Its &lt;a href="https://docs.ollama.com/faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; lists &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; with a default of 1, and notes that memory use scales with &lt;code&gt;OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH&lt;/code&gt;. Extra requests wait in a queue, and past &lt;code&gt;OLLAMA_MAX_QUEUE&lt;/code&gt; the server returns a 503.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp&lt;/strong&gt; runs several parallel "slots" in &lt;code&gt;llama serve&lt;/code&gt;. The &lt;code&gt;-np&lt;/code&gt; / &lt;code&gt;--parallel&lt;/code&gt; flag defaults to auto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM&lt;/strong&gt; batches requests continuously, adding new ones to the running batch as others finish. Combined with PagedAttention, this is what lets one GPU serve many users without the queue growing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Ollama is your backend and several people or agents share it, raise the parallel setting before blaming the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_NUM_PARALLEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each parallel slot needs its own context memory, so watch RAM or VRAM when you raise it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vLLM Memory Default That Surprises People
&lt;/h2&gt;

&lt;p&gt;vLLM claims most of the GPU as soon as it starts. The &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; flag defaults to &lt;code&gt;0.92&lt;/code&gt;, meaning vLLM takes 92 percent of GPU memory for the model and its KV cache, per the &lt;a href="https://docs.vllm.ai/en/stable/configuration/engine_args/" rel="noopener noreferrer"&gt;engine arguments reference&lt;/a&gt;. That's intentional: the spare memory becomes KV cache, which is what makes high concurrency possible.&lt;/p&gt;

&lt;p&gt;It also means a second vLLM instance, or anything else on that GPU, fails to allocate. Run two instances on one GPU and you have to split it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm serve Qwen/Qwen3-4B &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.45 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
vllm serve Qwen/Qwen3-1.7B &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.45 &lt;span class="nt"&gt;--port&lt;/span&gt; 8001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Can vLLM Run GGUF Models or Run on a Mac?
&lt;/h2&gt;

&lt;p&gt;Mostly no, for now. vLLM's GGUF support is labelled "highly experimental and under-optimized" in its own docs, and it now needs a separate &lt;code&gt;vllm-gguf-plugin&lt;/code&gt; package. If your models are GGUF files, llama.cpp or Ollama is the natural fit.&lt;/p&gt;

&lt;p&gt;On Macs, vLLM's installation docs describe experimental support for Apple Silicon that must be built from source and runs on the CPU. In practice, Apple Silicon users should pick Ollama or llama.cpp, both of which use Metal on the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama vs vLLM vs llama.cpp: Which One Should You Use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One developer, local machine, just want it working:&lt;/strong&gt; Ollama. &lt;code&gt;ollama run&lt;/code&gt; is the fastest path from nothing to a working model, and the &lt;a href="https://devtoolhub.com/ollama-hardware-requirements/" rel="noopener noreferrer"&gt;hardware requirements guide&lt;/a&gt; tells you what fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum control, unusual hardware, or embedding inference in your own tooling:&lt;/strong&gt; llama.cpp. You pick the exact quantization, the context size, the slot count and the backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared endpoint for a team or an application with real traffic, on NVIDIA or AMD GPUs:&lt;/strong&gt; vLLM. Its batching pays off as soon as requests overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small team sharing one Ollama box:&lt;/strong&gt; Ollama can work, but set &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; above 1 and test under load first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common path is to prototype with Ollama, then move the same model to vLLM when it becomes a service. All three expose an OpenAI-compatible API, so switching usually means changing a base URL and model name, not rewriting code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Ollama built on llama.cpp?&lt;/strong&gt;&lt;br&gt;
A: Ollama runs GGUF models using the ggml/llama.cpp stack, and it has also added its own engine work, including MLX on Apple Silicon. In our benchmark on the same GGUF file, the differences came from defaults (context size and parallel slots), not from the engine itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is vLLM faster than Ollama?&lt;/strong&gt;&lt;br&gt;
A: For many concurrent requests on a GPU, usually yes, because vLLM batches requests continuously and manages KV-cache memory with PagedAttention. For a single user on a laptop, the difference is small, and vLLM is much harder to run on a Mac.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use vLLM with GGUF files?&lt;/strong&gt;&lt;br&gt;
A: Only experimentally. vLLM's docs describe GGUF support as highly experimental and under-optimized, and it requires the separate &lt;code&gt;vllm-gguf-plugin&lt;/code&gt; package. For GGUF models, use llama.cpp or Ollama.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does Ollama slow down with multiple users?&lt;/strong&gt;&lt;br&gt;
A: By default Ollama processes one request per model at a time (&lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt; defaults to 1), so extra requests queue. Raise &lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt;, keeping in mind that each parallel slot needs its own context memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama vs vLLM vs llama.cpp is mostly a concurrency decision, not a single-request speed decision&lt;/li&gt;
&lt;li&gt;On the same GGUF file, default Ollama won for one user (18.7 vs 13.7 tok/s), llama.cpp won under 4 concurrent requests, and &lt;code&gt;OLLAMA_NUM_PARALLEL=4&lt;/code&gt; beat both&lt;/li&gt;
&lt;li&gt;Ollama defaults to &lt;code&gt;OLLAMA_NUM_PARALLEL=1&lt;/code&gt;; llama.cpp auto-sizes parallel slots; vLLM batches continuously&lt;/li&gt;
&lt;li&gt;vLLM takes 92 percent of GPU memory by default and treats GGUF and Apple Silicon as experimental&lt;/li&gt;
&lt;li&gt;All three expose an OpenAI-compatible API, so moving between them is mostly a config change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're choosing between Ollama vs vLLM vs llama.cpp today, start with Ollama on your laptop, measure with the concurrency you actually expect, and move to vLLM when the queue becomes the bottleneck.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>PostgreSQL 19: What's Actually Shipping, What Got Cut</title>
      <dc:creator>Amaresh Pelleti</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:02:53 +0000</pubDate>
      <link>https://dev.to/amareswer/postgresql-19-whats-actually-shipping-what-got-cut-4b1j</link>
      <guid>https://dev.to/amareswer/postgresql-19-whats-actually-shipping-what-got-cut-4b1j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://devtoolhub.com/postgresql-19-whats-shipping-whats-cut/" rel="noopener noreferrer"&gt;DevToolHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;PostgreSQL 19 is late. The project released Beta 4 on September 24, 2026, reverted several headline features along the way, and now plans a release candidate in early October, with general availability possibly also in October. If you planned around SQL/PGQ graph queries, &lt;code&gt;GROUP BY ALL&lt;/code&gt; or partition merge/split, those are out. &lt;code&gt;REPACK&lt;/code&gt;, &lt;code&gt;pg_plan_advice&lt;/code&gt; and parallel autovacuum are still in.&lt;/p&gt;

&lt;p&gt;Here's what changed. It's taken from the PostgreSQL project's own &lt;a href="https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386/" rel="noopener noreferrer"&gt;Beta 4 announcement&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/19/release-19.html" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;, plus Snowflake's &lt;a href="https://www.snowflake.com/en/blog/engineering/postgresql-19-release-delay-feature-reverts/" rel="noopener noreferrer"&gt;engineering writeup&lt;/a&gt; of the revert cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Release Stands Now
&lt;/h2&gt;

&lt;p&gt;The project normally ships a major version every fall, usually in late September. This year, the Beta 4 announcement says the next step is a release candidate "in early October," and that GA "may also occur in October." The announcement explains the delay directly: the community "chose to remove some features from the PostgreSQL 19 release" so the release stays reliable and close to schedule.&lt;/p&gt;

&lt;p&gt;Snowflake's post, written a week before Beta 4, counted 53 reverts since beta began, compared with about 44 for PostgreSQL 18. The difference this year is timing. Many reverts landed late and hit headline features, several after in-depth reviews that used AI tools to find bugs and build reproducible test cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PostgreSQL 19 Reverted
&lt;/h2&gt;

&lt;p&gt;The Beta 4 announcement lists these reverts since Beta 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL/PGQ&lt;/strong&gt; (property graph queries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Online enabling and disabling of data checksums&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporal &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE ... FOR PORTION OF&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE ... MERGE PARTITIONS&lt;/code&gt; and &lt;code&gt;SPLIT PARTITIONS&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The new &lt;strong&gt;&lt;code&gt;pg_get_role_ddl()&lt;/code&gt;, &lt;code&gt;pg_get_tablespace_ddl()&lt;/code&gt; and &lt;code&gt;pg_get_database_ddl()&lt;/code&gt;&lt;/strong&gt; functions (removed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Earlier in the cycle, per Snowflake's writeup, the project also reverted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GROUP BY ALL&lt;/code&gt;&lt;/strong&gt;: a post-commit review found it missed special handling of &lt;code&gt;ORDER BY&lt;/code&gt; entries with nondefault equality semantics, which produced wrong results. It is absent from the current release notes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-text output formats for &lt;code&gt;pg_dumpall&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;JSON_TABLE&lt;/code&gt; &lt;code&gt;ON ERROR&lt;/code&gt; cascading&lt;/strong&gt; to columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast defaults for domains&lt;/strong&gt; with nonvolatile constraints&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logical replication database-specific snapshots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nested query tracking in &lt;code&gt;pg_stat_statements&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Snowflake notes that property graphs, &lt;code&gt;GROUP BY ALL&lt;/code&gt; and partition merge/split are now PostgreSQL 20 material.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note on lz4:&lt;/strong&gt; Snowflake's post lists the default TOAST compression change to &lt;code&gt;lz4&lt;/code&gt; as reverted, citing missing buildfarm support. But the official release notes, dated September 14, still list "Change the default TOAST compression method from pglz to the more efficient lz4." The two sources disagree, so check the final release notes before relying on the new default. You can always set &lt;code&gt;default_toast_compression&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Still Shipping in PostgreSQL 19
&lt;/h2&gt;

&lt;p&gt;These features are in the release notes and have survived the revert cycle so far:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REPACK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reclaims disk space and reorganizes a table, combining &lt;code&gt;VACUUM FULL&lt;/code&gt; and &lt;code&gt;CLUSTER&lt;/code&gt;, with a concurrent option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pg_plan_advice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New extension for stabilizing and controlling query planner decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel autovacuum&lt;/td&gt;
&lt;td&gt;Autovacuum can use parallel workers, set with &lt;code&gt;autovacuum_max_parallel_workers&lt;/code&gt; and a per-table &lt;code&gt;autovacuum_parallel_workers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT DO SELECT ... RETURNING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns conflicting rows instead of doing nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IGNORE NULLS&lt;/code&gt; / &lt;code&gt;RESPECT NULLS&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;For &lt;code&gt;lead()&lt;/code&gt;, &lt;code&gt;lag()&lt;/code&gt;, &lt;code&gt;first_value()&lt;/code&gt;, &lt;code&gt;last_value()&lt;/code&gt; and &lt;code&gt;nth_value()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Snowflake's post, published before Beta 4, flagged &lt;code&gt;REPACK&lt;/code&gt;, the fast-path foreign key checks and &lt;code&gt;postgres_fdw&lt;/code&gt; statistics import as at risk. Beta 4 then shipped "several fixes for the new REPACK command" and "several fixes to the performance improvement feature for the foreign key constraint checks," which suggests both are still in. Nothing is final until GA, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Slipped: More Bugs Found Earlier
&lt;/h2&gt;

&lt;p&gt;Snowflake's writeup points to AI-assisted review as a real factor: more people are using AI tools to find bugs and produce reproducible test cases, and the fixes are large enough that pushing features to a later release made sense. It gives security patching as a parallel example: PostgreSQL used to average a couple of CVEs per release, while the August 2026 patch release for PostgreSQL 18 had 28. The same post notes the project had no official AI contribution policy at the time of writing.&lt;/p&gt;

&lt;p&gt;The takeaway isn't that the release is in trouble. The process is working as designed: features that weren't ready came out, and the reasons are public on the hackers mailing list. If you're troubleshooting a current Postgres deployment in the meantime, our &lt;a href="https://devtoolhub.com/postgresql-troubleshooting-guide/" rel="noopener noreferrer"&gt;PostgreSQL troubleshooting guide&lt;/a&gt; covers the common problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do Before GA
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stay on PostgreSQL 18 for production.&lt;/strong&gt; Snowflake's post calls it the safe upgrade target today, and 19 isn't at release candidate yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep your code for reverted features.&lt;/strong&gt; Look for &lt;code&gt;GROUP BY ALL&lt;/code&gt;, &lt;code&gt;GRAPH_TABLE&lt;/code&gt; / property graph syntax, &lt;code&gt;FOR PORTION OF&lt;/code&gt;, and &lt;code&gt;MERGE PARTITIONS&lt;/code&gt; / &lt;code&gt;SPLIT PARTITION&lt;/code&gt; in migrations, ORM-generated SQL and internal tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the features you plan to use on the beta.&lt;/strong&gt; &lt;code&gt;REPACK&lt;/code&gt; and the foreign key changes were still getting fixes in Beta 4, so test them against your own workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the final release notes at GA&lt;/strong&gt; before relying on anything flagged above, especially the &lt;code&gt;lz4&lt;/code&gt; default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For partitioning without the reverted merge/split commands, our &lt;a href="https://devtoolhub.com/postgresql-partitioning-ultimate-guide/" rel="noopener noreferrer"&gt;PostgreSQL partitioning guide&lt;/a&gt; covers what works today. Also, if you run Postgres on Kubernetes, see &lt;a href="https://devtoolhub.com/postgresql-on-kubernetes-cloudnativepg/" rel="noopener noreferrer"&gt;PostgreSQL on Kubernetes with CloudNativePG&lt;/a&gt; for how major-version upgrades are handled there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: When will PostgreSQL 19 be released?&lt;/strong&gt;&lt;br&gt;
A: The project's Beta 4 announcement (September 24, 2026) plans a release candidate in early October and says general availability may also occur in October. That's later than the usual late-September release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Was GROUP BY ALL removed from PostgreSQL 19?&lt;/strong&gt;&lt;br&gt;
A: Yes. It was reverted after a post-commit review found it mishandled &lt;code&gt;ORDER BY&lt;/code&gt; entries with nondefault equality semantics, producing wrong results. It doesn't appear in the current PostgreSQL 19 release notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is SQL/PGQ graph query support in PostgreSQL 19?&lt;/strong&gt;&lt;br&gt;
A: No. The Beta 4 announcement lists "Revert SQL/PGQ (property graph query) support." Snowflake's writeup describes it as PostgreSQL 20 material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is REPACK still in PostgreSQL 19?&lt;/strong&gt;&lt;br&gt;
A: As of Beta 4, yes. It's in the release notes, and Beta 4 shipped several fixes for it. It was flagged as at risk earlier, so confirm in the final release notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I upgrade to PostgreSQL 19 now?&lt;/strong&gt;&lt;br&gt;
A: Not in production. It's still in beta. Use PostgreSQL 18 for production and test your workloads on the PostgreSQL 19 beta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL 19 Beta 4 shipped September 24, 2026; a release candidate is planned for early October and GA may follow in October&lt;/li&gt;
&lt;li&gt;Reverted: SQL/PGQ, &lt;code&gt;GROUP BY ALL&lt;/code&gt;, partition merge/split, &lt;code&gt;FOR PORTION OF&lt;/code&gt;, online checksum toggling, the new &lt;code&gt;pg_get_*_ddl()&lt;/code&gt; functions and several smaller items&lt;/li&gt;
&lt;li&gt;Still shipping: &lt;code&gt;REPACK&lt;/code&gt;, &lt;code&gt;pg_plan_advice&lt;/code&gt;, parallel autovacuum, &lt;code&gt;ON CONFLICT DO SELECT&lt;/code&gt;, &lt;code&gt;IGNORE NULLS&lt;/code&gt; in window functions&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;lz4&lt;/code&gt; TOAST default is unclear: Snowflake reports it reverted, the September 14 release notes still list it&lt;/li&gt;
&lt;li&gt;Stay on PostgreSQL 18 for production and grep your code for the reverted syntax now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the PostgreSQL 19 release notes again at GA before you finalize a migration plan. They are the source that settles every open question above.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>sql</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
