<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feed/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mohammad Abu Mattar | Code Snippets</title><description>The latest Code Snippets from Mohammad Abu Mattar.</description><link>https://mkabumattar.com/</link><item><title>Bash Retry Function: Automatically Retry Failing Commands with Exponential Backoff</title><link>https://mkabumattar.com/codesnippets/post/bash-retry-function-exponential-backoff/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/bash-retry-function-exponential-backoff/</guid><description>A reusable Bash function that retries any failing command with configurable attempts and exponential backoff. Ideal for wrapping flaky network calls, AWS CLI commands, or deployment scripts in CI/CD pipelines.</description><pubDate>Mon, 20 Jul 2026 12:29:14 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wrap any flaky command in one reusable &lt;code&gt;retry&lt;/code&gt; function and stop re-running red pipelines by hand.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some commands fail for reasons that have nothing to do with your code. A registry hiccups, an API rate-limits you for a second, a DNS lookup blips, an AWS endpoint returns a &lt;code&gt;503&lt;/code&gt;. Run the exact same command again and it works. That&amp;#39;s a transient failure, and it&amp;#39;s everywhere in scripts that touch the network.&lt;/p&gt;
&lt;h3&gt;Why blind retries make it worse&lt;/h3&gt;
&lt;p&gt;The naive fix is a quick loop that retries immediately, but that often makes things worse. If a service is already struggling, a tight retry loop hammers it harder. Worse, if a hundred CI jobs all fail at the same moment and all retry at the same moment, they come back in a synchronized wave. That&amp;#39;s the thundering herd, and it can keep a recovering service down.&lt;/p&gt;
&lt;h3&gt;The real-world impact&lt;/h3&gt;
&lt;p&gt;The everyday cost is a pipeline that goes red for no real reason, so someone re-runs it by hand and loses ten minutes and their focus. The worse cost is a deploy script that gives up on the first blip and leaves a release half-applied. Both come from treating a temporary failure as a permanent one.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Retry the command a few times, but wait longer between each attempt, and add a little randomness so retries spread out instead of stacking up. That&amp;#39;s exponential backoff with jitter, and it fits in one small Bash function you can wrap around anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retry any command with a configurable number of attempts.&lt;/li&gt;
&lt;li&gt;Back off exponentially (1s, 2s, 4s, 8s) so you stop hammering a struggling service.&lt;/li&gt;
&lt;li&gt;Add jitter so a fleet of scripts doesn&amp;#39;t retry in lockstep.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Script Implementation&lt;/h2&gt;
&lt;h3&gt;Setup and defaults&lt;/h3&gt;
&lt;p&gt;Start with a safe shell, then read the knobs from the environment so the same function works in a laptop shell and a CI job without editing the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash
# retry.sh - retry a command with exponential backoff and jitter.
set -euo pipefail

# Tunables (override via environment):
: &amp;quot;${RETRY_MAX_ATTEMPTS:=5}&amp;quot;   # how many tries before giving up
: &amp;quot;${RETRY_BASE_DELAY:=1}&amp;quot;     # first backoff, in seconds
: &amp;quot;${RETRY_MAX_DELAY:=60}&amp;quot;     # cap so backoff never runs away
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The retry function&lt;/h3&gt;
&lt;p&gt;The function takes the command and its arguments as &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;, so it wraps anything without quoting gymnastics. It returns the command&amp;#39;s own exit code on success, and the last exit code if it runs out of attempts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;retry() {
  local attempt=1
  while true; do
    # Run the command exactly as passed. On success, we&amp;#39;re done.
    if &amp;quot;$@&amp;quot;; then
      return 0
    fi
    local exit_code=$?

    # Out of attempts: surface the failure clearly and pass the code up.
    if ((attempt &amp;gt;= RETRY_MAX_ATTEMPTS)); then
      echo &amp;quot;retry: &amp;#39;$*&amp;#39; failed after ${attempt} attempts (exit ${exit_code})&amp;quot; &amp;gt;&amp;amp;2
      return &amp;quot;$exit_code&amp;quot;
    fi

    # Exponential backoff: base * 2^(attempt-1), capped at max delay.
    local delay=$((RETRY_BASE_DELAY * 2 ** (attempt - 1)))
    ((delay &amp;gt; RETRY_MAX_DELAY)) &amp;amp;&amp;amp; delay=$RETRY_MAX_DELAY

    # Full jitter: sleep a random amount between 0 and delay.
    local wait=$((RANDOM % (delay + 1)))
    echo &amp;quot;retry: attempt ${attempt}/${RETRY_MAX_ATTEMPTS} failed (exit ${exit_code}); waiting ${wait}s&amp;quot; &amp;gt;&amp;amp;2
    sleep &amp;quot;$wait&amp;quot;
    ((attempt++))
  done
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why the jitter matters&lt;/h3&gt;
&lt;p&gt;The backoff line grows the wait each round, and the cap stops it from turning a fifth attempt into a multi-minute stall. The jitter line is the part people skip, and it&amp;#39;s the one that prevents the thundering herd. Instead of every caller waiting exactly 4 seconds and retrying at the same instant, each one waits a random slice of that window, so the load spreads out and the recovering service gets room to breathe.&lt;/p&gt;
&lt;h3&gt;Entry point&lt;/h3&gt;
&lt;p&gt;If you run the file directly it retries whatever you pass on the command line. If you source it, you just get the &lt;code&gt;retry&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Run directly: retry.sh &amp;lt;command...&amp;gt;
if [[ &amp;quot;${BASH_SOURCE[0]}&amp;quot; == &amp;quot;${0}&amp;quot; ]]; then
  if (($# == 0)); then
    echo &amp;quot;usage: retry.sh &amp;lt;command&amp;gt; [args...]&amp;quot; &amp;gt;&amp;amp;2
    exit 64
  fi
  retry &amp;quot;$@&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage and Benefits&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One function covers every flaky command in your scripts, so you stop copy-pasting &lt;code&gt;sleep&lt;/code&gt; loops and you get consistent logging for free. Because it wraps &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;, it does not care whether the command is &lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt;, or a script of your own.&lt;/p&gt;
&lt;h3&gt;Real invocations&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Wrap an AWS CLI upload that sometimes hits throttling.
retry aws s3 cp ./build.tar.gz s3://artifacts/build.tar.gz

# Wrap a health check that races a service starting up.
retry curl -fsS https://api.example.com/health

# Tune it inline for a slow, important step.
RETRY_MAX_ATTEMPTS=8 RETRY_BASE_DELAY=2 retry ./deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sourcing it in CI/CD&lt;/h3&gt;
&lt;p&gt;Keep &lt;code&gt;retry.sh&lt;/code&gt; in a shared scripts library and source it at the top of your pipeline steps, so every job gets the same behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;source ./scripts/retry.sh
retry terraform apply -auto-approve
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;How the function compares to the other common options.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Approach&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Backoff&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Jitter&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Works for any command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;retry()&lt;/code&gt; function&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes, exponential&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Tight &lt;code&gt;until&lt;/code&gt; loop&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;No&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;No&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;curl --retry N&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes (curl 7.66+)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Only with &lt;code&gt;--retry-all-errors&lt;/code&gt; extras&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;No, curl only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;AWS CLI built-in retries&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes (adaptive mode)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;No, AWS CLI only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Tool-specific retries are great when you&amp;#39;re already in that tool, but the function is the one thing that wraps all of them the same way.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Why use exponential backoff instead of a fixed delay?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;A fixed delay treats a one-second blip and a thirty-second outage the same way. Exponential backoff waits a little after the first failure and progressively longer after each one, so you recover fast from a quick hiccup but stop hammering a service that&amp;#39;s genuinely down. The cap (&lt;code&gt;RETRY_MAX_DELAY&lt;/code&gt;) keeps the later waits from becoming a stall.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What is jitter and why does it prevent a thundering herd?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Jitter is deliberate randomness added to the wait time. Without it, every script that failed at the same moment retries at the same moment, hitting the recovering service in a synchronized wave. With full jitter, each caller sleeps a random amount between zero and the backoff window, so the retries spread out and the load smooths.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I retry only on certain failures, not every non-zero exit?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Wrap the command in a small function that maps the failures you care about to a non-zero exit and everything else to zero. For example, treat a &lt;code&gt;curl&lt;/code&gt; exit of 22 (HTTP error) or 28 (timeout) as retryable and return success for the rest, then pass that wrapper to &lt;code&gt;retry&lt;/code&gt;. That keeps the retry loop generic while you decide what &amp;quot;failure&amp;quot; means.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Does this work in POSIX sh or only Bash?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;It uses Bash features: &lt;code&gt;RANDOM&lt;/code&gt;, &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;((...))&lt;/code&gt; arithmetic with &lt;code&gt;**&lt;/code&gt;, and &lt;code&gt;BASH_SOURCE&lt;/code&gt;. In a strict POSIX &lt;code&gt;sh&lt;/code&gt; you&amp;#39;d swap &lt;code&gt;RANDOM&lt;/code&gt; for something like &lt;code&gt;awk&lt;/code&gt; or &lt;code&gt;/dev/urandom&lt;/code&gt;, replace &lt;code&gt;local&lt;/code&gt;, and compute the power manually. If you can run &lt;code&gt;#!/usr/bin/env bash&lt;/code&gt;, keep it as is; it&amp;#39;s simpler and clearer.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How is this different from curl --retry or the AWS CLI&amp;#39;s own retries?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those are excellent inside their own tool. &lt;code&gt;curl --retry&lt;/code&gt; retries HTTP calls; the AWS CLI has adaptive retry modes for API throttling. The Bash function is the layer above: it retries anything, including your own scripts and combinations of commands, with one consistent policy and one consistent log format.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I cap the total time spent retrying?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Either lower &lt;code&gt;RETRY_MAX_ATTEMPTS&lt;/code&gt; and &lt;code&gt;RETRY_MAX_DELAY&lt;/code&gt; so the worst case is bounded, or wrap the call in GNU &lt;code&gt;timeout&lt;/code&gt;, for example &lt;code&gt;timeout 120 retry ./deploy.sh&lt;/code&gt;. The &lt;code&gt;timeout&lt;/code&gt; approach gives you a hard ceiling regardless of how the backoff math works out.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/bash.html#Shell-Arithmetic&quot;&gt;Bash Reference Manual: shell arithmetic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/bash.html#Bash-Variables&quot;&gt;Bash Reference Manual: RANDOM and special parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/&quot;&gt;AWS Architecture Blog: exponential backoff and jitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-retries.html&quot;&gt;AWS CLI: retries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://curl.se/docs/manpage.html#--retry&quot;&gt;curl manual: --retry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html&quot;&gt;GNU coreutils: timeout&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>devops</category><category>shell-scripting</category><category>automation</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0015-bash-retry-function-exponential-backoff/hero.png" length="0" type="image/jpeg"/></item><item><title>Node.js Environment Variable Validation with Zod at Startup</title><link>https://mkabumattar.com/codesnippets/post/nodejs-env-validation-zod-startup/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/nodejs-env-validation-zod-startup/</guid><description>Stop trusting process.env blindly. This Node.js and TypeScript snippet validates every environment variable at startup with a Zod schema, coerces strings into real types, and refuses to boot on bad config so you catch mistakes before a single request is served.</description><pubDate>Wed, 27 May 2026 20:02:59 GMT</pubDate><content:encoded>&lt;p&gt;Most Node.js apps treat &lt;code&gt;process.env&lt;/code&gt; like a trusted friend. You reach into it whenever you need a value, assume the key is there, assume it&amp;#39;s spelled right, and assume the string is actually the type you want. Since &lt;code&gt;process.env&lt;/code&gt; hands you everything as a plain string, you end up parsing ports into integers and turning &lt;code&gt;&amp;quot;false&amp;quot;&lt;/code&gt; into a real boolean all over the codebase.&lt;/p&gt;
&lt;p&gt;The fix is small and boring in the best way: validate your environment once, at startup, with a schema. If the config is wrong, the app refuses to boot and tells you exactly what&amp;#39;s broken before a single request is served. Here&amp;#39;s how to do it with Zod and a little bit of TypeScript.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Why does reading straight from &lt;code&gt;process.env&lt;/code&gt; keep biting you?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Silent Configuration Drift&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt; When you build traditional Node.js apps, it&amp;#39;s incredibly common to grab values straight from &lt;code&gt;process.env&lt;/code&gt; whenever and wherever you need them. You probably load these into your environment from a local file, but that setup never actually checks if your data makes sense. Your code just assumes every setting is present, spelled correctly, and formatted right. And because &lt;code&gt;process.env&lt;/code&gt; treats everything as a string, you find yourself parsing variables by hand all over the place. That&amp;#39;s where subtle bugs creep in: maybe you forgot to parse a port into an integer, or a string that says &lt;code&gt;&amp;quot;false&amp;quot;&lt;/code&gt; gets evaluated as truthy.&lt;/p&gt;
&lt;h3&gt;The Real-World Impact&lt;/h3&gt;
&lt;p&gt;When these settings break, your application usually doesn&amp;#39;t crash right away. Instead, you get painful runtime errors much later. Miss a database URL or an API key, and your server might start up perfectly fine. It&amp;#39;s only hours later, when a user triggers a specific request, that the app suddenly blows up. These delayed failures make debugging a nightmare because the crash happens far from the actual mistake.&lt;/p&gt;
&lt;Notice type=&quot;warning&quot;&gt;
  Hunting for a one-character typo in your environment config during a live
  production outage is exactly the kind of 2 AM stress nobody signed up for. The
  earlier you catch it, the cheaper it is.
&lt;/Notice&gt;&lt;hr&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you catch bad config before the app even starts?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;The Startup Schema Assertion&lt;/h3&gt;
&lt;p&gt;To put an end to this, set up a validation schema that runs the second your application boots. With Zod, you write a clear, strict schema describing exactly what your environment should look like. This runs before any of your server logic starts, so an invalid configuration won&amp;#39;t let the app launch at all. Zod acts like a smart gateway: it checks the values and automatically converts strings into real numbers, booleans, or objects.&lt;/p&gt;
&lt;p&gt;It helps to picture it as a mapping. Your raw environment &lt;Math math=&quot;E_{\text{raw}}&quot; /&gt; is just string keys paired with string values pulled from &lt;Math math=&quot;\text{process.env}&quot; /&gt;:&lt;/p&gt;
&lt;p&gt;&lt;Math
  display
  math=&quot;E_{\text{raw}} = \{\, (k_i, s_i) \mid k_i \in \text{Keys},\ s_i \in \text{String} \,\}&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;Zod&amp;#39;s parser &lt;Math math=&quot;f_{\text{Zod}}&quot; /&gt; maps that loose, unstructured data into a strictly typed configuration space &lt;Math math=&quot;E_{\text{validated}}&quot; /&gt;:&lt;/p&gt;
&lt;Math display math=&quot;f_{\text{Zod}}: E_{\text{raw}} \to E_{\text{validated}}&quot; /&gt;&lt;p&gt;&lt;Math
  display
  math=&quot;E_{\text{validated}} = \{\, (k_i, v_i) \mid v_i \in T(k_i) \,\}&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;where &lt;Math math=&quot;T(k_i)&quot; /&gt; is the valid typed domain for each key, such as &lt;Math math=&quot;\mathbb{N}&quot; /&gt; for ports, a format-compliant URI for the database connection, or a value from a fixed enum.&lt;/p&gt;
&lt;Notice type=&quot;tip&quot;&gt;
  **The Fix:** one schema, verified the moment your app turns on. The rest of
  your system then works with safe, fully typed values instead of messy raw
  strings.
&lt;/Notice&gt;&lt;h3&gt;Quick Takeaways&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Crash early and safely:&lt;/strong&gt; the app won&amp;#39;t boot if any required setting is missing or invalid.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic type conversion:&lt;/strong&gt; raw strings become real numbers, booleans, and validated URLs, fully typed for TypeScript.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single source of truth:&lt;/strong&gt; all configuration checks live in one place, so validation logic isn&amp;#39;t scattered everywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Where This Fits in Your Project&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Before the code, here&amp;#39;s where each piece lives.&lt;/em&gt;&lt;/p&gt;
&lt;FileTree&gt;&lt;ul&gt;
&lt;li&gt;your-app/&lt;ul&gt;
&lt;li&gt;src/&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;env.ts&lt;/strong&gt; the schema and the validated env export&lt;/li&gt;
&lt;li&gt;index.ts imports env at the very top&lt;/li&gt;
&lt;li&gt;server.ts uses the typed config&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;.env local values, gitignored&lt;/li&gt;
&lt;li&gt;.env.example documented keys, committed&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/FileTree&gt;&lt;p&gt;The whole trick is that &lt;code&gt;env.ts&lt;/code&gt; runs its validation on import, so importing it from your entry file is what makes a broken config stop the boot.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Building the Validator&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Four small pieces, wired together once.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;First, install Zod (and &lt;code&gt;dotenv&lt;/code&gt; so you can load a local &lt;code&gt;.env&lt;/code&gt; while developing):&lt;/p&gt;
&lt;Tabs&gt;&lt;Tab name=&quot;npm&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install zod dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;/Tab&gt;&lt;Tab name=&quot;pnpm&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add zod dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;/Tab&gt;&lt;Tab name=&quot;yarn&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yarn add zod dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;/Tab&gt;&lt;Tab name=&quot;bun&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun add zod dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;/Tab&gt;&lt;/Tabs&gt;&lt;p&gt;Now build it up step by step.&lt;/p&gt;
&lt;Steps&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Load the local environment.&lt;/strong&gt; Pull &lt;code&gt;.env&lt;/code&gt; into &lt;code&gt;process.env&lt;/code&gt; before anything else runs, and bring in Zod.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import dotenv from &amp;#39;dotenv&amp;#39;;
import {z} from &amp;#39;zod&amp;#39;;

// Load our local .env file before we validate anything
dotenv.config();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Declare a strict schema.&lt;/strong&gt; Spell out every variable you need and the type you expect. Zod handles strings, coerced numbers, booleans, and custom enums for you, and &lt;code&gt;z.infer&lt;/code&gt; gives you a matching TypeScript type for free.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Define the validation rules and expected types for our variables
export const envSchema = z.object({
  NODE_ENV: z
    .enum([&amp;#39;development&amp;#39;, &amp;#39;production&amp;#39;, &amp;#39;staging&amp;#39;, &amp;#39;test&amp;#39;])
    .default(&amp;#39;development&amp;#39;),
  PORT: z.coerce.number().min(1).max(65535).default(3000),
  DATABASE_URL: z
    .string()
    .url({message: &amp;#39;DATABASE_URL must be a valid connection URL&amp;#39;}),
  JWT_SECRET: z
    .string()
    .min(32, {message: &amp;#39;JWT_SECRET must be at least 32 characters long&amp;#39;}),
  ENABLE_LOGS: z.preprocess((val) =&amp;gt; val === &amp;#39;true&amp;#39;, z.boolean()).default(true),
});

// Infer the strict TypeScript type directly from our schema
export type EnvConfig = z.infer&amp;lt;typeof envSchema&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Parse once and fail loudly.&lt;/strong&gt; Run the schema against &lt;code&gt;process.env&lt;/code&gt;. If anything is wrong, print a clean list of issues and exit before the server starts.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Parse the environment and handle any validation failures gracefully
const validateEnv = (): EnvConfig =&amp;gt; {
  const result = envSchema.safeParse(process.env);

  if (!result.success) {
    console.error(&amp;#39;Environment validation failed during boot:&amp;#39;);

    // Print each issue clearly without leaking any sensitive data
    result.error.issues.forEach((issue) =&amp;gt; {
      const propertyPath = issue.path.join(&amp;#39;.&amp;#39;);
      console.error(`  - [${propertyPath}]: ${issue.message}`);
    });

    // Terminate the process immediately
    process.exit(1);
  }

  return result.data;
};

// Export our validated configuration so the rest of the app can use it
export const env = validateEnv();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;Import it at the very top of your entry file.&lt;/strong&gt; Because validation runs on import, a broken config stops the process before &lt;code&gt;app.listen&lt;/code&gt; is ever reached.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import {env} from &amp;#39;./env&amp;#39;;
import express from &amp;#39;express&amp;#39;;

const app = express();

// The port is now guaranteed to be a valid, parsed number
app.listen(env.PORT, () =&amp;gt; {
  console.log(`Server is running in ${env.NODE_ENV} mode on port ${env.PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/Steps&gt;&lt;hr&gt;
&lt;h2&gt;Usage and Benefits&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;What do you actually get out of this?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Architectural Advantages&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt; This pattern saves you from writing repetitive null checks and manual parsing logic throughout your codebase. You don&amp;#39;t have to wonder whether a variable is undefined or whether a port was correctly parsed into a number. Some developers prefer to extend the global &lt;code&gt;ProcessEnv&lt;/code&gt; type in TypeScript for editor autocomplete, but that only helps at compile time. It won&amp;#39;t stop a runtime error when a variable is missing or a boolean string is parsed incorrectly. By validating at startup, you get both editor autocomplete and guaranteed runtime safety.&lt;/p&gt;
&lt;h3&gt;Diagnostic Console Output&lt;/h3&gt;
&lt;p&gt;If you miss a variable or format one incorrectly, the app halts immediately and prints a neat, readable error message. This feedback makes it easy to see exactly what went wrong so you can fix it right away.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ npm run dev

Environment validation failed during boot:
  - [DATABASE_URL]: DATABASE_URL must be a valid connection URL
  - [PORT]: Number must be less than or equal to 65535
  - [JWT_SECRET]: JWT_SECRET must be at least 32 characters long
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;info&quot;&gt;
  Notice the output shows the field path and the message, never the value. A bad
  `JWT_SECRET` or `DATABASE_URL` won&apos;t end up in your production logs.
&lt;/Notice&gt;&lt;hr&gt;
&lt;h2&gt;Comparing Validation Approaches&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Is Zod really the best fit, or just the popular one?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a quick look at how Zod stacks up against other common ways developers handle configuration checks.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Validation Pattern&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Type Safety (TypeScript)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Automatic Coercion&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Boilerplate Required&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Error Output Detail&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zod Schema&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native type inference&lt;/td&gt;
&lt;td&gt;High, built-in&lt;/td&gt;
&lt;td&gt;Low definition overhead&lt;/td&gt;
&lt;td&gt;Detailed, structured issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Joi Library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires manual types&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Medium validation block&lt;/td&gt;
&lt;td&gt;Detailed schema errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom Script&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual type assertions&lt;/td&gt;
&lt;td&gt;Manual conversion code&lt;/td&gt;
&lt;td&gt;High custom coding&lt;/td&gt;
&lt;td&gt;Whatever you write yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Manual scripts and older libraries get the job done, but Zod gives you full TypeScript inference, automatic coercion, and clean error reporting with almost no extra work, which makes it a great fit for modern Node.js setups.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Should I validate environment variables in every file or just once?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Just once, at startup. Validate in a single &lt;code&gt;env.ts&lt;/code&gt; module, export the typed &lt;code&gt;env&lt;/code&gt; object, and import that everywhere else instead of reading &lt;code&gt;process.env&lt;/code&gt; directly. Because the validation runs on import, the check happens exactly one time as the app boots, and every other file consumes already-safe values.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I handle optional variables or sensible defaults?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use Zod&amp;#39;s &lt;code&gt;.optional()&lt;/code&gt; for values that may be absent and &lt;code&gt;.default(...)&lt;/code&gt; for ones that should fall back to a known value. For example, &lt;code&gt;PORT: z.coerce.number().default(3000)&lt;/code&gt; means a missing &lt;code&gt;PORT&lt;/code&gt; quietly becomes &lt;code&gt;3000&lt;/code&gt;, while &lt;code&gt;DATABASE_URL: z.string().url()&lt;/code&gt; stays required and fails the boot if it&amp;#39;s missing or malformed.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Does this give me autocomplete on my config?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, that&amp;#39;s one of the best parts. &lt;code&gt;z.infer&amp;lt;typeof envSchema&amp;gt;&lt;/code&gt; produces a TypeScript type that matches your schema exactly, so the exported &lt;code&gt;env&lt;/code&gt; object is fully typed: &lt;code&gt;env.PORT&lt;/code&gt; is a &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;env.NODE_ENV&lt;/code&gt; is the enum, and so on. You get autocomplete and compile-time checks without manually maintaining a separate type.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Won&amp;#39;t printing validation errors leak my secrets?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;No. The error handler only logs each issue&amp;#39;s path and message (for example &lt;code&gt;[JWT_SECRET]: must be at least 32 characters long&lt;/code&gt;), never the actual value. Your secrets stay out of the logs even when validation fails, which keeps the output safe to surface in CI and production.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Can I use this in a frontend or browser bundle?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;This pattern is built for server-side Node.js. In the browser, secrets should never ship in the bundle at all, so rely on your framework&amp;#39;s public-env mechanism instead (like &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; variables or &lt;code&gt;import.meta.env&lt;/code&gt;). Keep this Zod schema on the server, and only expose the handful of values that are genuinely safe for the client.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How is this different from envalid or Joi?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;The core idea is the same: assert your config against a schema at startup. Zod&amp;#39;s edge is native TypeScript inference (no separate type definitions), built-in coercion, and the fact that many projects already use Zod for request and form validation. Reusing one library for both means less to learn and one consistent way to describe data.&lt;/p&gt;
&lt;/Accordion&gt;&lt;hr&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/roshan_ican/validating-environment-variables-in-nodejs-with-zod-2epn&quot;&gt;Validating Environment Variables in Node.js with Zod - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.creatures.sh/blog/env-type-safety-and-validation/&quot;&gt;Environment variables type safety and validation with Zod - creatures.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/schead/ensuring-environment-variable-integrity-with-zod-in-typescript-3di5&quot;&gt;Ensuring Environment Variable Integrity with Zod in TypeScript - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mingyang-li.medium.com/validating-environment-variables-like-a-pro-using-zod-in-node-js-1287f81c8350&quot;&gt;Validating Environment Variables Like a Pro: Using Zod in Node.js - Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/whoffagents/type-safe-environment-variables-in-nodejs-with-zod-570i&quot;&gt;Type-Safe Environment Variables in Node.js with Zod - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oneuptime.com/blog/post/2026-01-06-nodejs-production-environment-variables/view&quot;&gt;How to Configure Node.js for Production with Environment Variables - OneUptime&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>nodejs</category><category>typescript</category><category>backend</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0014-nodejs-env-validation-zod-startup/hero.png" length="0" type="image/jpeg"/></item><item><title>AWS EC2 Instance Management with Boto3: Start, Stop, and Query Instances</title><link>https://mkabumattar.com/codesnippets/post/aws-ec2-instance-management-boto3-python/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/aws-ec2-instance-management-boto3-python/</guid><description>Learn how to automate AWS EC2 instance management using Python and Boto3. This guide covers authentication with IAM roles, starting and stopping instances, using waiters, filtering by tags, running bulk operations, and handling API errors. Practical code examples included for DevOps engineers and cloud developers</description><pubDate>Tue, 14 Apr 2026 20:02:59 GMT</pubDate><content:encoded>&lt;p&gt;If you&amp;#39;ve ever spent 20 minutes clicking through the AWS Console just to stop a handful of dev instances, you already know the pain. It&amp;#39;s tedious, it doesn&amp;#39;t scale, and one wrong click can ruin your afternoon.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s where Boto3 comes in. It&amp;#39;s the official AWS SDK for Python, and it lets you talk to AWS services, including EC2, directly from your code. Instead of pointing and clicking, you write a script once and run it whenever you need it. Your code doesn&amp;#39;t get tired, doesn&amp;#39;t misclick, and works the same at 3 AM as it does at 3 PM.&lt;/p&gt;
&lt;p&gt;This guide covers everything you need to get comfortable with EC2 automation: setting up authentication, starting and stopping instances, waiting for state changes, filtering by tags, running bulk operations, and handling errors like a pro.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Why Automate EC2 Management with Boto3?&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Why bother writing code when the AWS Console already does the job?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fair question. The console is fine when you&amp;#39;ve got two or three instances. But once you&amp;#39;re managing a real environment, manual work starts costing you more than time.&lt;/p&gt;
&lt;p&gt;Think about it: every action you take through the console depends on a human doing it right, every single time. With Boto3, you write the logic once, and it runs consistently regardless of who&amp;#39;s on call or how tired they are.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s where automation pays off most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scheduled shutdowns:&lt;/strong&gt; Stop dev and staging instances overnight or on weekends. There&amp;#39;s no reason to pay for servers that nobody&amp;#39;s using at 2 AM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent tagging:&lt;/strong&gt; Every instance your script touches gets tagged correctly. No more missing &lt;code&gt;Environment&lt;/code&gt; or &lt;code&gt;Owner&lt;/code&gt; tags because someone forgot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incident response:&lt;/strong&gt; When something goes wrong, your runbook can stop a compromised instance or spin up a replacement without waiting for a human to log in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit trails:&lt;/strong&gt; Log every action with timestamps, so you&amp;#39;ve got a clear record of what happened, when, and why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer mistakes:&lt;/strong&gt; Repetitive manual work breeds errors. Scripts don&amp;#39;t forget steps or click the wrong button.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once your automation is in place, managing 200 instances takes roughly the same effort as managing two.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Setting Up Boto3 and Authenticating with AWS&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you connect Boto3 to your AWS account securely?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Start with the install:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install boto3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, authentication. This is where a lot of people make mistakes, so it&amp;#39;s worth doing right from the start. The method you use should depend on where your code runs.&lt;/p&gt;
&lt;h3&gt;Option 1: IAM Roles (Best for EC2 and Lambda)&lt;/h3&gt;
&lt;p&gt;If your script runs on an EC2 instance or a Lambda function, attach an IAM role to that resource. Boto3 picks up the credentials automatically from the instance metadata service. You don&amp;#39;t need to touch a key file or set any environment variables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

# No keys needed here Boto3 reads the IAM role attached to this instance
ec2_client = boto3.client(&amp;#39;ec2&amp;#39;, region_name=&amp;#39;us-east-1&amp;#39;)

# Quick check to confirm the connection works
response = ec2_client.describe_instances()
print(&amp;#39;Connected via IAM role good to go.&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the cleanest approach for production. There are no credentials to rotate, leak, or accidentally commit to Git.&lt;/p&gt;
&lt;h3&gt;Option 2: Named Profiles (Best for Local Development)&lt;/h3&gt;
&lt;p&gt;When you&amp;#39;re working locally, use a named AWS CLI profile. It keeps credentials in &lt;code&gt;~/.aws/credentials&lt;/code&gt; where they belong, not in your source code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Run this once in your terminal to set up a profile
aws configure --profile myproject
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

# Load credentials from the &amp;#39;myproject&amp;#39; profile in ~/.aws/credentials
session = boto3.Session(profile_name=&amp;#39;myproject&amp;#39;)
ec2_client = session.client(&amp;#39;ec2&amp;#39;, region_name=&amp;#39;us-east-1&amp;#39;)

print(f&amp;#39;Using profile: {session.profile_name}&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can have multiple profiles for different accounts or environments, which makes switching between dev and prod much easier.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A word on hardcoded credentials:&lt;/strong&gt; Don&amp;#39;t do it. Not in scripts, not in config files checked into source control, not anywhere. Use IAM roles in production and named profiles locally. If you&amp;#39;re using access keys, rotate them regularly and give them only the permissions they actually need.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Starting and Stopping EC2 Instances&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you start and stop instances programmatically with Boto3?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is probably why you&amp;#39;re here, so let&amp;#39;s get into it. Boto3 uses &lt;code&gt;start_instances&lt;/code&gt; and &lt;code&gt;stop_instances&lt;/code&gt; for these operations. Both are simple calls, but there are a few details worth knowing.&lt;/p&gt;
&lt;h3&gt;Stopping an Instance&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
from botocore.exceptions import ClientError

def stop_instance(instance_id: str, region: str = &amp;#39;us-east-1&amp;#39;) -&amp;gt; dict:
    &amp;quot;&amp;quot;&amp;quot;Stop a running EC2 instance and return the state transition.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    try:
        response = ec2.stop_instances(InstanceIds=[instance_id])

        # AWS tells us both the old state and the new state
        state_info = response[&amp;#39;StoppingInstances&amp;#39;][0]
        previous = state_info[&amp;#39;PreviousState&amp;#39;][&amp;#39;Name&amp;#39;]
        current = state_info[&amp;#39;CurrentState&amp;#39;][&amp;#39;Name&amp;#39;]

        print(f&amp;#39;{instance_id}: {previous} -&amp;gt; {current}&amp;#39;)
        return response

    except ClientError as e:
        print(f&amp;#39;Could not stop instance: {e.response[&amp;quot;Error&amp;quot;][&amp;quot;Code&amp;quot;]} - {e}&amp;#39;)
        raise

# Usage
stop_instance(&amp;#39;i-0abcd1234ef567890&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Starting an Instance&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
from botocore.exceptions import ClientError

def start_instance(instance_id: str, region: str = &amp;#39;us-east-1&amp;#39;) -&amp;gt; dict:
    &amp;quot;&amp;quot;&amp;quot;Start a stopped EC2 instance and return the state transition.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    try:
        response = ec2.start_instances(InstanceIds=[instance_id])

        state_info = response[&amp;#39;StartingInstances&amp;#39;][0]
        previous = state_info[&amp;#39;PreviousState&amp;#39;][&amp;#39;Name&amp;#39;]
        current = state_info[&amp;#39;CurrentState&amp;#39;][&amp;#39;Name&amp;#39;]

        print(f&amp;#39;{instance_id}: {previous} -&amp;gt; {current}&amp;#39;)
        return response

    except ClientError as e:
        code = e.response[&amp;#39;Error&amp;#39;][&amp;#39;Code&amp;#39;]

        # Don&amp;#39;t crash if the instance is already running
        if code == &amp;#39;IncorrectInstanceState&amp;#39;:
            print(f&amp;#39;{instance_id} is already in the desired state.&amp;#39;)
        else:
            raise

# Usage
start_instance(&amp;#39;i-0abcd1234ef567890&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing worth noting: both methods accept a list of instance IDs, so you can act on multiple instances in a single API call. If you&amp;#39;re managing more than one instance, that&amp;#39;s a lot cleaner than looping and calling separately.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Stop multiple instances at once no loop needed
ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=&amp;#39;us-east-1&amp;#39;)
ec2.stop_instances(InstanceIds=[
    &amp;#39;i-0abcd1234ef567890&amp;#39;,
    &amp;#39;i-0efgh5678ij901234&amp;#39;,
    &amp;#39;i-0klmn9012op345678&amp;#39;,
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response tells you the previous and current state for each one, which makes logging and auditing easy.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Using Boto3 Waiters to Block Until a State is Reached&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you know when an instance has actually finished starting or stopping?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a common mistake: you call &lt;code&gt;start_instances&lt;/code&gt;, assume it&amp;#39;s done, and immediately try to SSH in. But the instance is still booting. Your script fails, and now you&amp;#39;re debugging something that wasn&amp;#39;t actually broken.&lt;/p&gt;
&lt;p&gt;The fix is waiters. A Boto3 waiter is a built-in polling loop that keeps checking the AWS API until your instance reaches the state you want. It handles the timing for you and raises an error if something goes wrong or takes too long.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
from botocore.exceptions import WaiterError

def start_and_wait(instance_id: str, region: str = &amp;#39;us-east-1&amp;#39;):
    &amp;quot;&amp;quot;&amp;quot;Start an instance and block until it&amp;#39;s fully running.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    ec2.start_instances(InstanceIds=[instance_id])
    print(f&amp;#39;Starting {instance_id}...&amp;#39;)

    waiter = ec2.get_waiter(&amp;#39;instance_running&amp;#39;)

    try:
        waiter.wait(
            InstanceIds=[instance_id],
            WaiterConfig={
                &amp;#39;Delay&amp;#39;: 15,       # Check every 15 seconds
                &amp;#39;MaxAttempts&amp;#39;: 40  # Give up after ~10 minutes
            }
        )
        print(f&amp;#39;{instance_id} is running.&amp;#39;)

    except WaiterError as e:
        print(f&amp;#39;Waiter timed out: {e}&amp;#39;)
        raise


def stop_and_wait(instance_id: str, region: str = &amp;#39;us-east-1&amp;#39;):
    &amp;quot;&amp;quot;&amp;quot;Stop an instance and block until it&amp;#39;s fully stopped.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    ec2.stop_instances(InstanceIds=[instance_id])
    print(f&amp;#39;Stopping {instance_id}...&amp;#39;)

    waiter = ec2.get_waiter(&amp;#39;instance_stopped&amp;#39;)

    try:
        waiter.wait(
            InstanceIds=[instance_id],
            WaiterConfig={&amp;#39;Delay&amp;#39;: 15, &amp;#39;MaxAttempts&amp;#39;: 40}
        )
        print(f&amp;#39;{instance_id} is stopped.&amp;#39;)

    except WaiterError as e:
        print(f&amp;#39;Waiter failed: {e}&amp;#39;)
        raise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The four most useful EC2 waiters are &lt;code&gt;instance_running&lt;/code&gt;, &lt;code&gt;instance_stopped&lt;/code&gt;, &lt;code&gt;instance_terminated&lt;/code&gt;, and &lt;code&gt;instance_exists&lt;/code&gt;. Each one polls &lt;code&gt;describe_instances&lt;/code&gt; under the hood and checks the state automatically, so you don&amp;#39;t have to.&lt;/p&gt;
&lt;p&gt;If you want to wait on multiple instances at once, just pass all the IDs together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Wait for several instances to stop one waiter call handles all of them
waiter = ec2.get_waiter(&amp;#39;instance_stopped&amp;#39;)
waiter.wait(
    InstanceIds=[&amp;#39;i-0abc123&amp;#39;, &amp;#39;i-0def456&amp;#39;, &amp;#39;i-0ghi789&amp;#39;],
    WaiterConfig={&amp;#39;Delay&amp;#39;: 15, &amp;#39;MaxAttempts&amp;#39;: 40}
)
print(&amp;#39;All instances are stopped.&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Tune &lt;code&gt;Delay&lt;/code&gt; and &lt;code&gt;MaxAttempts&lt;/code&gt; based on your actual instances. A small instance with a simple AMI might be running in under a minute. A larger one running a heavy bootstrap script could take five or ten. If your waiter times out too often, bump up &lt;code&gt;MaxAttempts&lt;/code&gt; before adding more complex logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Filtering and Querying Instances&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you find the right instances without knowing their IDs ahead of time?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hardcoding instance IDs is a trap. Instances get replaced, IDs change, and suddenly your script is managing the wrong thing, or nothing at all. A better approach is to query by attributes you control, like tags or state.&lt;/p&gt;
&lt;h3&gt;Querying by State&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

def get_instances_by_state(state: str, region: str = &amp;#39;us-east-1&amp;#39;) -&amp;gt; list:
    &amp;quot;&amp;quot;&amp;quot;
    Return all instances in a given state.
    Valid states: &amp;#39;running&amp;#39;, &amp;#39;stopped&amp;#39;, &amp;#39;pending&amp;#39;, &amp;#39;stopping&amp;#39;, &amp;#39;terminated&amp;#39;
    &amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    response = ec2.describe_instances(
        Filters=[{&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [state]}]
    )

    # describe_instances groups results into Reservations, so we flatten them
    instances = []
    for reservation in response[&amp;#39;Reservations&amp;#39;]:
        instances.extend(reservation[&amp;#39;Instances&amp;#39;])

    return instances


# Find everything that&amp;#39;s currently running
running = get_instances_by_state(&amp;#39;running&amp;#39;)
print(f&amp;#39;{len(running)} running instance(s) found.&amp;#39;)

for inst in running:
    print(f&amp;quot;  {inst[&amp;#39;InstanceId&amp;#39;]}  {inst[&amp;#39;InstanceType&amp;#39;]}  {inst[&amp;#39;Placement&amp;#39;][&amp;#39;AvailabilityZone&amp;#39;]}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Querying by Tags&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

def get_instances_by_tag(tag_key: str, tag_value: str, region: str = &amp;#39;us-east-1&amp;#39;) -&amp;gt; list:
    &amp;quot;&amp;quot;&amp;quot;Find instances that have a specific tag key/value pair.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    # AWS filter syntax for tags is &amp;#39;tag:&amp;lt;key&amp;gt;&amp;#39;
    response = ec2.describe_instances(
        Filters=[{&amp;#39;Name&amp;#39;: f&amp;#39;tag:{tag_key}&amp;#39;, &amp;#39;Values&amp;#39;: [tag_value]}]
    )

    instances = []
    for reservation in response[&amp;#39;Reservations&amp;#39;]:
        instances.extend(reservation[&amp;#39;Instances&amp;#39;])

    return instances


# Find all production instances
prod = get_instances_by_tag(&amp;#39;Environment&amp;#39;, &amp;#39;production&amp;#39;)

for inst in prod:
    # Pull the Name tag if it exists, otherwise label it &amp;#39;unnamed&amp;#39;
    name = next(
        (t[&amp;#39;Value&amp;#39;] for t in inst.get(&amp;#39;Tags&amp;#39;, []) if t[&amp;#39;Key&amp;#39;] == &amp;#39;Name&amp;#39;),
        &amp;#39;unnamed&amp;#39;
    )
    print(f&amp;quot;  {inst[&amp;#39;InstanceId&amp;#39;]}  {name}  {inst[&amp;#39;State&amp;#39;][&amp;#39;Name&amp;#39;]}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can stack filters together, too. AWS applies them with AND logic, so you&amp;#39;ll only get instances that match all of them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Only running instances in the production environment both filters must match
response = ec2.describe_instances(
    Filters=[
        {&amp;#39;Name&amp;#39;: &amp;#39;tag:Environment&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;production&amp;#39;]},
        {&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;running&amp;#39;]}
    ]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That single call replaces what would otherwise be a manual filter in the console or a clunky post-processing loop in code.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Bulk Operations: Stopping Instances by Tag or VPC&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;How do you stop an entire group of instances at once without listing each ID manually?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is where scripting really earns its keep. Instead of hunting down individual instance IDs, you describe what you want, collect the IDs, and stop them all in one shot.&lt;/p&gt;
&lt;h3&gt;Stop All Instances in a VPC&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

def stop_instances_in_vpc(vpc_id: str, region: str = &amp;#39;us-east-1&amp;#39;) -&amp;gt; list:
    &amp;quot;&amp;quot;&amp;quot;Stop all running instances in a given VPC.&amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    # Find every running instance in this VPC
    response = ec2.describe_instances(
        Filters=[
            {&amp;#39;Name&amp;#39;: &amp;#39;vpc-id&amp;#39;, &amp;#39;Values&amp;#39;: [vpc_id]},
            {&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;running&amp;#39;]}
        ]
    )

    instance_ids = [
        inst[&amp;#39;InstanceId&amp;#39;]
        for r in response[&amp;#39;Reservations&amp;#39;]
        for inst in r[&amp;#39;Instances&amp;#39;]
    ]

    if not instance_ids:
        print(f&amp;#39;Nothing running in {vpc_id}.&amp;#39;)
        return []

    print(f&amp;#39;Stopping {len(instance_ids)} instance(s) in {vpc_id}...&amp;#39;)
    ec2.stop_instances(InstanceIds=instance_ids)

    return instance_ids


stopped = stop_instances_in_vpc(&amp;#39;vpc-0abc12345def67890&amp;#39;)
print(f&amp;#39;Stopped: {stopped}&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Stop All Instances with a Specific Tag&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

def stop_instances_by_tag(
    tag_key: str,
    tag_value: str,
    region: str = &amp;#39;us-east-1&amp;#39;,
    dry_run: bool = True
) -&amp;gt; list:
    &amp;quot;&amp;quot;&amp;quot;
    Stop all running instances that match a tag.
    Set dry_run=True to preview what would be stopped without actually doing it.
    &amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    response = ec2.describe_instances(
        Filters=[
            {&amp;#39;Name&amp;#39;: f&amp;#39;tag:{tag_key}&amp;#39;, &amp;#39;Values&amp;#39;: [tag_value]},
            {&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;running&amp;#39;]}
        ]
    )

    instance_ids = [
        inst[&amp;#39;InstanceId&amp;#39;]
        for r in response[&amp;#39;Reservations&amp;#39;]
        for inst in r[&amp;#39;Instances&amp;#39;]
    ]

    if not instance_ids:
        print(f&amp;#39;No running instances tagged {tag_key}={tag_value}.&amp;#39;)
        return []

    if dry_run:
        print(f&amp;#39;[DRY RUN] Would stop {len(instance_ids)} instance(s):&amp;#39;)
        for iid in instance_ids:
            print(f&amp;#39;  - {iid}&amp;#39;)
        return instance_ids

    ec2.stop_instances(InstanceIds=instance_ids)
    print(f&amp;#39;Stopped {len(instance_ids)} instance(s) tagged {tag_key}={tag_value}.&amp;#39;)

    return instance_ids


# Always preview before committing
stop_instances_by_tag(&amp;#39;Environment&amp;#39;, &amp;#39;dev&amp;#39;, dry_run=True)

# When you&amp;#39;re confident, flip the flag
# stop_instances_by_tag(&amp;#39;Environment&amp;#39;, &amp;#39;dev&amp;#39;, dry_run=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Always build a &lt;code&gt;dry_run&lt;/code&gt; mode into your bulk scripts.&lt;/strong&gt; It takes two minutes to add and can save you from a very bad day. Preview first, execute second.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;Error Handling: Throttling, Permissions, and Invalid States&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;What happens when your Boto3 script hits an error, and how do you handle it cleanly?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;AWS APIs fail. Not often, but enough that your production scripts need to be ready for it. The three most common issues you&amp;#39;ll run into are rate limiting, missing permissions, and trying to do something that doesn&amp;#39;t make sense for the instance&amp;#39;s current state.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s a function that handles all of them gracefully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
import time
from botocore.exceptions import ClientError

def robust_stop_instance(
    instance_id: str,
    region: str = &amp;#39;us-east-1&amp;#39;,
    max_retries: int = 3
) -&amp;gt; bool:
    &amp;quot;&amp;quot;&amp;quot;
    Stop an instance with sensible error handling and retry logic.
    Returns True on success, False if the operation can&amp;#39;t be completed.
    &amp;quot;&amp;quot;&amp;quot;
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)

    for attempt in range(1, max_retries + 1):
        try:
            response = ec2.stop_instances(InstanceIds=[instance_id])
            state = response[&amp;#39;StoppingInstances&amp;#39;][0][&amp;#39;CurrentState&amp;#39;][&amp;#39;Name&amp;#39;]
            print(f&amp;#39;Stopped {instance_id}. Current state: {state}&amp;#39;)
            return True

        except ClientError as e:
            code = e.response[&amp;#39;Error&amp;#39;][&amp;#39;Code&amp;#39;]

            if code in (&amp;#39;RequestLimitExceeded&amp;#39;, &amp;#39;Throttling&amp;#39;):
                # Back off exponentially and try again
                wait_time = 2 ** attempt
                print(f&amp;#39;Rate limited. Waiting {wait_time}s before retry {attempt}/{max_retries}...&amp;#39;)
                time.sleep(wait_time)

            elif code == &amp;#39;UnauthorizedOperation&amp;#39;:
                # No point retrying this is a permissions issue
                print(f&amp;#39;Permission denied: {e.response[&amp;quot;Error&amp;quot;][&amp;quot;Message&amp;quot;]}&amp;#39;)
                print(&amp;#39;Check the IAM policy on your role or profile.&amp;#39;)
                return False

            elif code == &amp;#39;IncorrectInstanceState&amp;#39;:
                # Instance might already be stopped or terminated
                print(f&amp;#39;{instance_id} is in an unexpected state. Skipping.&amp;#39;)
                return False

            elif code == &amp;#39;InvalidInstanceID.NotFound&amp;#39;:
                # Wrong region, or the instance no longer exists
                print(f&amp;#39;{instance_id} not found in {region}.&amp;#39;)
                return False

            else:
                # Something unexpected surface it clearly
                print(f&amp;#39;Unexpected error [{code}]: {e}&amp;#39;)
                raise

    print(f&amp;#39;Gave up after {max_retries} attempts.&amp;#39;)
    return False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key error codes to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RequestLimitExceeded&lt;/code&gt; / &lt;code&gt;Throttling&lt;/code&gt;:&lt;/strong&gt; You&amp;#39;re hitting the AWS API rate limit. Use exponential backoff and retry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;UnauthorizedOperation&lt;/code&gt;:&lt;/strong&gt; The IAM role or user doesn&amp;#39;t have the right permission. No amount of retrying will fix this you need to update the policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;IncorrectInstanceState&lt;/code&gt;:&lt;/strong&gt; You&amp;#39;re trying to do something the instance&amp;#39;s current state doesn&amp;#39;t allow, like stopping an instance that&amp;#39;s already stopped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;InvalidInstanceID.NotFound&lt;/code&gt;:&lt;/strong&gt; The instance ID doesn&amp;#39;t exist in that region. Double-check both the ID and the region.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Can Boto3 manage EC2 instances across multiple regions in one script?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, and it&amp;#39;s pretty easy to do. Just create a separate client for each region you need. You can loop over a list of region names and run the same operations in each one. If you need it to go fast, &lt;code&gt;concurrent.futures&lt;/code&gt; lets you run those calls in parallel.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
from concurrent.futures import ThreadPoolExecutor

regions = [&amp;#39;us-east-1&amp;#39;, &amp;#39;eu-west-1&amp;#39;, &amp;#39;ap-southeast-1&amp;#39;]

def count_running(region):
    ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=region)
    resp = ec2.describe_instances(
        Filters=[{&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;running&amp;#39;]}]
    )
    count = sum(len(r[&amp;#39;Instances&amp;#39;]) for r in resp[&amp;#39;Reservations&amp;#39;])
    print(f&amp;#39;{region}: {count} running instance(s)&amp;#39;)

# Query all regions at the same time instead of one by one
with ThreadPoolExecutor() as pool:
    pool.map(count_running, regions)
&lt;/code&gt;&lt;/pre&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What IAM permissions does my role need to start and stop instances?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;At a minimum you&amp;#39;ll need &lt;code&gt;ec2:StartInstances&lt;/code&gt;, &lt;code&gt;ec2:StopInstances&lt;/code&gt;, &lt;code&gt;ec2:DescribeInstances&lt;/code&gt;, and &lt;code&gt;ec2:DescribeInstanceStatus&lt;/code&gt;. If you&amp;#39;re filtering by tags, add &lt;code&gt;ec2:DescribeTags&lt;/code&gt; too. Scope permissions as tightly as you can using IAM condition keys, especially for production environments.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do Boto3 waiters differ from just sleeping for a fixed amount of time?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;A fixed sleep is a guess. A waiter actually checks. Waiters poll the AWS API and return the moment the state changes, which means they&amp;#39;re faster when things go smoothly and more informative when they don&amp;#39;t. If the timeout is exceeded, you get a &lt;code&gt;WaiterError&lt;/code&gt; you can handle, rather than a script that silently moves on assuming everything is fine.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Is there a difference between using a Boto3 client and a resource for EC2?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes. &lt;code&gt;boto3.client(&amp;#39;ec2&amp;#39;)&lt;/code&gt; is the low-level interface that maps directly to API calls and returns raw dictionaries. &lt;code&gt;boto3.resource(&amp;#39;ec2&amp;#39;)&lt;/code&gt; gives you a higher-level, object-oriented interface with things like &lt;code&gt;ec2.Instance(&amp;#39;i-0abc123&amp;#39;)&lt;/code&gt;. Both work, but the client is more flexible for filtering and bulk operations, which is why most automation scripts use it.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Can I use Boto3 to manage spot instances the same way as on-demand instances?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mostly yes. &lt;code&gt;start_instances&lt;/code&gt;, &lt;code&gt;stop_instances&lt;/code&gt;, and &lt;code&gt;describe_instances&lt;/code&gt; all work with spot instances. The main difference is that spot instances can be interrupted by AWS when capacity is reclaimed, so your automation should be ready for unexpected state changes. Use &lt;code&gt;describe_spot_instance_requests&lt;/code&gt; to check spot-specific status and watch for interruption notices.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I handle pagination when there are many instances to query?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;describe_instances&lt;/code&gt; only returns up to 1,000 results per call. For bigger environments, use a paginator instead of calling directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3

ec2 = boto3.client(&amp;#39;ec2&amp;#39;, region_name=&amp;#39;us-east-1&amp;#39;)
paginator = ec2.get_paginator(&amp;#39;describe_instances&amp;#39;)

instances = []
# paginate() handles NextToken automatically no manual looping needed
for page in paginator.paginate(Filters=[{&amp;#39;Name&amp;#39;: &amp;#39;instance-state-name&amp;#39;, &amp;#39;Values&amp;#39;: [&amp;#39;running&amp;#39;]}]):
    for reservation in page[&amp;#39;Reservations&amp;#39;]:
        instances.extend(reservation[&amp;#39;Instances&amp;#39;])

print(f&amp;#39;Total running instances: {len(instances)}&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The paginator handles &lt;code&gt;NextToken&lt;/code&gt; automatically, so you&amp;#39;ll always get everything without extra logic.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What&amp;#39;s the safest way to test bulk stop scripts before running them in production?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;dry_run&lt;/code&gt; pattern shown in the bulk operations section. Run with &lt;code&gt;dry_run=True&lt;/code&gt; first to see exactly which instances would be affected before you commit. Beyond that, test in a staging account or a non-production VPC. Some Boto3 calls also support a &lt;code&gt;DryRun=True&lt;/code&gt; parameter at the API level, which validates your permissions without making any changes.&lt;/p&gt;
&lt;/Accordion&gt;&lt;hr&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html&quot;&gt;Boto3 Documentation EC2 Client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html#waiters&quot;&gt;Boto3 Documentation Waiters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html&quot;&gt;Boto3 Documentation Paginators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html&quot;&gt;Boto3 Documentation Session and Credential Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html&quot;&gt;AWS EC2 API Reference DescribeInstances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_StartInstances.html&quot;&gt;AWS EC2 API Reference StartInstances &amp;amp; StopInstances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html&quot;&gt;AWS IAM Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html&quot;&gt;AWS IAM Roles for EC2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html&quot;&gt;AWS EC2 Instance Lifecycle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html&quot;&gt;AWS EC2 Tagging Your Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://botocore.amazonaws.com/v1/documentation/api/latest/reference/exceptions.html&quot;&gt;Botocore Exceptions Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html&quot;&gt;AWS EC2 Spot Instance Interruptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor&quot;&gt;Python &lt;code&gt;concurrent.futures&lt;/code&gt; ThreadPoolExecutor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html&quot;&gt;AWS CLI Configuration and Credential Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/wellarchitected/latest/operational-excellence-pillar/welcome.html&quot;&gt;AWS Well-Architected Framework Operational Excellence Pillar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>cloud</category><category>aws</category><category>devops</category><category>automation</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0013-aws-ec2-instance-management-boto3-python/hero.png" length="0" type="image/jpeg"/></item><item><title>Redis Caching Patterns: Cache-Aside, Write-Through &amp; Cache Invalidation</title><link>https://mkabumattar.com/codesnippets/post/redis-caching-patterns-architecture/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/redis-caching-patterns-architecture/</guid><description>Master production-ready Redis caching patterns with practical examples. Learn cache-aside (lazy loading), write-through, consistency patterns, TTL strategies, and cache invalidation techniques to reduce database load and improve application performance.</description><pubDate>Tue, 07 Apr 2026 20:02:59 GMT</pubDate><content:encoded>&lt;h3&gt;Need to scale your backend without throwing money at servers? Redis caching patterns are your answer.&lt;/h3&gt;
&lt;p&gt;Most databases can handle hundreds of queries per second, but thousands? Your app slows to a crawl. Adding another database server just moves the problem. The real solution: serve data from memory 99% of the time using Redis.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Database Load Under Scale&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;100 requests/sec for &amp;quot;get product details&amp;quot;:
• Database: 200ms per query
• 100 × 200ms = serious bottleneck
• Add 1000 concurrent users = complete collapse
• Throwing more servers doesn&amp;#39;t help (they all queue at the database)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Redis Caching Patterns&lt;/h3&gt;
&lt;p&gt;Redis stores data in memory, delivering results in microseconds instead of milliseconds. But &lt;strong&gt;which caching pattern&lt;/strong&gt; you choose determines whether your cache helps or hurts.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt;: Check cache first, miss triggers database load + populate cache (most flexible)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Through&lt;/strong&gt;: Update cache and database together (most consistent)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL Strategy&lt;/strong&gt;: Keys expire automatically, preventing stale data without manual invalidation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache Invalidation&lt;/strong&gt;: Delete cache when data updates, triggering refresh on next request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stampede Prevention&lt;/strong&gt;: Prevent thundering herd when cache expires or misses expire&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Pattern 1: Cache-Aside (Lazy Loading)&lt;/h2&gt;
&lt;h3&gt;How It Works&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. Request comes in
2. Check Redis cache
3. Cache HIT → return immediately (microseconds)
4. Cache MISS → query database (milliseconds)
5. Populate cache with TTL
6. Next request hits cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
import json
from functools import wraps
import time

redis_client = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, decode_responses=True)

def cache_aside(ttl=3600):
    &amp;quot;&amp;quot;&amp;quot;Decorator implementing cache-aside pattern&amp;quot;&amp;quot;&amp;quot;
    def decorator(func):
        @wraps(func)
        def wrapper(key, *args, **kwargs):
            # Check cache first
            cached = redis_client.get(key)
            if cached:
                # Cache hit
                return json.loads(cached)

            # Cache miss: call function (hits database)
            result = func(*args, **kwargs)

            # Populate cache with TTL
            redis_client.setex(key, ttl, json.dumps(result))

            return result
        return wrapper
    return decorator

# Database query (expensive)
def get_user_from_db(user_id):
    # Simulated database query
    time.sleep(0.2)  # 200ms
    return {&amp;quot;id&amp;quot;: user_id, &amp;quot;name&amp;quot;: f&amp;quot;User {user_id}&amp;quot;, &amp;quot;email&amp;quot;: f&amp;quot;user{user_id}@example.com&amp;quot;}

# Decorated function
@cache_aside(ttl=3600)
def get_user(user_id):
    return get_user_from_db(user_id)

# Usage
print(get_user(f&amp;quot;user:1&amp;quot;))  # First call: 200ms (cache miss)
print(get_user(f&amp;quot;user:1&amp;quot;))  # Second call: &amp;lt;1ms (cache hit)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import {createClient} from &amp;#39;redis&amp;#39;;
import {promisify} from &amp;#39;util&amp;#39;;

const redis = createClient();
redis.connect();

const getAsync = promisify(redis.get).bind(redis);
const setexAsync = promisify(redis.setex).bind(redis);

// Database query (expensive)
async function getUserFromDB(userId: string) {
  // Simulated database query
  await new Promise((resolve) =&amp;gt; setTimeout(resolve, 200));
  return {
    id: userId,
    name: `User ${userId}`,
    email: `user${userId}@example.com`,
  };
}

// Cache-aside implementation
async function getUser(userId: string, ttl = 3600) {
  const cacheKey = `user:${userId}`;

  // Check cache
  const cached = await getAsync(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Cache miss: hit database
  const user = await getUserFromDB(userId);

  // Populate cache
  await setexAsync(cacheKey, ttl, JSON.stringify(user));

  return user;
}

// Usage
(async () =&amp;gt; {
  console.time(&amp;#39;first&amp;#39;);
  await getUser(&amp;#39;user:1&amp;#39;); // 200ms (miss)
  console.timeEnd(&amp;#39;first&amp;#39;);

  console.time(&amp;#39;second&amp;#39;);
  await getUser(&amp;#39;user:1&amp;#39;); // &amp;lt;1ms (hit)
  console.timeEnd(&amp;#39;second&amp;#39;);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&amp;quot;context&amp;quot;
	&amp;quot;encoding/json&amp;quot;
	&amp;quot;fmt&amp;quot;
	&amp;quot;github.com/redis/go-redis/v9&amp;quot;
	&amp;quot;time&amp;quot;
)

type User struct {
	ID    int    `json:&amp;quot;id&amp;quot;`
	Name  string `json:&amp;quot;name&amp;quot;`
	Email string `json:&amp;quot;email&amp;quot;`
}

var rdb = redis.NewClient(&amp;amp;redis.Options{
	Addr: &amp;quot;localhost:6379&amp;quot;,
})

func getUserFromDB(ctx context.Context, userID int) (User, error) {
	// Simulated database query
	time.Sleep(200 * time.Millisecond)
	return User{
		ID:    userID,
		Name:  fmt.Sprintf(&amp;quot;User %d&amp;quot;, userID),
		Email: fmt.Sprintf(&amp;quot;user%d@example.com&amp;quot;, userID),
	}, nil
}

func getUser(ctx context.Context, userID int) (User, error) {
	cacheKey := fmt.Sprintf(&amp;quot;user:%d&amp;quot;, userID)

	// Check cache
	val, err := rdb.Get(ctx, cacheKey).Result()
	if err == nil {
		var user User
		json.Unmarshal([]byte(val), &amp;amp;user)
		return user, nil
	}

	// Cache miss: hit database
	user, err := getUserFromDB(ctx, userID)
	if err != nil {
		return user, err
	}

	// Populate cache (1 hour TTL)
	data, _ := json.Marshal(user)
	rdb.SetEx(ctx, cacheKey, string(data), time.Hour)

	return user, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pattern 2: Write-Through&lt;/h2&gt;
&lt;h3&gt;How It Works&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. Data update arrives
2. Update cache AND database together
3. Both succeed or both fail (atomic)
4. Next read hits cache (always consistent)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;When to Use&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Critical data (payments, user accounts)&lt;/li&gt;
&lt;li&gt;Data that changes frequently&lt;/li&gt;
&lt;li&gt;When consistency is more important than speed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Python Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
import json

redis_client = redis.Redis(host=&amp;#39;localhost&amp;#39;, port=6379, decode_responses=True)

def update_user_write_through(user_id: int, user_data: dict):
    &amp;quot;&amp;quot;&amp;quot;Update cache AND database together&amp;quot;&amp;quot;&amp;quot;
    cache_key = f&amp;quot;user:{user_id}&amp;quot;

    try:
        # Update cache first (faster)
        redis_client.setex(cache_key, 3600, json.dumps(user_data))

        # Then update database
        # Assuming `db.update_user(user_id, user_data)` exists
        db.update_user(user_id, user_data)

        return True
    except Exception as e:
        # If database fails, invalidate cache
        redis_client.delete(cache_key)
        raise e

# Usage
update_user_write_through(42, {
    &amp;quot;name&amp;quot;: &amp;quot;Alice&amp;quot;,
    &amp;quot;email&amp;quot;: &amp;quot;alice@example.com&amp;quot;
})

# Next read hits cache
user = redis_client.get(&amp;quot;user:42&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function updateUserWriteThrough(userId: string, userData: any) {
  const cacheKey = `user:${userId}`;

  try {
    // Update cache first
    await redis.setex(cacheKey, 3600, JSON.stringify(userData));

    // Then update database
    await db.updateUser(userId, userData);

    return true;
  } catch (error) {
    // Rollback: invalidate cache on database failure
    await redis.del(cacheKey);
    throw error;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pattern 3: TTL Strategy&lt;/h2&gt;
&lt;h3&gt;Automatic Expiration&lt;/h3&gt;
&lt;p&gt;Redis automatically deletes keys after TTL expires. No manual invalidation needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
from datetime import datetime, timedelta

redis_client = redis.Redis()

# Different TTL for different data types
def cache_with_ttl(key: str, value: str, data_type: str):
    ttl_map = {
        &amp;quot;user_profile&amp;quot;: 3600,           # 1 hour (changes infrequently)
        &amp;quot;product&amp;quot;: 86400,                # 24 hours (stable data)
        &amp;quot;product_price&amp;quot;: 300,            # 5 minutes (changes frequently)
        &amp;quot;session&amp;quot;: 1800,                 # 30 minutes (security)
        &amp;quot;statistics&amp;quot;: 60,                # 1 minute (real-time)
    }

    ttl = ttl_map.get(data_type, 3600)
    redis_client.setex(key, ttl, value)

# Usage
cache_with_ttl(&amp;quot;product:123&amp;quot;, &amp;quot;iPhone 15&amp;quot;, &amp;quot;product&amp;quot;)        # 24 hours
cache_with_ttl(&amp;quot;price:apple&amp;quot;, &amp;quot;$999&amp;quot;, &amp;quot;product_price&amp;quot;)       # 5 minutes
cache_with_ttl(&amp;quot;stats:daily&amp;quot;, &amp;quot;1000 users&amp;quot;, &amp;quot;statistics&amp;quot;)    # 1 minute
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sliding Window TTL&lt;/h3&gt;
&lt;p&gt;Reset TTL on each access (useful for sessions):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_with_sliding_ttl(key: str, ttl: int):
    &amp;quot;&amp;quot;&amp;quot;Get value and reset TTL&amp;quot;&amp;quot;&amp;quot;
    value = redis_client.get(key)
    if value:
        # Reset TTL on access
        redis_client.expire(key, ttl)
    return value

# Usage: Session stays alive as long as user is active
session = get_with_sliding_ttl(&amp;quot;session:abc123&amp;quot;, 1800)  # 30 min
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pattern 4: Cache Invalidation&lt;/h2&gt;
&lt;h3&gt;Event-Driven Invalidation&lt;/h3&gt;
&lt;p&gt;Delete cache when data changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def update_product(product_id: int, new_data: dict):
    # Update database
    db.update_product(product_id, new_data)

    # Invalidate cache immediately
    redis_client.delete(f&amp;quot;product:{product_id}&amp;quot;)

    # Also invalidate related caches (cascade invalidation)
    redis_client.delete(f&amp;quot;category:{new_data[&amp;#39;category&amp;#39;]}&amp;quot;)
    redis_client.delete(&amp;quot;products:all&amp;quot;)

# When product price changes
update_product(123, {&amp;quot;price&amp;quot;: &amp;quot;$899&amp;quot;, &amp;quot;category&amp;quot;: &amp;quot;electronics&amp;quot;})
# → Deletes &amp;quot;product:123&amp;quot;, &amp;quot;category:electronics&amp;quot;, &amp;quot;products:all&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tag-Based Invalidation&lt;/h3&gt;
&lt;p&gt;Invalidate multiple keys with one operation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json

def cache_with_tags(key: str, value: str, tags: list):
    &amp;quot;&amp;quot;&amp;quot;Cache value and tag it for bulk invalidation&amp;quot;&amp;quot;&amp;quot;
    # Store value
    redis_client.setex(key, 3600, value)

    # Store tag reference (set of keys with this tag)
    for tag in tags:
        redis_client.sadd(f&amp;quot;tag:{tag}&amp;quot;, key)

def invalidate_by_tag(tag: str):
    &amp;quot;&amp;quot;&amp;quot;Invalidate all keys with a tag&amp;quot;&amp;quot;&amp;quot;
    # Get all keys with this tag
    keys = redis_client.smembers(f&amp;quot;tag:{tag}&amp;quot;)

    # Delete all tagged keys
    if keys:
        redis_client.delete(*keys)

    # Clean up tag
    redis_client.delete(f&amp;quot;tag:{tag}&amp;quot;)

# Usage
cache_with_tags(&amp;quot;product:1&amp;quot;, &amp;quot;iPhone&amp;quot;, [&amp;quot;electronics&amp;quot;, &amp;quot;apple&amp;quot;])
cache_with_tags(&amp;quot;product:2&amp;quot;, &amp;quot;MacBook&amp;quot;, [&amp;quot;electronics&amp;quot;, &amp;quot;apple&amp;quot;])
cache_with_tags(&amp;quot;product:3&amp;quot;, &amp;quot;Apple Watch&amp;quot;, [&amp;quot;wearables&amp;quot;, &amp;quot;apple&amp;quot;])

# Invalidate all Apple products at once
invalidate_by_tag(&amp;quot;apple&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Preventing Cache Stampedes&lt;/h2&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Thousands of requests for a popular key
↓
Key expires
↓
ALL requests hit database simultaneously (thundering herd)
↓
Database overload, service degradation
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Solution: Probabilistic Regeneration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
import random
import time

redis_client = redis.Redis()

def get_with_low_ttl_regen(key: str, expensive_calculation, ttl=3600):
    &amp;quot;&amp;quot;&amp;quot;
    Regenerate cache probabilistically near expiration
    Avoids thundering herd when TTL reaches 0
    &amp;quot;&amp;quot;&amp;quot;
    value = redis_client.get(key)

    if value:
        # Check TTL
        remaining_ttl = redis_client.ttl(key)

        # Near end of life? Regenerate probabilistically
        if remaining_ttl &amp;lt; 0:
            # Key expired
            pass  # Recalculate below
        elif remaining_ttl &amp;lt; ttl * 0.2:  # Last 20% of life
            # Probability of regeneration increases as TTL decreases
            prob = 1 - (remaining_ttl / (ttl * 0.2))
            if random.random() &amp;lt; prob:
                # First request regenerates, others get stale data temporarily
                new_value = expensive_calculation()
                redis_client.setex(key, ttl, new_value)
                return new_value

        return value

    # Cache miss: calculate fresh data
    result = expensive_calculation()
    redis_client.setex(key, ttl, result)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Distributed Lock Pattern&lt;/h3&gt;
&lt;p&gt;Prevent parallel cache recalculations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import uuid

def get_with_lock(key: str, expensive_calculation, lock_timeout=10):
    &amp;quot;&amp;quot;&amp;quot;Use lock to prevent multiple calculations&amp;quot;&amp;quot;&amp;quot;
    lock_key = f&amp;quot;lock:{key}&amp;quot;
    cache_key = key

    # Check cache
    cached = redis_client.get(cache_key)
    if cached:
        return cached

    # Try to acquire lock
    lock_id = str(uuid.uuid4())
    acquired = redis_client.set(lock_key, lock_id, nx=True, ex=lock_timeout)

    if acquired:
        # We got the lock, calculate
        try:
            result = expensive_calculation()
            redis_client.setex(cache_key, 3600, result)
            return result
        finally:
            # Release lock
            if redis_client.get(lock_key) == lock_id:
                redis_client.delete(lock_key)
    else:
        # Someone else got the lock, wait for result
        for _ in range(10):
            time.sleep(0.1)
            cached = redis_client.get(cache_key)
            if cached:
                return cached

        # Timeout: calculate ourselves
        return expensive_calculation()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Serialization Strategies&lt;/h2&gt;
&lt;h3&gt;JSON vs MessagePack vs Protocol Buffers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
import msgpack

def store_json(key, obj):
    &amp;quot;&amp;quot;&amp;quot;Simple but larger&amp;quot;&amp;quot;&amp;quot;
    redis_client.set(key, json.dumps(obj))

def store_msgpack(key, obj):
    &amp;quot;&amp;quot;&amp;quot;Smaller, faster&amp;quot;&amp;quot;&amp;quot;
    redis_client.set(key, msgpack.packb(obj))

# Benchmark: 1000 products
products = [{&amp;quot;id&amp;quot;: i, &amp;quot;name&amp;quot;: f&amp;quot;Product {i}&amp;quot;, &amp;quot;price&amp;quot;: 99.99} for i in range(1000)]

json_size = len(json.dumps(products))          # ~50KB
msgpack_size = len(msgpack.packb(products))    # ~30KB (40% smaller)

# Choice depends on:
# - JSON: Human readable, broad ecosystem
# - MsgPack: Smaller, faster
# - Protobuf: Strongly typed, enterprise
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monitoring Cache Performance&lt;/h2&gt;
&lt;h3&gt;Track Hit Rate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis

redis_client = redis.Redis()

def get_cache_stats():
    &amp;quot;&amp;quot;&amp;quot;Get cache performance metrics&amp;quot;&amp;quot;&amp;quot;
    info = redis_client.info(&amp;#39;stats&amp;#39;)

    total_commands = info.get(&amp;#39;total_commands_processed&amp;#39;, 0)
    hits = info.get(&amp;#39;keyspace_hits&amp;#39;, 0)
    misses = info.get(&amp;#39;keyspace_misses&amp;#39;, 0)

    hit_rate = (hits / (hits + misses)) * 100 if (hits + misses) &amp;gt; 0 else 0

    return {
        &amp;quot;hit_rate&amp;quot;: hit_rate,
        &amp;quot;total_hits&amp;quot;: hits,
        &amp;quot;total_misses&amp;quot;: misses,
        &amp;quot;evictions&amp;quot;: info.get(&amp;#39;evicted_keys&amp;#39;, 0),
        &amp;quot;used_memory&amp;quot;: info.get(&amp;#39;used_memory_human&amp;#39;, &amp;#39;N/A&amp;#39;),
    }

# Target: &amp;gt;80% hit rate
# &amp;lt;50%: Cache is ineffective, reconsider keys/TTLs
# &amp;gt;95%: Good fit for caching strategy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Connection Pooling&lt;/h2&gt;
&lt;p&gt;Reuse connections instead of creating new ones:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis
from redis.connection import ConnectionPool

# Without pooling (inefficient)
conn = redis.Redis()

# With pooling (efficient)
pool = ConnectionPool.from_url(
    &amp;#39;redis://localhost:6379&amp;#39;,
    max_connections=50,
    decode_responses=True
)
redis_client = redis.Redis(connection_pool=pool)

# Reuses connections automatically
for i in range(1000):
    redis_client.get(f&amp;quot;key:{i}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;1. Consistent Key Naming&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Good: Hierarchical, searchable
&amp;quot;user:123&amp;quot;
&amp;quot;user:123:settings&amp;quot;
&amp;quot;product:456&amp;quot;
&amp;quot;product:456:reviews&amp;quot;
&amp;quot;order:789:items&amp;quot;

# Bad: Ambiguous
&amp;quot;u123&amp;quot;
&amp;quot;prod_456&amp;quot;
&amp;quot;item-789&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Set Reasonable TTLs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;TTL_STRATEGY = {
    &amp;quot;user_profile&amp;quot;: 3600,           # 1 hour
    &amp;quot;product_catalog&amp;quot;: 86400,       # 24 hours
    &amp;quot;session&amp;quot;: 1800,                # 30 minutes
    &amp;quot;api_response&amp;quot;: 300,            # 5 minutes
    &amp;quot;real_time_data&amp;quot;: 60,           # 1 minute
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Plan for Failures&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def safe_cache_get(key: str, fallback_fn):
    &amp;quot;&amp;quot;&amp;quot;Gracefully handle Redis failures&amp;quot;&amp;quot;&amp;quot;
    try:
        value = redis_client.get(key)
        if value:
            return value
    except redis.ConnectionError:
        # Redis down? Use fallback
        pass

    # No cache: call function
    return fallback_fn()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Redis caching is the difference between a responsive application and a slow one.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Choose the right pattern (cache-aside for flexibility, write-through for consistency), set appropriate TTLs, prevent stampedes, and monitor hit rates. Combined with database optimization, caching forms the foundation of high-performance systems.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/documentation&quot;&gt;Redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/manual/client-side-caching/&quot;&gt;REDIS Caching Strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis-py.readthedocs.io/&quot;&gt;redis-py Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://node-redis.io/&quot;&gt;node-redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redis/go-redis&quot;&gt;go-redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>backend</category><category>performance</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0012-redis-caching-patterns-architecture/hero.png" length="0" type="image/jpeg"/></item><item><title>PostgreSQL Query Optimization: Indexes, EXPLAIN ANALYZE &amp; Execution Plans</title><link>https://mkabumattar.com/codesnippets/post/postgresql-query-optimization-indexes-explain/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/postgresql-query-optimization-indexes-explain/</guid><description>Master PostgreSQL query optimization with practical examples. Learn EXPLAIN ANALYZE interpretation, effective index strategies, query rewriting, and connection pooling to identify and fix slow queries in production microservices.</description><pubDate>Sun, 29 Mar 2026 20:02:59 GMT</pubDate><content:encoded>&lt;h3&gt;Need to optimize slow PostgreSQL queries? Here&amp;#39;s how with EXPLAIN ANALYZE and strategic indexing.&lt;/h3&gt;
&lt;p&gt;Slow database queries kill application performance. But most developers don&amp;#39;t know where the actual bottleneck is, so they guess at fixes. EXPLAIN ANALYZE reveals exactly what&amp;#39;s expensive and where to optimize.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Why Queries Get Slow&lt;/h3&gt;
&lt;p&gt;In development with small datasets, missing indexes don&amp;#39;t matter. Switch to production with millions of rows, and suddenly sequential table scans become devastating. Queries that ran in 50ms take 5+ seconds. You need to know why.&lt;/p&gt;
&lt;h3&gt;Common Performance Issues&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Missing indexes&lt;/strong&gt;: Forcing full table scans on every query&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;N+1 queries&lt;/strong&gt;: Fetching data in loops instead of bulk operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bad query plans&lt;/strong&gt;: Using inefficient joins or sorts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undersized indices&lt;/strong&gt;: Index on wrong columns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection pool exhaustion&lt;/strong&gt;: Running out of available connections&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;EXPLAIN ANALYZE: Your Debugging Superpower&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; shows exactly how PostgreSQL executes your query, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which operations are most expensive (by cost)&lt;/li&gt;
&lt;li&gt;Actual vs. estimated row counts&lt;/li&gt;
&lt;li&gt;Time spent in each step&lt;/li&gt;
&lt;li&gt;Seq Scans vs. Index Scans&lt;/li&gt;
&lt;li&gt;Sort and Hash operations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Run EXPLAIN ANALYZE to identify expensive operations&lt;/li&gt;
&lt;li&gt;Add indexes where sequential scans happen on large tables&lt;/li&gt;
&lt;li&gt;Use pg_stat_statements to automatically find slow queries&lt;/li&gt;
&lt;li&gt;Fix N+1 query patterns before rewriting complex queries&lt;/li&gt;
&lt;li&gt;Setup connection pooling for better resource utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Understanding EXPLAIN ANALYZE&lt;/h2&gt;
&lt;h3&gt;Basic Query Analysis&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Compare these two queries
EXPLAIN ANALYZE
SELECT * FROM users WHERE created_at &amp;gt; &amp;#39;2025-01-01&amp;#39;;

-- Output example:
-- Seq Scan on users  (cost=0.00..35.50 rows=1000 width=100)
--   Filter: (created_at &amp;gt; &amp;#39;2025-01-01&amp;#39;)
--   Planning Time: 0.123 ms
--   Execution Time: 45.234 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What this tells you:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Seq Scan&lt;/code&gt;: Reading entire table (slow for large tables)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cost=0.00..35.50&lt;/code&gt;: PostgreSQL&amp;#39;s estimated cost&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rows=1000&lt;/code&gt;: Expected to return 1000 rows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Execution Time: 45.234 ms&lt;/code&gt;: Actual time taken&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Reading the Cost&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE
SELECT u.id, u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = &amp;#39;active&amp;#39;;

-- Output:
-- Hash Left Join  (cost=2534.50..5892.33 rows=5000 width=50)
--   Hash Cond: (o.user_id = u.id)
--   -&amp;gt;  Seq Scan on orders o  (cost=0.00..1234.50 rows=50000 width=8)
--   -&amp;gt;  Hash  (cost=2500.00..2500.00 rows=5000 width=42)
--         -&amp;gt;  Index Scan using idx_users_status on users u  (cost=10.00..2500.00 rows=5000 width=42)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cost interpretation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lower cost = faster execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cost=A..B&lt;/code&gt;: A = startup cost, B = total cost&lt;/li&gt;
&lt;li&gt;Focus on the highest-cost operations first&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating Effective Indexes&lt;/h2&gt;
&lt;h3&gt;Basic Index Creation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Simple B-tree index (most common)
CREATE INDEX idx_users_email ON users(email);

-- Multi-column index
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

-- Partial index (only index active users)
CREATE INDEX idx_users_active ON users(id) WHERE status = &amp;#39;active&amp;#39;;

-- Unique index (constraint + performance)
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Index Types: When to Use Each&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- B-tree (default, best for most queries)
CREATE INDEX idx_standard ON table_name(column);

-- Hash (equality only, rarely faster than B-tree)
CREATE INDEX idx_hash ON table_name USING hash(column);

-- GiST (geometric, full-text search)
CREATE INDEX idx_fulltext ON articles USING gist(search_vector);

-- GIN (array, JSON, full-text - faster for large result sets)
CREATE INDEX idx_json ON logs USING gin(metadata);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Partial Indexes for Common Cases&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Don&amp;#39;t index inactive records
CREATE INDEX idx_active_orders ON orders(id) WHERE status != &amp;#39;cancelled&amp;#39;;

-- Only index recent data
CREATE INDEX idx_recent_events ON events(id) WHERE created_at &amp;gt; NOW() - INTERVAL &amp;#39;90 days&amp;#39;;

-- Saves space and speeds up queries on active data
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Multi-Column Index Strategy&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- For WHERE + ORDER BY combinations
-- Query: WHERE user_id = ? ORDER BY created_at DESC
CREATE INDEX idx_user_date ON orders(user_id, created_at DESC);

-- Column order matters: Put filtered columns first
-- Good:   WHERE status = ? AND type = ?
CREATE INDEX idx_good ON events(status, type);

-- Bad:    WHERE status = ? AND type = ?
CREATE INDEX idx_bad ON events(type, status);  -- Wrong order
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detecting and Fixing N+1 Queries&lt;/h2&gt;
&lt;h3&gt;The N+1 Problem&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# BAD: N+1 queries (1 + N queries)
users = db.query(User).all()  # Query 1
for user in users:
    orders = db.query(Order).filter(Order.user_id == user.id).all()  # Queries 2 to N+1
    print(f&amp;quot;{user.name}: {len(orders)} orders&amp;quot;)

# Result with 1000 users = 1001 queries
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Fix: Eager Loading&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# GOOD: Eager load with join
from sqlalchemy import joinedload

users = db.query(User).options(joinedload(User.orders)).all()  # Single query with join

for user in users:
    print(f&amp;quot;{user.name}: {len(user.orders)} orders&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SQL Equivalent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- BAD (N+1):
SELECT * FROM users;  -- 1000 queries...
SELECT * FROM orders WHERE user_id = ?;

-- GOOD (Single query):
SELECT u.id, u.name, o.id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Finding Slow Queries Automatically&lt;/h2&gt;
&lt;h3&gt;Enable Query Logging&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Enable logging of slow queries (500ms+)
ALTER SYSTEM SET log_min_duration_statement = 500;
SELECT pg_reload_conf();

-- View current setting
SHOW log_min_duration_statement;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Use pg_stat_statements&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Install extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Find slowest queries
SELECT query, calls, mean_time, max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- Find queries that were called most
SELECT query, calls, mean_time
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;

-- Clear stats to get fresh baseline
SELECT pg_stat_statements_reset();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Query Optimization Patterns&lt;/h2&gt;
&lt;h3&gt;Pattern 1: Missing Index on WHERE Clause&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Before: Seq Scan (slow)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;

-- Fix: Add index
CREATE INDEX idx_orders_customer ON orders(customer_id);

-- After: Index Scan (fast)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pattern 2: Inefficient Subqueries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- SLOW: Subquery evaluated for each row
SELECT * FROM orders
WHERE customer_id IN (
  SELECT id FROM customers WHERE status = &amp;#39;premium&amp;#39;
);

-- FAST: Use JOIN instead
SELECT DISTINCT o.*
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.status = &amp;#39;premium&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pattern 3: Using Functions in WHERE Clauses&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- SLOW: Function applied to indexed column
SELECT * FROM users WHERE LOWER(email) = &amp;#39;test@example.com&amp;#39;;

-- FAST: Use expression index
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = &amp;#39;test@example.com&amp;#39;;

-- OR: Normalize incoming data instead
SELECT * FROM users WHERE email = &amp;#39;test@example.com&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pattern 4: Missing ORDER BY Index&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- SLOW: Sort step required
EXPLAIN ANALYZE
SELECT * FROM events ORDER BY created_at DESC LIMIT 10;

-- Fix: Add index for sort
CREATE INDEX idx_events_date_desc ON events(created_at DESC);

-- Now uses Index Scan + Limit, no Sort step
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Connection Pooling&lt;/h2&gt;
&lt;h3&gt;Why Connection Pooling Matters&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Without pooling: Each request opens/closes connection (expensive)
-- With pooling: Reuse existing connections (cheap)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PgBouncer Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[databases]
myapp = host=localhost port=5432 dbname=myapp_prod

[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# Connection pooling settings
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application Connection&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Before pooling: Direct to PostgreSQL
import psycopg2
conn = psycopg2.connect(&amp;quot;dbname=myapp user=postgres host=localhost&amp;quot;)

# After pooling: Connect to PgBouncer on port 6432
import psycopg2
conn = psycopg2.connect(&amp;quot;dbname=myapp user=postgres host=localhost port=6432&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Optimization Workflow&lt;/h2&gt;
&lt;h3&gt;Step 1: Identify the Slow Query&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# From application logs or pg_stat_statements
# Example: SELECT query is taking 8000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Run EXPLAIN ANALYZE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN ANALYZE
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at &amp;gt; &amp;#39;2025-01-01&amp;#39;
ORDER BY o.total DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Look for Expensive Operations&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Sort (cost=9234.50..9244.50)  ← Look here
  -&amp;gt;  Hash Join (cost=2500.00..9234.00)  ← And here
        -&amp;gt;  Seq Scan on orders o (cost=0.00..5000.00)  ← Sequential scan on large table
        -&amp;gt;  Hash (cost=100.00..100.00 rows=1000)
              -&amp;gt;  Seq Scan on customers c (cost=0.00..100.00)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Add Indexes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Index for WHERE clause
CREATE INDEX idx_orders_date ON orders(created_at);

-- Index for JOIN condition
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- Index for ORDER BY
CREATE INDEX idx_orders_total ON orders(total DESC);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Rerun EXPLAIN ANALYZE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Should now use Index Scans instead of Seq Scans
-- No Sort step if you have the right index
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;1. Index Naming Convention&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Consistent naming helps find related indexes
CREATE INDEX idx_table_column_type ON table_name(column);

-- Examples:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_customer_date ON orders(customer_id, created_at);
CREATE INDEX idx_products_active ON products(id) WHERE active = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Monitor Index Usage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Find unused indexes (bloat)
SELECT schemaname, tablename, indexname
FROM pg_indexes
WHERE schemaname NOT IN (&amp;#39;pg_catalog&amp;#39;, &amp;#39;information_schema&amp;#39;);

-- Check if index is being used
SELECT
  indexrelname,
  idx_scan,
  idx_tup_read,
  idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Regular Maintenance&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Analyze table stats (for query planner)
ANALYZE users;

-- Vacuum to clean up dead rows
VACUUM FULL users;

-- Reindex if fragmented (heavy write workloads)
REINDEX INDEX idx_users_email;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/sql-explain.html&quot;&gt;PostgreSQL EXPLAIN Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes-types.html&quot;&gt;Index Types &amp;amp; When to Use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/planner.html&quot;&gt;Query Planning &amp;amp; Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pgbouncer.org/&quot;&gt;PgBouncer Connection Pooling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>database</category><category>performance</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0011-postgresql-query-optimization-indexes-explain/hero.png" length="0" type="image/jpeg"/></item><item><title>Multi-Environment Secret Management with HashiCorp Vault</title><link>https://mkabumattar.com/codesnippets/post/hashicorp-vault-multi-environment-secrets/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/hashicorp-vault-multi-environment-secrets/</guid><description>Manage secrets securely across dev, staging, and production with HashiCorp Vault. This snippet demonstrates dynamic secret generation, rotation, and cross-environment secret syncing patterns.</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Need to manage secrets safely across multiple environments? Here&amp;#39;s how with HashiCorp Vault.&lt;/h3&gt;
&lt;p&gt;Storing secrets in &lt;code&gt;.env&lt;/code&gt; files, hardcoding them, or even using separate secret managers per environment creates security risks and operational chaos. Vault provides a unified, audited solution for secret generation, rotation, and access control across all your environments.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Multi-Environment Secret Chaos&lt;/h3&gt;
&lt;p&gt;When managing dev, staging, and production environments, secrets management becomes a headache:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different secrets per environment with no central tracking&lt;/li&gt;
&lt;li&gt;Manual rotation processes prone to human error&lt;/li&gt;
&lt;li&gt;No audit trail for who accessed which secrets and when&lt;/li&gt;
&lt;li&gt;Secrets leaking through Git history or logs&lt;/li&gt;
&lt;li&gt;Difficulty revoking compromised credentials instantly&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;HashiCorp Vault Overview&lt;/h3&gt;
&lt;p&gt;Vault is a secrets management platform that provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic secrets&lt;/strong&gt;: Generate credentials on-demand that auto-expire&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret rotation&lt;/strong&gt;: Automatically rotate credentials without downtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit logging&lt;/strong&gt;: Complete trail of all secret access and operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment isolation&lt;/strong&gt;: Separate secrets per environment with shared policies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-auth methods&lt;/strong&gt;: Support for AppRole, Kubernetes, IAM, JWT, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use HashiCorp Vault as a centralized secret backend&lt;/li&gt;
&lt;li&gt;Generate dynamic credentials that auto-expire&lt;/li&gt;
&lt;li&gt;Implement automatic rotation for long-lived secrets&lt;/li&gt;
&lt;li&gt;Audit all secret access across environments&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Installation and Setup&lt;/h2&gt;
&lt;h3&gt;Start Vault with Docker&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;services:
  vault:
    image: vault:latest
    container_name: vault
    ports:
      - &amp;quot;8200:8200&amp;quot;
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: &amp;quot;root&amp;quot;
      VAULT_DEV_LISTEN_ADDRESS: &amp;quot;0.0.0.0:8200&amp;quot;
    cap_add:
      - IPC_LOCK
    volumes:
      - vault-data:/vault/data
    command: server -dev

volumes:
  vault-data:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the Vault server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose up -d
export VAULT_ADDR=&amp;#39;http://localhost:8200&amp;#39;
export VAULT_TOKEN=&amp;#39;root&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CLI Examples&lt;/h2&gt;
&lt;h3&gt;Basic Secret Storage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Store a static secret
vault kv put secret/dev/database \
  username=&amp;quot;dbuser&amp;quot; \
  password=&amp;quot;supersecret&amp;quot; \
  host=&amp;quot;db.dev.internal&amp;quot;

# Retrieve secret
vault kv get secret/dev/database

# Get specific field
vault kv get -field=password secret/dev/database
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Organize Secrets by Environment&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Development secrets
vault kv put secret/dev/app-api \
  api_key=&amp;quot;dev-key-12345&amp;quot; \
  api_secret=&amp;quot;dev-secret-67890&amp;quot;

# Staging secrets
vault kv put secret/staging/app-api \
  api_key=&amp;quot;staging-key-abcde&amp;quot; \
  api_secret=&amp;quot;staging-secret-fghij&amp;quot;

# Production secrets
vault kv put secret/prod/app-api \
  api_key=&amp;quot;prod-key-xyz123&amp;quot; \
  api_secret=&amp;quot;prod-secret-abc456&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Programming Examples&lt;/h2&gt;
&lt;h3&gt;Python: Fetch Secrets&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hvac
import os
from typing import Dict, Any

class VaultClient:
    def __init__(self, vault_addr: str = &amp;quot;http://localhost:8200&amp;quot;, token: str = None):
        self.client = hvac.Client(url=vault_addr, token=token or os.getenv(&amp;#39;VAULT_TOKEN&amp;#39;))

    def get_secret(self, path: str, field: str = None) -&amp;gt; Dict[str, Any]:
        &amp;quot;&amp;quot;&amp;quot;Fetch a secret from Vault&amp;quot;&amp;quot;&amp;quot;
        response = self.client.secrets.kv.v2.read_secret_version(path=path)
        data = response[&amp;#39;data&amp;#39;][&amp;#39;data&amp;#39;]

        if field:
            return data.get(field)
        return data

    def get_database_creds(self, environment: str) -&amp;gt; Dict[str, str]:
        &amp;quot;&amp;quot;&amp;quot;Get database credentials for specific environment&amp;quot;&amp;quot;&amp;quot;
        path = f&amp;quot;secret/{environment}/database&amp;quot;
        return self.get_secret(path)

    def get_api_keys(self, environment: str) -&amp;gt; Dict[str, str]:
        &amp;quot;&amp;quot;&amp;quot;Get API keys for specific environment&amp;quot;&amp;quot;&amp;quot;
        path = f&amp;quot;secret/{environment}/app-api&amp;quot;
        return self.get_secret(path)

# Usage
if __name__ == &amp;quot;__main__&amp;quot;:
    vault = VaultClient()

    # Get dev database credentials
    db_creds = vault.get_database_creds(&amp;quot;dev&amp;quot;)
    print(f&amp;quot;Database: {db_creds[&amp;#39;host&amp;#39;]}&amp;quot;)
    print(f&amp;quot;User: {db_creds[&amp;#39;username&amp;#39;]}&amp;quot;)

    # Get specific field
    api_key = vault.get_secret(&amp;quot;secret/prod/app-api&amp;quot;, field=&amp;quot;api_key&amp;quot;)
    print(f&amp;quot;API Key: {api_key}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js: Fetch Secrets&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import * as VaultClient from &amp;#39;node-vault&amp;#39;;

class SecretManager {
  private vault: ReturnType&amp;lt;typeof VaultClient&amp;gt;;

  constructor(address: string = &amp;#39;http://localhost:8200&amp;#39;, token: string) {
    this.vault = VaultClient({
      apiVersion: &amp;#39;v1&amp;#39;,
      endpoint: address,
      token: token || process.env.VAULT_TOKEN,
    });
  }

  async getSecret(path: string, field?: string): Promise&amp;lt;any&amp;gt; {
    try {
      const response = await this.vault.read(path);
      const data = response.data.data;

      if (field) {
        return data[field];
      }
      return data;
    } catch (error) {
      console.error(`Failed to retrieve secret from ${path}:`, error);
      throw error;
    }
  }

  async getDatabaseConfig(environment: string): Promise&amp;lt;any&amp;gt; {
    return this.getSecret(`secret/${environment}/database`);
  }

  async getApiCredentials(environment: string): Promise&amp;lt;any&amp;gt; {
    return this.getSecret(`secret/${environment}/app-api`);
  }
}

// Usage
(async () =&amp;gt; {
  const manager = new SecretManager(&amp;#39;http://localhost:8200&amp;#39;, &amp;#39;root&amp;#39;);

  const dbConfig = await manager.getDatabaseConfig(&amp;#39;dev&amp;#39;);
  console.log(`Connecting to: ${dbConfig.host}`);

  const apiKey = await manager.getApiCredentials(&amp;#39;prod&amp;#39;);
  console.log(`API Key: ${apiKey.api_key}`);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dynamic Database Credentials&lt;/h2&gt;
&lt;h3&gt;Configure Database Secret Engine&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Enable database secrets engine
vault secrets enable database

# Configure PostgreSQL connection
vault write database/config/postgresql \
  plugin_name=postgresql-database-plugin \
  allowed_roles=&amp;quot;readonly,readwrite&amp;quot; \
  connection_url=&amp;quot;postgresql://admin:password@db.prod.internal:5432/appdb&amp;quot; \
  username=&amp;quot;vault_admin&amp;quot; \
  password=&amp;quot;vault_admin_password&amp;quot;

# Create readonly role
vault write database/roles/readonly \
  db_name=postgresql \
  creation_statements=&amp;quot;CREATE ROLE \&amp;quot;{{name}}\&amp;quot; WITH LOGIN PASSWORD &amp;#39;{{password}}&amp;#39; VALID UNTIL &amp;#39;{{expiration}}&amp;#39;; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \&amp;quot;{{name}}\&amp;quot;;&amp;quot; \
  default_ttl=&amp;quot;1h&amp;quot; \
  max_ttl=&amp;quot;24h&amp;quot;

# Create readwrite role
vault write database/roles/readwrite \
  db_name=postgresql \
  creation_statements=&amp;quot;CREATE ROLE \&amp;quot;{{name}}\&amp;quot; WITH LOGIN PASSWORD &amp;#39;{{password}}&amp;#39; VALID UNTIL &amp;#39;{{expiration}}&amp;#39;; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \&amp;quot;{{name}}\&amp;quot;;&amp;quot; \
  default_ttl=&amp;quot;6h&amp;quot; \
  max_ttl=&amp;quot;24h&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Retrieve Dynamic Credentials&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Get temporary readonly credentials
vault read database/creds/readonly
# Output:
# Key                Value
# ---                -----
# lease_duration     1h
# lease_id           database/creds/readonly/...
# password           a-temporary-password
# username           v-token-readonly-xxxxxxxx

# Get temporary readwrite credentials
vault read database/creds/readwrite
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python: Use Dynamic Credentials&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hvac
import psycopg2
from typing import Generator
import contextlib

class DynamicDatabaseAccess:
    def __init__(self, vault_addr: str, vault_token: str):
        self.vault = hvac.Client(url=vault_addr, token=vault_token)
        self.db_host = os.getenv(&amp;#39;DB_HOST&amp;#39;, &amp;#39;db.prod.internal&amp;#39;)
        self.db_port = os.getenv(&amp;#39;DB_PORT&amp;#39;, &amp;#39;5432&amp;#39;)
        self.db_name = os.getenv(&amp;#39;DB_NAME&amp;#39;, &amp;#39;appdb&amp;#39;)

    def get_dynamic_credentials(self, role: str) -&amp;gt; dict:
        &amp;quot;&amp;quot;&amp;quot;Fetch temporary database credentials from Vault&amp;quot;&amp;quot;&amp;quot;
        response = self.vault.secrets.database.read_dynamic_credentials(role)
        return response[&amp;#39;data&amp;#39;]

    @contextlib.contextmanager
    def get_connection(self, role: str = &amp;#39;readonly&amp;#39;) -&amp;gt; Generator:
        &amp;quot;&amp;quot;&amp;quot;Get a database connection with dynamic credentials&amp;quot;&amp;quot;&amp;quot;
        creds = self.get_dynamic_credentials(role)

        conn = psycopg2.connect(
            host=self.db_host,
            port=self.db_port,
            database=self.db_name,
            user=creds[&amp;#39;username&amp;#39;],
            password=creds[&amp;#39;password&amp;#39;]
        )

        try:
            yield conn
        finally:
            conn.close()

# Usage
db_access = DynamicDatabaseAccess(&amp;#39;http://localhost:8200&amp;#39;, &amp;#39;root&amp;#39;)

with db_access.get_connection(role=&amp;#39;readonly&amp;#39;) as conn:
    cursor = conn.cursor()
    cursor.execute(&amp;quot;SELECT COUNT(*) FROM users&amp;quot;)
    print(f&amp;quot;Total users: {cursor.fetchone()[0]}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Policy-Based Access Control&lt;/h2&gt;
&lt;h3&gt;Create Policies for Different Roles&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-terraform&quot;&gt;# Read-only access to dev/staging secrets
path &amp;quot;secret/data/dev/*&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;, &amp;quot;list&amp;quot;]
}

path &amp;quot;secret/data/staging/*&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;, &amp;quot;list&amp;quot;]
}

# Deny access to production
path &amp;quot;secret/data/prod/*&amp;quot; {
  capabilities = [&amp;quot;deny&amp;quot;]
}

# Allow token self-renewal
path &amp;quot;auth/token/renew-self&amp;quot; {
  capabilities = [&amp;quot;update&amp;quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-terraform&quot;&gt;# Full access to production secrets
path &amp;quot;secret/data/prod/*&amp;quot; {
  capabilities = [&amp;quot;create&amp;quot;, &amp;quot;read&amp;quot;, &amp;quot;update&amp;quot;, &amp;quot;delete&amp;quot;, &amp;quot;list&amp;quot;]
}

# Read-only access to staging
path &amp;quot;secret/data/staging/*&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;, &amp;quot;list&amp;quot;]
}

# Database credential access
path &amp;quot;database/creds/prod/*&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;]
}

# Audit logging
path &amp;quot;sys/audit&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Apply Policies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create policies
vault policy write dev-team policies/dev-team.hcl
vault policy write prod-team policies/prod-team.hcl

# Assign to users/apps
vault write auth/userpass/users/jane policies=&amp;quot;prod-team&amp;quot;
vault write auth/userpass/users/alice policies=&amp;quot;dev-team&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kubernetes Integration&lt;/h2&gt;
&lt;h3&gt;Enable Kubernetes Auth&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Enable Kubernetes authentication
vault auth enable kubernetes

# Configure Kubernetes auth
vault write auth/kubernetes/config \
  token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
  kubernetes_host=&amp;quot;https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT&amp;quot; \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

# Create role for pods
vault write auth/kubernetes/role/app-role \
  bound_service_account_names=app \
  bound_service_account_namespaces=default \
  policies=default,app-secrets \
  ttl=24h
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pod Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      serviceAccountName: app
      containers:
        - name: myapp
          image: myapp:latest
          env:
            - name: VAULT_ADDR
              value: &amp;#39;http://vault.vault.svc.cluster.local:8200&amp;#39;
            - name: VAULT_SKIP_VERIFY
              value: &amp;#39;false&amp;#39;
          volumeMounts:
            - name: vault-token
              mountPath: /vault/secrets
      volumes:
        - name: vault-token
          projected:
            sources:
              - serviceAccountToken:
                  audience: vault
                  expirationSeconds: 3600
                  path: token
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Audit Logging&lt;/h2&gt;
&lt;h3&gt;Enable Audit Logging&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Enable file audit backend
vault audit enable file file_path=/vault/logs/audit.log

# Enable syslog audit backend
vault audit enable syslog tag=&amp;quot;vault&amp;quot;

# View audit logs
tail -f /vault/logs/audit.log | jq &amp;#39;.&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query Audit Logs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Show all secret reads
cat /vault/logs/audit.log | jq &amp;#39;select(.type == &amp;quot;response&amp;quot; and .auth.policy_results.granted_policies &amp;gt;= 0)&amp;#39;

# Show secret modifications
cat /vault/logs/audit.log | jq &amp;#39;select(.type == &amp;quot;request&amp;quot; and .request.operation == &amp;quot;write&amp;quot;)&amp;#39;

# Show failed auth attempts
cat /vault/logs/audit.log | jq &amp;#39;select(.response.auth == null)&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;1. Use AppRole for Applications&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create AppRole
vault auth enable approle

# Generate application role
vault write auth/approle/role/my-app \
  token_ttl=1h \
  token_max_ttl=4h \
  policies=&amp;quot;app-secrets&amp;quot;

# Get role ID
vault read auth/approle/role/my-app/role-id

# Generate secret ID
vault write -f auth/approle/role/my-app/secret-id
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Rotate Secrets Regularly&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Configure auto-rotation every 30 days
vault write database/config/postgresql \
  connection_url=&amp;quot;postgresql://admin:password@db.prod.internal:5432/appdb&amp;quot; \
  rotation_statements=&amp;quot;ALTER ROLE \&amp;quot;{{name}}\&amp;quot; WITH PASSWORD &amp;#39;{{password}}&amp;#39;;&amp;quot; \
  rotation_period=720h
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Implement Least Privilege&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-terraform&quot;&gt;# Only grant necessary capabilities
path &amp;quot;secret/data/myapp/*&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;]  # Not &amp;quot;update&amp;quot; or &amp;quot;delete&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vaultproject.io/docs&quot;&gt;HashiCorp Vault Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.vaultproject.io/api-docs&quot;&gt;Vault API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hvac/hvac&quot;&gt;hvac Python Client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hashicorp/node-vault&quot;&gt;node-vault NPM Package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>security</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0010-hashicorp-vault-multi-environment-secrets/hero.png" length="0" type="image/jpeg"/></item><item><title>Top 7 Open Source OCR Models for Document Processing</title><link>https://mkabumattar.com/codesnippets/post/top-7-open-source-ocr-models/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/top-7-open-source-ocr-models/</guid><description>Explore the best open source OCR models for converting documents, images, and PDFs to text. Compare olmOCR, PaddleOCR, OCRFlux, and more with performance benchmarks and implementation guides.</description><pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;AI Tool&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Turn your documents into perfect digital copies with these powerful open source OCR models. No more dealing with messy text extraction get clean, accurate markdown from PDFs, images, and scanned documents.&lt;/p&gt;
&lt;h2&gt;The OCR Revolution&lt;/h2&gt;
&lt;h3&gt;Beyond Basic Text Extraction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Complete document understanding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These modern OCR models do way more than just read text. They understand document structure, tables, diagrams, math equations, and multiple languages, turning everything into well-formatted markdown.&lt;/p&gt;
&lt;h3&gt;Local Processing Benefits&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Privacy and control&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Run these models right on your own machine without sending sensitive documents to cloud services. You keep complete control over your data while getting accuracy that rivals enterprise solutions.&lt;/p&gt;
&lt;h2&gt;olmOCR-2-7B-1025&lt;/h2&gt;
&lt;h3&gt;High-Performance Document OCR&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Allen Institute&amp;#39;s flagship model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is fine-tuned from Qwen2.5-VL-7B-Instruct using GRPO reinforcement learning, scoring 82.4 on the olmOCR-bench evaluation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

# Load the model
model_id = &amp;quot;allenai/olmOCR-2-7B-1025&amp;quot;
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map=&amp;quot;auto&amp;quot;)

# Process document
messages = [
    {&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;Convert this document to markdown&amp;quot;},
    {&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: f&amp;quot;[IMAGE_PLACEHOLDER]&amp;quot;}
]

inputs = tokenizer.apply_chat_template(messages, return_tensors=&amp;quot;pt&amp;quot;).to(model.device)
outputs = model.generate(inputs, max_new_tokens=4096)
result = tokenizer.decode(outputs[0], skip_special_tokens=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mathematical equations&lt;/strong&gt;: Handles complex math expressions perfectly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Table recognition&lt;/strong&gt;: Keeps table structure and formatting intact&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layout understanding&lt;/strong&gt;: Manages multi-column documents and complex layouts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large-scale processing&lt;/strong&gt;: Built for processing millions of documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated retries&lt;/strong&gt;: Includes error handling and automatic rotation correction&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;PaddleOCR VL&lt;/h2&gt;
&lt;h3&gt;Efficient Multilingual OCR&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ultra-compact vision-language model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Integrates NaViT visual encoder with ERNIE language model, supporting 109 languages with minimal resource usage.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import paddle
from paddlenlp import Taskflow

# Initialize OCR pipeline
ocr = Taskflow(&amp;quot;document_parsing&amp;quot;)

# Process document
result = ocr({&amp;quot;doc&amp;quot;: &amp;quot;path/to/document.pdf&amp;quot;})
print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;109 languages&lt;/strong&gt;: Chinese, English, Japanese, Arabic, Hindi, Thai&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex elements&lt;/strong&gt;: Tables, formulas, charts recognition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High accuracy&lt;/strong&gt;: State-of-the-art performance on OmniDocBench&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast inference&lt;/strong&gt;: Optimized for real-world deployment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compact size&lt;/strong&gt;: Efficient resource utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;OCRFlux-3B&lt;/h2&gt;
&lt;h3&gt;Multimodal Document Conversion&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Preview release from ChatDOC&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Fine-tuned from Qwen2.5-VL-3B-Instruct for clean markdown output from PDFs and images.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from transformers import Qwen2_5_VLForConditionalGeneration, AutoTokenizer, AutoProcessor
from qwen_vl_utils import process_vision_info

# Load model
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
    &amp;quot;ChatDOC/OCRFlux-3B&amp;quot;,
    torch_dtype=torch.bfloat16,
    device_map=&amp;quot;auto&amp;quot;
)
processor = AutoProcessor.from_pretrained(&amp;quot;ChatDOC/OCRFlux-3B&amp;quot;)

# Process image
messages = [
    {
        &amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;,
        &amp;quot;content&amp;quot;: [
            {&amp;quot;type&amp;quot;: &amp;quot;image&amp;quot;, &amp;quot;image&amp;quot;: &amp;quot;path/to/image.jpg&amp;quot;},
            {&amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;, &amp;quot;text&amp;quot;: &amp;quot;Convert to markdown&amp;quot;}
        ]
    }
]

text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(text=[text], images=image_inputs, videos=video_inputs, return_tensors=&amp;quot;pt&amp;quot;).to(model.device)

generated_ids = model.generate(**inputs, max_new_tokens=4096)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clean markdown&lt;/strong&gt;: Structured output with proper formatting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-page tables&lt;/strong&gt;: Merges table data across multiple pages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer hardware&lt;/strong&gt;: Runs on GTX 3090 and similar GPUs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalable deployment&lt;/strong&gt;: vLLM inference support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State-of-the-art accuracy&lt;/strong&gt;: Superior parsing quality&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MiniCPM-V 4.5&lt;/h2&gt;
&lt;h3&gt;Mobile-Optimized OCR&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Latest in the MiniCPM-V series&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built on Qwen3-8B and SigLIP2-400M, delivers exceptional performance for text recognition in images, documents, and videos.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from transformers import AutoModel, AutoTokenizer
import torch

# Load model
model = AutoModel.from_pretrained(&amp;#39;openbmb/MiniCPM-V-4.5&amp;#39;, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(&amp;#39;openbmb/MiniCPM-V-4.5&amp;#39;, trust_remote_code=True)
model.eval().cuda()

# Process image
image = Image.open(&amp;#39;path/to/image.jpg&amp;#39;).convert(&amp;#39;RGB&amp;#39;)
question = &amp;#39;Convert this document to text&amp;#39;

msgs = [{&amp;#39;role&amp;#39;: &amp;#39;user&amp;#39;, &amp;#39;content&amp;#39;: [image, question]}]
result = model.chat(msgs=msgs, tokenizer=tokenizer, sampling=True, temperature=0.7)
print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mobile deployment&lt;/strong&gt;: Optimized for edge devices&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-image processing&lt;/strong&gt;: Handles multiple images simultaneously&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Video OCR&lt;/strong&gt;: Text recognition in video content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State-of-the-art benchmarks&lt;/strong&gt;: Leading performance across evaluations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical efficiency&lt;/strong&gt;: Everyday application ready&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;InternVL2.5-4B&lt;/h2&gt;
&lt;h3&gt;Compact Multimodal Understanding&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Efficient vision-language model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Combines InternViT vision encoder with Qwen2.5 language model for comprehensive OCR and understanding.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import torch
from transformers import AutoTokenizer, AutoModel

# Load model
model = AutoModel.from_pretrained(
    &amp;#39;OpenGVLab/InternVL2_5-4B&amp;#39;,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
    trust_remote_code=True
).eval().cuda()

tokenizer = AutoTokenizer.from_pretrained(
    &amp;#39;OpenGVLab/InternVL2_5-4B&amp;#39;,
    trust_remote_code=True
)

# Process image
image = Image.open(&amp;#39;path/to/image.jpg&amp;#39;).convert(&amp;#39;RGB&amp;#39;)
question = &amp;#39;Extract all text from this image&amp;#39;

response = model.chat(tokenizer, image, question)
print(response)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic resolution&lt;/strong&gt;: 448x448 pixel tile processing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource efficient&lt;/strong&gt;: Suitable for constrained environments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text recognition&lt;/strong&gt;: Strong OCR performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reasoning tasks&lt;/strong&gt;: Advanced multimodal understanding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compact architecture&lt;/strong&gt;: 4 billion parameters total&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Granite Vision 3.3 2b&lt;/h2&gt;
&lt;h3&gt;Document Understanding Specialist&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;IBM&amp;#39;s vision-language model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built on Granite 3.1-2b-instruct with SigLIP2 vision encoder for automated content extraction.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from transformers import AutoProcessor, AutoModelForVision2Seq
import torch

# Load model
processor = AutoProcessor.from_pretrained(&amp;quot;ibm-granite/granite-vision-3.3-2b&amp;quot;)
model = AutoModelForVision2Seq.from_pretrained(&amp;quot;ibm-granite/granite-vision-3.3-2b&amp;quot;, device_map=&amp;quot;auto&amp;quot;)

# Process image
image = Image.open(&amp;quot;path/to/document.png&amp;quot;).convert(&amp;quot;RGB&amp;quot;)
text_prompt = &amp;quot;&amp;lt;|start_of_text|&amp;gt;&amp;quot;

inputs = processor(text=text_prompt, images=image, return_tensors=&amp;quot;pt&amp;quot;).to(model.device)
generated_ids = model.generate(**inputs, max_new_tokens=500)
generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Table extraction&lt;/strong&gt;: Automated table content extraction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chart recognition&lt;/strong&gt;: Infographics and plot understanding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-page support&lt;/strong&gt;: Handles multi-page documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image segmentation&lt;/strong&gt;: Advanced visual processing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced safety&lt;/strong&gt;: Improved security features&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TrOCR Large (SROIE Fine-tuned)&lt;/h2&gt;
&lt;h3&gt;Specialized Text Recognition&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Transformer-based OCR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Encoder-decoder architecture combining BEiT image transformer with RoBERTa text transformer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from transformers import TrOCRProcessor, VisionEncoderDecoderModel
import torch

# Load model
processor = TrOCRProcessor.from_pretrained(&amp;#39;microsoft/trocr-large-printed&amp;#39;)
model = VisionEncoderDecoderModel.from_pretrained(&amp;#39;microsoft/trocr-large-printed&amp;#39;)

# Process image
image = Image.open(&amp;#39;path/to/image.jpg&amp;#39;).convert(&amp;#39;RGB&amp;#39;)
pixel_values = processor(images=image, return_tensors=&amp;quot;pt&amp;quot;).pixel_values
generated_ids = model.generate(pixel_values)

generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(generated_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single-line text&lt;/strong&gt;: Optimized for printed text recognition&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High accuracy&lt;/strong&gt;: State-of-the-art performance on benchmarks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transformer architecture&lt;/strong&gt;: Modern deep learning approach&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-trained models&lt;/strong&gt;: Leverages large-scale training data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sequence processing&lt;/strong&gt;: Handles text as sequential tokens&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Choosing the Right Model&lt;/h2&gt;
&lt;h3&gt;Use Case Considerations&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Match model to needs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High accuracy&lt;/strong&gt;: olmOCR-2-7B-1025, OCRFlux-3B&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: PaddleOCR VL, InternVL2.5-4B&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multilingual&lt;/strong&gt;: PaddleOCR VL, MiniCPM-V 4.5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile/Edge&lt;/strong&gt;: MiniCPM-V 4.5, InternVL2.5-4B&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Specialized&lt;/strong&gt;: TrOCR (printed text), Granite Vision (documents)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Performance Optimization&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hardware considerations&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU memory&lt;/strong&gt;: Match model size to available VRAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch processing&lt;/strong&gt;: Use models supporting multiple images&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quantization&lt;/strong&gt;: Consider quantized versions for efficiency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local deployment&lt;/strong&gt;: All models support local inference&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementation Best Practices&lt;/h2&gt;
&lt;h3&gt;Preprocessing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Optimize input quality&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Image preprocessing
def preprocess_image(image_path):
    image = Image.open(image_path).convert(&amp;#39;RGB&amp;#39;)
    # Resize if needed
    if max(image.size) &amp;gt; 2240:
        image = image.resize((2240, 2240), Image.Resampling.LANCZOS)
    return image
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Robust processing&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def safe_ocr_processing(model, image_path):
    try:
        image = preprocess_image(image_path)
        result = model.process(image)
        return result
    except Exception as e:
        logging.error(f&amp;quot;OCR processing failed: {e}&amp;quot;)
        return None
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Output Formatting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Structured results&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def format_ocr_output(raw_text, confidence_scores=None):
    &amp;quot;&amp;quot;&amp;quot;Format OCR output with metadata&amp;quot;&amp;quot;&amp;quot;
    return {
        &amp;quot;text&amp;quot;: raw_text,
        &amp;quot;confidence&amp;quot;: confidence_scores,
        &amp;quot;timestamp&amp;quot;: datetime.now().isoformat(),
        &amp;quot;model_version&amp;quot;: &amp;quot;olmOCR-2-7B-1025&amp;quot;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These open source OCR models represent the cutting edge of document processing technology. Choose based on your specific requirements accuracy, speed, language support, or hardware constraints and start converting documents with unprecedented quality.&lt;/p&gt;
</content:encoded><category>ai-ml</category><category>computer-vision</category><category>document-processing</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0009-top-7-open-source-ocr-models/hero.png" length="0" type="image/jpeg"/></item><item><title>Why printf Beats echo in Linux Scripts</title><link>https://mkabumattar.com/codesnippets/post/printf-beats-echo-linux-scripts/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/printf-beats-echo-linux-scripts/</guid><description>Learn why printf is superior to echo for output in Linux scripts. Discover the reliability issues with echo, the advantages of printf, and when to use each command for better scripting practices.</description><pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Scripting Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You know that feeling when a script works perfectly on your machine but fails miserably somewhere else? That&amp;#39;s probably because you&amp;#39;re using echo for output. Let me show you why printf is a much better choice for your Linux scripts.&lt;/p&gt;
&lt;h2&gt;The Problem with echo&lt;/h2&gt;
&lt;h3&gt;Unpredictable Behavior&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;echo&amp;#39;s reliability issues&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;echo&lt;/code&gt; command doesn&amp;#39;t behave the same way everywhere. Options like &lt;code&gt;-n&lt;/code&gt; (to suppress the newline) or escape sequences like &lt;code&gt;\n&lt;/code&gt; and &lt;code&gt;\t&lt;/code&gt; might work fine in one shell but act completely different in another.&lt;/p&gt;
&lt;h3&gt;Real-World Script Failures&lt;/h3&gt;
&lt;p&gt;You&amp;#39;ve got a script that runs great on your system, but when you deploy it to production or share it with a colleague, it starts acting up. These inconsistencies create bugs that are really hard to track down and fix.&lt;/p&gt;
&lt;h2&gt;Why printf is More Reliable&lt;/h2&gt;
&lt;h3&gt;POSIX Standard Compliance&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Predictable output everywhere&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt; follows the POSIX standard, so it works exactly the same across all shells and systems. You don&amp;#39;t have to worry about whether your escape sequences will actually work or not.&lt;/p&gt;
&lt;h3&gt;Advanced Formatting Capabilities&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt; gives you real control over how your output looks. You can align text, format numbers precisely, and combine multiple values in a single command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Simple alignment
printf &amp;quot;%-10s %s\n&amp;quot; &amp;quot;User:&amp;quot; &amp;quot;$USER&amp;quot;

# Clean numeric formatting
printf &amp;quot;Usage: %.2f%%\n&amp;quot; 85.6789

# Multiple values at once
printf &amp;quot;%s logged in at %s\n&amp;quot; &amp;quot;$USER&amp;quot; &amp;quot;$(date)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Replacing echo with printf&lt;/h2&gt;
&lt;h3&gt;Basic Text Output&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Simple substitution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of &lt;code&gt;echo &amp;quot;Hello, world&amp;quot;&lt;/code&gt;, just write &lt;code&gt;printf &amp;quot;Hello, world\n&amp;quot;&lt;/code&gt;. The newline is right there in the command, so you always know what you&amp;#39;re getting.&lt;/p&gt;
&lt;h3&gt;Suppressing Newlines&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;No more -n confusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Forget about &lt;code&gt;echo -n &amp;quot;Processing...&amp;quot;&lt;/code&gt; and its inconsistent behavior. With &lt;code&gt;printf&lt;/code&gt;, you just omit the newline: &lt;code&gt;printf &amp;quot;Processing...&amp;quot;&lt;/code&gt;. Simple and reliable.&lt;/p&gt;
&lt;h3&gt;Variable Output Safety&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Proper variable handling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t use &lt;code&gt;echo $VARIABLE&lt;/code&gt; - it can cause all sorts of issues with spaces or special characters. Go with &lt;code&gt;printf &amp;quot;%s\n&amp;quot; &amp;quot;$VARIABLE&amp;quot;&lt;/code&gt; instead. It&amp;#39;s safe and predictable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Safe variable printing
name=&amp;quot;John Doe&amp;quot;
printf &amp;quot;User: %s\n&amp;quot; &amp;quot;$name&amp;quot;

# Multiple variables work great
printf &amp;quot;User: %s | UID: %d\n&amp;quot; &amp;quot;$USER&amp;quot; &amp;quot;$UID&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Escape Sequences&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Guaranteed processing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt; always handles escape sequences correctly. &lt;code&gt;echo&lt;/code&gt; might just print them as literal text, depending on your shell.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Reliable line breaks
printf &amp;quot;Line 1\nLine 2\n&amp;quot;

# Clean tabbed output
printf &amp;quot;Name:\t%s\nAge:\t%d\n&amp;quot; &amp;quot;$name&amp;quot; &amp;quot;$age&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Structured Output with printf&lt;/h2&gt;
&lt;h3&gt;Table Formatting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Clean data presentation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can create nicely aligned tables and structured output that other programs can easily read.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# CPU usage table
printf &amp;quot;%-8s %s\n&amp;quot; &amp;quot;CPU&amp;quot; &amp;quot;Usage&amp;quot;
printf &amp;quot;%-8s %d%%\n&amp;quot; &amp;quot;core0&amp;quot; 42
printf &amp;quot;%-8s %d%%\n&amp;quot; &amp;quot;core1&amp;quot; 37
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSV Generation&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Consistent data export&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate CSV files that format properly no matter where your script runs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;printf &amp;quot;%s,%s,%s\n&amp;quot; &amp;quot;Name&amp;quot; &amp;quot;Age&amp;quot; &amp;quot;City&amp;quot;
printf &amp;quot;%s,%s,%s\n&amp;quot; &amp;quot;$name&amp;quot; &amp;quot;$age&amp;quot; &amp;quot;$city&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;When to Use Which Command&lt;/h2&gt;
&lt;h3&gt;echo for Quick Tasks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Interactive use cases&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;echo&lt;/code&gt; is still fine for quick checks in the terminal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Testing something quickly&lt;/li&gt;
&lt;li&gt;Simple debugging output&lt;/li&gt;
&lt;li&gt;Interactive shell sessions&lt;/li&gt;
&lt;li&gt;One-off commands&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;printf for Scripts&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Production code&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;printf&lt;/code&gt; when it matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Production scripts&lt;/li&gt;
&lt;li&gt;Automated tools&lt;/li&gt;
&lt;li&gt;Log file generation&lt;/li&gt;
&lt;li&gt;Any output that other programs will read&lt;/li&gt;
&lt;li&gt;Scripts that need to work across different systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Locale Considerations&lt;/h2&gt;
&lt;h3&gt;Numeric Formatting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Decimal separator issues&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt; respects your system&amp;#39;s locale settings. In some countries, decimal points become commas, which can break your scripts if you&amp;#39;re not careful.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Force standard decimal format
LC_NUMERIC=C printf &amp;quot;%.2f\n&amp;quot; 3.14159
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Safe Numeric Handling&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Prevent locale surprises&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For scripts that need to work everywhere, set &lt;code&gt;LC_NUMERIC=C&lt;/code&gt; or handle numbers explicitly to avoid any locale-related issues.&lt;/p&gt;
&lt;h2&gt;Making the Switch&lt;/h2&gt;
&lt;h3&gt;Gradual Migration&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Update existing scripts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can replace your &lt;code&gt;echo&lt;/code&gt; statements one by one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;echo &amp;quot;text&amp;quot;&lt;/code&gt; becomes &lt;code&gt;printf &amp;quot;text\n&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo -n &amp;quot;text&amp;quot;&lt;/code&gt; becomes &lt;code&gt;printf &amp;quot;text&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo $var&lt;/code&gt; becomes &lt;code&gt;printf &amp;quot;%s\n&amp;quot; &amp;quot;$var&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Testing Output&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Verify behavior&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Always test your scripts on different systems and shells to make sure the output looks right everywhere.&lt;/p&gt;
&lt;h2&gt;The printf Advantage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt; gives you the reliability and control you need for serious scripting. &lt;code&gt;echo&lt;/code&gt; works for quick tasks, but &lt;code&gt;printf&lt;/code&gt; makes sure your scripts behave predictably no matter where they run. I recommend making &lt;code&gt;printf&lt;/code&gt; your default choice for output in production code.&lt;/p&gt;
</content:encoded><category>shell-scripting</category><category>devops</category><category>linux</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0008-printf-beats-echo-linux-scripts/hero.png" length="0" type="image/jpeg"/></item><item><title>Essential Bash Variables for Every Script</title><link>https://mkabumattar.com/codesnippets/post/essential-bash-variables/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/essential-bash-variables/</guid><description>Master the most useful Bash special parameters and environment variables. Learn how to use $0, $?, $@, $UID, $EUID, and XDG variables to write more robust and portable scripts.</description><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You know what&amp;#39;s worse than writing scripts? Writing scripts that break every time you move them to a different machine. Let&amp;#39;s fix that with some built-in Bash variables that&amp;#39;ll make your life way easier.&lt;/p&gt;
&lt;h2&gt;The Problem with Hard-coded Paths&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ve all been there. You hard-code &lt;code&gt;/home/john/.config&lt;/code&gt; in your script, and it works great on your laptop. Then you run it on the server, and boom everything breaks because the server uses a different username or directory structure. When you&amp;#39;ve got dozens (or hundreds) of scripts, tracking down all these hard-coded values becomes a real headache.&lt;/p&gt;
&lt;h2&gt;The Solution: Built-in Bash Variables&lt;/h2&gt;
&lt;p&gt;Bash already knows about your system. It&amp;#39;s got built-in variables for script paths, user IDs, home directories, and more. Instead of guessing or hard-coding, just ask Bash. Your scripts will work everywhere without modification.&lt;/p&gt;
&lt;h2&gt;Key Bash Variables Summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;$BASH_SOURCE&lt;/code&gt; (or &lt;code&gt;$0&lt;/code&gt;) to get the script path reliably&lt;/li&gt;
&lt;li&gt;Check exit codes with &lt;code&gt;$?&lt;/code&gt; to handle errors properly&lt;/li&gt;
&lt;li&gt;Access arguments with &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;$@&lt;/code&gt;, and &lt;code&gt;$*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Get user IDs with &lt;code&gt;$UID&lt;/code&gt; and &lt;code&gt;$EUID&lt;/code&gt; for permission checks&lt;/li&gt;
&lt;li&gt;Use XDG variables for standard user directories instead of hard-coding paths&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Special Parameters&lt;/h2&gt;
&lt;h3&gt;Get the Script Path&lt;/h3&gt;
&lt;p&gt;Sometimes you need to know where your script lives. Maybe you&amp;#39;re building a help menu and want to show the script name, or you need to reference files relative to the script&amp;#39;s location. Here&amp;#39;s how you grab that info.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# $BASH_SOURCE gives you the full path to this script
SCRIPT_PATH=&amp;quot;$BASH_SOURCE&amp;quot;
echo &amp;quot;Full path: $SCRIPT_PATH&amp;quot;
# Output: /home/user/scripts/my_script.sh

# Use basename to strip the directory and keep just the filename
SCRIPT_NAME=&amp;quot;$(basename &amp;quot;$BASH_SOURCE&amp;quot;)&amp;quot;
echo &amp;quot;Script name: $SCRIPT_NAME&amp;quot;
# Output: my_script.sh

# Here&amp;#39;s a practical use case: building a help menu
cat &amp;lt;&amp;lt;EOF
Usage: $SCRIPT_NAME [OPTIONS]

Options:
  -h, --help     Show this help message
  -v, --version  Show version information
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;tip&quot;&gt;&lt;p&gt;There&amp;#39;s also &lt;code&gt;$0&lt;/code&gt;, which works similarly but has a quirk. If someone sources your script (like &lt;code&gt;source my_script.sh&lt;/code&gt;), &lt;code&gt;$0&lt;/code&gt; returns &amp;quot;bash&amp;quot; instead of the script name. &lt;code&gt;$BASH_SOURCE&lt;/code&gt; doesn&amp;#39;t have this issue, so use that if you&amp;#39;re writing Bash-specific code.&lt;/p&gt;
&lt;/Notice&gt;&lt;h3&gt;Check Exit Status&lt;/h3&gt;
&lt;p&gt;When a command finishes, it leaves behind an exit code kind of like a status report. If everything went fine, you get &lt;code&gt;0&lt;/code&gt;. If something went wrong, you get a number between &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;255&lt;/code&gt;. The variable &lt;code&gt;$?&lt;/code&gt; holds this exit code, and you can use it to figure out what happened and respond accordingly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Method 1: Explicit check using $?
ls /nonexistent_directory
if [[ $? -ne 0 ]]; then
  echo &amp;quot;Error: Directory does not exist&amp;quot;
  exit 1
fi

# Method 2: Cleaner approach test the command directly
# If ls succeeds, run the &amp;#39;then&amp;#39; block. If it fails, run &amp;#39;else&amp;#39;
if ls /home; then
  echo &amp;quot;Directory exists&amp;quot;
else
  echo &amp;quot;Directory does not exist&amp;quot;
fi

# Method 3: One-liner with logical operators
# &amp;amp;&amp;amp; means &amp;quot;run this if the previous command succeeded&amp;quot;
# || means &amp;quot;run this if the previous command failed&amp;quot;
ls /home &amp;amp;&amp;amp; echo &amp;quot;Success!&amp;quot; || echo &amp;quot;Failed!&amp;quot;

# Method 4: Handle specific error codes
# Some commands return different numbers for different errors
grep &amp;quot;pattern&amp;quot; file.txt
case $? in
  0) echo &amp;quot;Pattern found&amp;quot;;;
  1) echo &amp;quot;Pattern not found&amp;quot;;;
  2) echo &amp;quot;File error or syntax problem&amp;quot;;;
  *) echo &amp;quot;Unknown error&amp;quot;;;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;important&quot;&gt;&lt;p&gt;Here&amp;#39;s the catch &lt;code&gt;$?&lt;/code&gt; only remembers the last command. If you run another command, the old exit code is gone. So if you need it later, save it right away:&lt;/p&gt;
&lt;/Notice&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;some_command
exit_code=$?  # Save it now!
echo &amp;quot;Did some other stuff&amp;quot;
# Now we can still check the original exit code
if [[ $exit_code -ne 0 ]]; then
  echo &amp;quot;Original command failed&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Access Script Arguments&lt;/h3&gt;
&lt;p&gt;You&amp;#39;ll want your scripts to accept arguments things like filenames, options, or configuration values. Bash makes this pretty straightforward, though the syntax looks a bit weird at first. Let&amp;#39;s break it down.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Individual arguments are numbered: $1, $2, $3, etc.
echo &amp;quot;First argument: $1&amp;quot;
echo &amp;quot;Second argument: $2&amp;quot;
echo &amp;quot;Third argument: $3&amp;quot;
# Run like: ./script.sh hello world test
# Output: hello, world, test

# Count how many arguments were passed
echo &amp;quot;Total arguments: ${#@}&amp;quot;

# Loop through all arguments
# $@ treats each argument separately, which is usually what you want
echo -e &amp;quot;\nProcessing all arguments:&amp;quot;
for arg in &amp;quot;$@&amp;quot;; do
  echo &amp;quot;  - $arg&amp;quot;
done

# The difference between $@ and $*
# This matters when your arguments contain spaces
function demo_at {
  # Each argument stays separate
  printf &amp;#39;@: [%s]\n&amp;#39; &amp;quot;$@&amp;quot;
}

function demo_star {
  # All arguments merge into one string
  printf &amp;#39;*: [%s]\n&amp;#39; &amp;quot;$*&amp;quot;
}

echo -e &amp;quot;\nDifference between \$@ and \$*:&amp;quot;
demo_at &amp;quot;arg one&amp;quot; &amp;quot;arg two&amp;quot;    # Prints: @: [arg one] and @: [arg two]
demo_star &amp;quot;arg one&amp;quot; &amp;quot;arg two&amp;quot;  # Prints: *: [arg one arg two] (all together)

# Real-world example: A function that processes files
function process_files() {
  # First, make sure we got at least one file
  if [[ ${#@} -eq 0 ]]; then
    echo &amp;quot;Error: No files specified&amp;quot;
    echo &amp;quot;Usage: process_files file1 file2 ...&amp;quot;
    return 1
  fi

  # Process each file
  for file in &amp;quot;$@&amp;quot;; do
    if [[ -f &amp;quot;$file&amp;quot; ]]; then
      echo &amp;quot;Processing: $file&amp;quot;
      # Your actual file processing logic goes here
      # wc -l &amp;quot;$file&amp;quot;  # Example: count lines
    else
      echo &amp;quot;Warning: File not found: $file&amp;quot;
    fi
  done
}

# You&amp;#39;d call it like this:
# process_files report.txt data.csv notes.md
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;tip&quot;&gt;&lt;p&gt;Always use quotes around &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;. If you don&amp;#39;t, filenames with spaces will break. For example, without quotes, &lt;code&gt;&amp;quot;my file.txt&amp;quot;&lt;/code&gt; becomes two separate arguments: &lt;code&gt;&amp;quot;my&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;file.txt&amp;quot;&lt;/code&gt;. With quotes, it stays as one argument.&lt;/p&gt;
&lt;/Notice&gt;&lt;h2&gt;Environment Variables&lt;/h2&gt;
&lt;h3&gt;Get User ID&lt;/h3&gt;
&lt;p&gt;Sometimes you need to know who&amp;#39;s running your script. Maybe you&amp;#39;re creating user-specific temp files, or you need to check if someone&amp;#39;s running as root. That&amp;#39;s where &lt;code&gt;$UID&lt;/code&gt; and &lt;code&gt;$EUID&lt;/code&gt; come in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Root check: A common pattern for admin scripts
# Root user always has UID 0
if [[ $EUID -eq 0 ]]; then
  echo &amp;quot;Running with root privileges&amp;quot;
  # Safe to do system-level stuff here
else
  echo &amp;quot;Running as regular user (UID: $UID)&amp;quot;
  echo &amp;quot;Some operations may require sudo&amp;quot;
  # Maybe prompt for sudo, or just warn and continue
fi

# Build paths specific to the current user
# This is useful for multi-user systems
USER_CACHE_DIR=&amp;quot;/run/user/$UID/cache&amp;quot;
echo &amp;quot;User cache directory: $USER_CACHE_DIR&amp;quot;
# Example: /run/user/1000/cache

# Practical example: Create a temp directory that&amp;#39;s unique per user
# This prevents conflicts when multiple users run your script
function create_user_temp() {
  local temp_dir=&amp;quot;/tmp/myapp-$UID&amp;quot;

  if [[ ! -d &amp;quot;$temp_dir&amp;quot; ]]; then
    mkdir -p &amp;quot;$temp_dir&amp;quot;
    echo &amp;quot;Created temp directory: $temp_dir&amp;quot;
  fi

  echo &amp;quot;$temp_dir&amp;quot;
}

# Now each user gets their own isolated temp space
TEMP_DIR=$(create_user_temp)
echo &amp;quot;Using temp directory: $TEMP_DIR&amp;quot;
# User 1000: /tmp/myapp-1000
# User 1001: /tmp/myapp-1001
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;note&quot;&gt;&lt;p&gt;You might be wondering about the difference between &lt;code&gt;$UID&lt;/code&gt; and &lt;code&gt;$EUID&lt;/code&gt;. Honestly, for most scripts, they&amp;#39;re the same. They only differ in weird edge cases involving &lt;code&gt;sudo&lt;/code&gt; or special setuid programs. When in doubt, just use &lt;code&gt;$EUID&lt;/code&gt; for permission checks it&amp;#39;s what matters.&lt;/p&gt;
&lt;/Notice&gt;&lt;h3&gt;Use XDG Directory Specification&lt;/h3&gt;
&lt;p&gt;Here&amp;#39;s a question: where should your script save config files? Or cache data? Or store downloaded files? You might think &amp;quot;just use &lt;code&gt;~/.config&lt;/code&gt;&amp;quot; but what if a user wants to organize things differently? That&amp;#39;s where XDG variables come in they&amp;#39;re the standard way Linux systems handle user directories.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Set XDG variables if they&amp;#39;re not already set
# The syntax ${VAR:=default} means &amp;quot;use VAR if it exists, otherwise use default&amp;quot;
export &amp;quot;${XDG_CONFIG_HOME:=$HOME/.config}&amp;quot;        # Config files
export &amp;quot;${XDG_CACHE_HOME:=$HOME/.cache}&amp;quot;          # Temporary cache data
export &amp;quot;${XDG_DATA_HOME:=$HOME/.local/share}&amp;quot;    # Application data
export &amp;quot;${XDG_STATE_HOME:=$HOME/.local/state}&amp;quot;   # State files (logs, history, etc.)

# Now build your app&amp;#39;s specific directories
APP_NAME=&amp;quot;myapp&amp;quot;
APP_CONFIG_DIR=&amp;quot;$XDG_CONFIG_HOME/$APP_NAME&amp;quot;  # ~/.config/myapp
APP_CACHE_DIR=&amp;quot;$XDG_CACHE_HOME/$APP_NAME&amp;quot;    # ~/.cache/myapp
APP_DATA_DIR=&amp;quot;$XDG_DATA_HOME/$APP_NAME&amp;quot;      # ~/.local/share/myapp

# Create these directories if they don&amp;#39;t exist yet
function initialize_app_directories() {
  mkdir -p &amp;quot;$APP_CONFIG_DIR&amp;quot;
  mkdir -p &amp;quot;$APP_CACHE_DIR&amp;quot;
  mkdir -p &amp;quot;$APP_DATA_DIR&amp;quot;

  echo &amp;quot;Initialized application directories:&amp;quot;
  echo &amp;quot;  Config: $APP_CONFIG_DIR&amp;quot;
  echo &amp;quot;  Cache:  $APP_CACHE_DIR&amp;quot;
  echo &amp;quot;  Data:   $APP_DATA_DIR&amp;quot;
}

initialize_app_directories

# Example: Save settings to the config directory
function save_config() {
  local config_file=&amp;quot;$APP_CONFIG_DIR/settings.conf&amp;quot;

  cat &amp;gt; &amp;quot;$config_file&amp;quot; &amp;lt;&amp;lt;EOF
# Application settings
debug_mode=false
log_level=info
max_connections=100
EOF

  echo &amp;quot;Saved config to: $config_file&amp;quot;
}

# Example: Load settings from the config directory
function load_config() {
  local config_file=&amp;quot;$APP_CONFIG_DIR/settings.conf&amp;quot;

  if [[ -f &amp;quot;$config_file&amp;quot; ]]; then
    # Source the config file to load variables
    source &amp;quot;$config_file&amp;quot;
    echo &amp;quot;Loaded config from: $config_file&amp;quot;
    echo &amp;quot;Debug mode: $debug_mode&amp;quot;
    echo &amp;quot;Log level: $log_level&amp;quot;
  else
    echo &amp;quot;Config file not found: $config_file&amp;quot;
    return 1
  fi
}

save_config
load_config
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;Notice type=&quot;tip&quot;&gt;&lt;p&gt;This isn&amp;#39;t just about being &amp;quot;correct&amp;quot; it&amp;#39;s about respecting how users organize their systems. Some people put config files on a separate partition, or use custom paths for backups. By using XDG variables, your script just works in all these scenarios.&lt;/p&gt;
&lt;/Notice&gt;&lt;h2&gt;Complete Example: Production-Ready Script&lt;/h2&gt;
&lt;p&gt;Alright, let&amp;#39;s put it all together. Here&amp;#39;s what a professional script looks like when you use all these variables properly. It&amp;#39;s got error handling, logging, argument processing, and follows XDG standards. Feel free to use this as a template for your own scripts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Strict mode: exit on errors, undefined variables, and pipe failures
set -euo pipefail

# Set up XDG directories with sensible defaults
export &amp;quot;${XDG_CONFIG_HOME:=$HOME/.config}&amp;quot;
export &amp;quot;${XDG_CACHE_HOME:=$HOME/.cache}&amp;quot;
export &amp;quot;${XDG_DATA_HOME:=$HOME/.local/share}&amp;quot;

# Grab script info using the variables we learned about
SCRIPT_NAME=&amp;quot;$(basename &amp;quot;$BASH_SOURCE&amp;quot;)&amp;quot;
SCRIPT_DIR=&amp;quot;$(cd &amp;quot;$(dirname &amp;quot;$BASH_SOURCE&amp;quot;)&amp;quot; &amp;amp;&amp;amp; pwd)&amp;quot;
APP_NAME=&amp;quot;myapp&amp;quot;

# Build our application&amp;#39;s directory structure
APP_CONFIG_DIR=&amp;quot;$XDG_CONFIG_HOME/$APP_NAME&amp;quot;
APP_CACHE_DIR=&amp;quot;$XDG_CACHE_HOME/$APP_NAME&amp;quot;
APP_LOG_FILE=&amp;quot;$APP_CACHE_DIR/app.log&amp;quot;

# ANSI color codes for pretty output
RED=&amp;#39;\033[0;31m&amp;#39;
GREEN=&amp;#39;\033[0;32m&amp;#39;
YELLOW=&amp;#39;\033[1;33m&amp;#39;
NC=&amp;#39;\033[0m&amp;#39;  # Reset to no color

# Simple logging function with timestamps
# Usage: log &amp;quot;INFO&amp;quot; &amp;quot;Something happened&amp;quot;
function log() {
  local level=&amp;quot;$1&amp;quot;
  shift
  local message=&amp;quot;$*&amp;quot;
  local timestamp=$(date &amp;#39;+%Y-%m-%d %H:%M:%S&amp;#39;)

  # Print to screen AND save to log file
  echo &amp;quot;[$timestamp] [$level] $message&amp;quot; | tee -a &amp;quot;$APP_LOG_FILE&amp;quot;
}

# Centralized error handling
# This logs the error and exits with a non-zero status
function error_exit() {
  log &amp;quot;ERROR&amp;quot; &amp;quot;$*&amp;quot; &amp;gt;&amp;amp;2
  exit 1
}

# Check if we have everything we need to run
function check_prerequisites() {
  # Example: Some scripts need root privileges
  if [[ $EUID -ne 0 ]]; then
    error_exit &amp;quot;This script must be run as root (current UID: $UID)&amp;quot;
  fi

  # Verify required commands are installed
  local required_commands=(&amp;quot;curl&amp;quot; &amp;quot;jq&amp;quot; &amp;quot;aws&amp;quot;)
  for cmd in &amp;quot;${required_commands[@]}&amp;quot;; do
    if ! command -v &amp;quot;$cmd&amp;quot; &amp;amp;&amp;gt; /dev/null; then
      error_exit &amp;quot;Required command not found: $cmd. Please install it first.&amp;quot;
    fi
  done

  log &amp;quot;INFO&amp;quot; &amp;quot;All prerequisites satisfied&amp;quot;
}

# Set up directories and initialize the app
function initialize() {
  # Create necessary directories
  mkdir -p &amp;quot;$APP_CONFIG_DIR&amp;quot; &amp;quot;$APP_CACHE_DIR&amp;quot;
  touch &amp;quot;$APP_LOG_FILE&amp;quot;

  # Log some useful info for debugging
  log &amp;quot;INFO&amp;quot; &amp;quot;Initialized $APP_NAME&amp;quot;
  log &amp;quot;INFO&amp;quot; &amp;quot;Script: $SCRIPT_NAME (located at $SCRIPT_DIR)&amp;quot;
  log &amp;quot;INFO&amp;quot; &amp;quot;User: $USER (UID: $UID)&amp;quot;
}

# Parse command-line arguments
function process_arguments() {
  # If no arguments, show help and exit
  if [[ ${#@} -eq 0 ]]; then
    cat &amp;lt;&amp;lt;EOF
Usage: $SCRIPT_NAME [OPTIONS] [FILES...]

Options:
  -h, --help     Show this help message
  -v, --verbose  Enable verbose logging
  -d, --debug    Enable debug mode

Examples:
  $SCRIPT_NAME file1.txt file2.txt
  $SCRIPT_NAME --verbose *.log
EOF
    exit 0
  fi

  # Set up some flags
  local verbose=false
  local debug=false
  local files=()

  # Loop through all arguments
  while [[ $# -gt 0 ]]; do
    case &amp;quot;$1&amp;quot; in
      -h|--help)
        process_arguments  # Recursively call to show help
        ;;
      -v|--verbose)
        verbose=true
        log &amp;quot;INFO&amp;quot; &amp;quot;Verbose mode enabled&amp;quot;
        shift
        ;;
      -d|--debug)
        debug=true
        set -x  # This makes Bash print every command before running it
        shift
        ;;
      -*)
        error_exit &amp;quot;Unknown option: $1&amp;quot;
        ;;
      *)
        # Anything that doesn&amp;#39;t start with - is treated as a file
        files+=(&amp;quot;$1&amp;quot;)
        shift
        ;;
    esac
  done

  log &amp;quot;INFO&amp;quot; &amp;quot;Processing ${#files[@]} file(s)&amp;quot;

  # Process each file
  for file in &amp;quot;${files[@]}&amp;quot;; do
    if [[ -f &amp;quot;$file&amp;quot; ]]; then
      log &amp;quot;INFO&amp;quot; &amp;quot;Processing file: $file&amp;quot;
      # Your actual file processing logic would go here
      # Example: wc -l &amp;quot;$file&amp;quot;
    else
      log &amp;quot;WARN&amp;quot; &amp;quot;File not found: $file&amp;quot;
    fi
  done
}

# Main entry point
function main() {
  initialize
  check_prerequisites || exit $?
  process_arguments &amp;quot;$@&amp;quot; || exit $?

  log &amp;quot;INFO&amp;quot; &amp;quot;Script completed successfully&amp;quot;
}

# This is where everything starts
# We pass all command-line arguments to main using &amp;quot;$@&amp;quot;
main &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick Reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$0&lt;/code&gt; or &lt;code&gt;$BASH_SOURCE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Script path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/user/script.sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$(basename $0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Script name only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;script.sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exit status of last command&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt; (success) or &lt;code&gt;1-255&lt;/code&gt; (error)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;$3&lt;/code&gt;...&lt;/td&gt;
&lt;td&gt;Positional parameters&lt;/td&gt;
&lt;td&gt;First, second, third argument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All arguments as array&lt;/td&gt;
&lt;td&gt;Each argument separate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All arguments as string&lt;/td&gt;
&lt;td&gt;All arguments in one string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${#@}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of arguments&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$UID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real user ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$EUID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Effective user ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt; (when using sudo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;&lt;code&gt;john&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Home directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/john&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$XDG_CONFIG_HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Config directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.config&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$XDG_CACHE_HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$XDG_DATA_HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Data directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.local/share&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;Do these things:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always quote your variables:&lt;/strong&gt; Use &lt;code&gt;&amp;quot;$var&amp;quot;&lt;/code&gt; instead of &lt;code&gt;$var&lt;/code&gt;. This prevents weird issues when variables contain spaces or special characters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set defaults for XDG variables:&lt;/strong&gt; Use &lt;code&gt;${XDG_CONFIG_HOME:=$HOME/.config}&lt;/code&gt; so your script works even if the variable isn&amp;#39;t set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer &lt;code&gt;$BASH_SOURCE&lt;/code&gt; over &lt;code&gt;$0&lt;/code&gt;:&lt;/strong&gt; It&amp;#39;s more reliable, especially when your script might be sourced instead of executed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check exit codes for important operations:&lt;/strong&gt; Don&amp;#39;t just assume things worked. Check &lt;code&gt;$?&lt;/code&gt; or use &lt;code&gt;if&lt;/code&gt; statements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quote &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; when passing arguments:&lt;/strong&gt; This preserves spaces in filenames and arguments.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Avoid these mistakes:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;#39;t hard-code paths:&lt;/strong&gt; Never write &lt;code&gt;/home/john/.config&lt;/code&gt; directly. Use variables so your script works for everyone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;#39;t use &lt;code&gt;$*&lt;/code&gt; by default:&lt;/strong&gt; It merges all arguments into one string, which usually isn&amp;#39;t what you want. Stick with &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;#39;t assume &lt;code&gt;$?&lt;/code&gt; sticks around:&lt;/strong&gt; It changes with every command. Save it immediately if you need it later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;#39;t forget to quote path variables:&lt;/strong&gt; Unquoted variables break when paths have spaces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;#39;t use &lt;code&gt;$UID&lt;/code&gt; for permission checks:&lt;/strong&gt; Use &lt;code&gt;$EUID&lt;/code&gt; instead it&amp;#39;s what actually matters for access control.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html&quot;&gt;GNU Bash Manual - Special Parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://specifications.freedesktop.org/basedir/latest/&quot;&gt;XDG Base Directory Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tldp.org/LDP/abs/html/&quot;&gt;Advanced Bash-Scripting Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>shell-scripting</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0007-essential-bash-variables/hero.png" length="0" type="image/jpeg"/></item><item><title>Per-App Shell History for Bash</title><link>https://mkabumattar.com/codesnippets/post/bash-per-app-history/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/bash-per-app-history/</guid><description>Keep your Bash history organized across different terminal applications. This snippet automatically creates separate history files for each terminal emulator you use.</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Terminal Chaos? Organize Your Bash History!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ever jumped between iTerm2, Ghostty, and VS Code&amp;#39;s terminal only to have your command history get all mixed up? This Bash snippet keeps things clean by creating separate history files for each terminal app you use.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Mixed Command History&lt;/h3&gt;
&lt;p&gt;When you use multiple terminal emulators (iTerm, Alacritty, Ghostty, VS Code, etc.), they all share the same shell history file by default. This means commands you ran in one app show up when you press the up arrow in another. It gets messy fast, especially when you use different terminals for different projects or contexts.&lt;/p&gt;
&lt;h3&gt;Context Switching Issues&lt;/h3&gt;
&lt;p&gt;Different terminals used for different purposes (work, personal, testing) end up with mixed command histories, making it difficult to maintain context.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Per-App History Files&lt;/h3&gt;
&lt;p&gt;This Bash function detects which terminal application you&amp;#39;re using and automatically assigns a dedicated history file for it. Apple Terminal and Ghostty on Linux get the default history, while everything else gets its own isolated file. Clean, simple, and automatic.&lt;/p&gt;
&lt;h3&gt;Automatic Organization&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatically creates separate history files for each terminal app.&lt;/li&gt;
&lt;li&gt;Keeps Apple Terminal and Ghostty (Linux) on the default history.&lt;/li&gt;
&lt;li&gt;Works seamlessly with iTerm2, Alacritty, Warp, Kitty, and more.&lt;/li&gt;
&lt;li&gt;Just add it to your &lt;code&gt;~/.bashrc&lt;/code&gt; or &lt;code&gt;~/.bash_profile&lt;/code&gt; and forget about it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;h3&gt;Bash Function Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;function set_app_history() {

  # Get terminal app name (fallback: Apple_Terminal)
  local app=&amp;quot;${TERM_PROGRAM:-Apple_Terminal}&amp;quot;

  # Default history file
  HISTFILE=&amp;quot;$HOME/.bash/.bash_history&amp;quot;

  # Ghostty on Linux / WSL → use default history
  if [[ &amp;quot;$app&amp;quot; == &amp;quot;Ghostty&amp;quot; &amp;amp;&amp;amp; &amp;quot;$(uname -s)&amp;quot; == &amp;quot;Linux&amp;quot; ]]; then
    export HISTFILE
    return
  fi

  # Apple Terminal → use default history
  if [[ &amp;quot;$app&amp;quot; == &amp;quot;Apple_Terminal&amp;quot; ]]; then
    export HISTFILE
    return
  fi

  # Everything else → per-app history
  app=&amp;quot;${app//[^A-Za-z0-9]/_}&amp;quot;
  HISTFILE=&amp;quot;$HOME/.bash/.bash_history_${app}&amp;quot;
  export HISTFILE
}

set_app_history
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benefits and Usage&lt;/h2&gt;
&lt;h3&gt;Organization Advantages&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach keeps your shell history organized without any manual intervention. You can switch between terminals without worrying about command pollution or losing context. Plus, it&amp;#39;s especially useful if you use different terminals for work, personal projects, or testing each gets its own clean slate.&lt;/p&gt;
&lt;h3&gt;Managing History Files&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bonus Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Want to see which history files you have? Run this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ls -lh ~/.bash/.bash_history*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ll see all your per-app history files. Each one is tied to a specific terminal emulator, so you can even back them up or clean them individually.&lt;/p&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;h3&gt;Your History Management&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your Turn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How do you organize your shell history? Do you use per-app separation or something else? Drop your setup below!&lt;/p&gt;
&lt;h3&gt;Alternative Approaches&lt;/h3&gt;
&lt;p&gt;Share your favorite methods for keeping shell history organized across different terminal applications.&lt;/p&gt;
</content:encoded><category>shell-scripting</category><category>productivity</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0006-bash-per-app-history/hero.png" length="0" type="image/jpeg"/></item><item><title>Per-App Shell History for Zsh</title><link>https://mkabumattar.com/codesnippets/post/zsh-per-app-history/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/zsh-per-app-history/</guid><description>Keep your Zsh history organized across different terminal applications. This snippet automatically creates separate history files for each terminal emulator you use.</description><pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Terminal Chaos? Organize Your Shell History!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ever jumped between iTerm2, Ghostty, and VS Code&amp;#39;s terminal only to have your command history get all mixed up? This Zsh snippet keeps things clean by creating separate history files for each terminal app you use.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Mixed Command History&lt;/h3&gt;
&lt;p&gt;When you use multiple terminal emulators (iTerm, Alacritty, Ghostty, VS Code, etc.), they all share the same shell history file by default. This means commands you ran in one app show up when you press the up arrow in another. It gets messy fast, especially when you use different terminals for different projects or contexts.&lt;/p&gt;
&lt;h3&gt;Context Switching Issues&lt;/h3&gt;
&lt;p&gt;Different terminals used for different purposes (work, personal, testing) end up with mixed command histories, making it difficult to maintain context.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Per-App History Files&lt;/h3&gt;
&lt;p&gt;This Zsh function detects which terminal application you&amp;#39;re using and automatically assigns a dedicated history file for it. Apple Terminal and Ghostty on Linux get the default history, while everything else gets its own isolated file. Clean, simple, and automatic.&lt;/p&gt;
&lt;h3&gt;Automatic Organization&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatically creates separate history files for each terminal app.&lt;/li&gt;
&lt;li&gt;Keeps Apple Terminal and Ghostty (Linux) on the default history.&lt;/li&gt;
&lt;li&gt;Works seamlessly with iTerm2, Alacritty, Warp, Kitty, and more.&lt;/li&gt;
&lt;li&gt;Just add it to your &lt;code&gt;~/.zshrc&lt;/code&gt; and forget about it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;h3&gt;Zsh Function Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;function set_app_history() {

  # Get terminal app name (fallback: Apple_Terminal)
  local app=${TERM_PROGRAM:-Apple_Terminal}

  # Default history file
  HISTFILE=&amp;quot;$HOME/.zsh/.zhistory&amp;quot;

  # Ghostty on Linux / WSL → use default history
  if [[ &amp;quot;$app&amp;quot; == &amp;quot;Ghostty&amp;quot; &amp;amp;&amp;amp; &amp;quot;$(uname -s)&amp;quot; == &amp;quot;Linux&amp;quot; ]]; then
    export HISTFILE
    return
  fi

  # Apple Terminal → use default history
  if [[ &amp;quot;$app&amp;quot; == &amp;quot;Apple_Terminal&amp;quot; ]]; then
    export HISTFILE
    return
  fi

  # Everything else → per-app history
  app=${app//[^A-Za-z0-9]/_}
  HISTFILE=&amp;quot;$HOME/.zsh/.zhistory_${app}&amp;quot;
  export HISTFILE
}

set_app_history
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benefits and Usage&lt;/h2&gt;
&lt;h3&gt;Organization Advantages&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach keeps your shell history organized without any manual intervention. You can switch between terminals without worrying about command pollution or losing context. Plus, it&amp;#39;s especially useful if you use different terminals for work, personal projects, or testing each gets its own clean slate.&lt;/p&gt;
&lt;h3&gt;Managing History Files&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bonus Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Want to see which history files you have? Run this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ls -lh ~/.zsh/.zhistory*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ll see all your per-app history files. Each one is tied to a specific terminal emulator, so you can even back them up or clean them individually.&lt;/p&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;h3&gt;Your History Management&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your Turn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How do you organize your shell history? Do you use per-app separation or something else? Drop your setup below!&lt;/p&gt;
&lt;h3&gt;Alternative Approaches&lt;/h3&gt;
&lt;p&gt;Share your favorite methods for keeping shell history organized across different terminal applications.&lt;/p&gt;
</content:encoded><category>shell-scripting</category><category>productivity</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0005-zsh-per-app-history/hero.png" length="0" type="image/jpeg"/></item><item><title>Optimizing your python code with __slots__?</title><link>https://mkabumattar.com/codesnippets/post/python-slots-optimization/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/python-slots-optimization/</guid><description>Discover how Python `__slots__` can reduce memory usage by up to 40% in data-heavy applications. Perfect for MLOps pipelines and big data processing where millions of objects consume precious memory resources.</description><pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Memory Optimization with &lt;code&gt;__slots__&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;Understanding the Problem&lt;/h3&gt;
&lt;h3&gt;Dev Tip: Optimizing Data Models in Big Data Workflows with &lt;code&gt;__slots__&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In big data and MLOps workflows, you often work with massive datasets where you create millions of objects to represent data points, features, or model predictions. Traditional Python classes can consume a significant amount of memory due to the hidden &lt;code&gt;__dict__&lt;/code&gt; attribute. This overhead can lead to memory bottlenecks and slow down processing, especially when you&amp;#39;re working with large-scale data pipelines and machine learning models.&lt;/p&gt;
&lt;h3&gt;How &lt;code&gt;__slots__&lt;/code&gt; Works&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;__slots__&lt;/code&gt; attribute is a simple yet powerful way to optimize your code by drastically reducing the memory footprint of your objects. By defining &lt;code&gt;__slots__&lt;/code&gt;, you tell Python to allocate a fixed memory space for a class&amp;#39;s attributes, bypassing the need for a dynamic &lt;code&gt;__dict__&lt;/code&gt;. This makes your application more memory-efficient and can lead to faster attribute access, which is crucial for high-performance computing tasks in MLOps and big data.&lt;/p&gt;
&lt;h2&gt;Real-World Use Case: MLOps Data Pipeline&lt;/h2&gt;
&lt;h3&gt;Customer Churn Prediction Example&lt;/h3&gt;
&lt;h3&gt;Real-World Scenario: A Data Pipeline for MLOps&lt;/h3&gt;
&lt;p&gt;Imagine you&amp;#39;re building a data pipeline for a machine learning model that predicts customer churn. Your pipeline processes millions of customer records, and each record is represented by a Python object.&lt;/p&gt;
&lt;p&gt;Using a &lt;strong&gt;regular class&lt;/strong&gt;, each customer object would have a memory overhead from its &lt;code&gt;__dict__&lt;/code&gt;. When you process millions of these objects, the cumulative memory usage becomes immense, potentially crashing your application or requiring more expensive, higher-memory machines.&lt;/p&gt;
&lt;p&gt;By using a class with &lt;code&gt;__slots__&lt;/code&gt;, you can create a memory-efficient data model. This approach minimizes memory consumption, allowing you to process more data with the same resources and speeding up your pipeline. This is critical in MLOps where efficient resource utilization is key to managing costs and scaling operations.&lt;/p&gt;
&lt;h3&gt;Big Data Processing Benefits&lt;/h3&gt;
&lt;h3&gt;Real-World Scenario: MLOps Data Pipeline with &lt;code&gt;__slots__&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In an MLOps pipeline, data is often represented as objects for processing, feature engineering, and model training. When dealing with big data, memory efficiency is critical to avoid crashes and keep cloud computing costs low. Using &lt;code&gt;__slots__&lt;/code&gt; can drastically reduce the memory footprint of these data objects, making your pipeline more robust and scalable.&lt;/p&gt;
&lt;p&gt;The following full code snippet simulates a big data MLOps workflow where we process a large number of customer records. We will create two versions of a &lt;code&gt;Customer&lt;/code&gt; data class: one with the default Python behavior and one optimized with &lt;code&gt;__slots__&lt;/code&gt;. The code will then compare the memory usage and time taken to create a million instances of each, demonstrating the practical benefits of &lt;code&gt;__slots__&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Code Implementation&lt;/h2&gt;
&lt;h3&gt;Class Definitions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys
import time
import random
import pandas as pd
from typing import List

# --- Part 1: Data Model Definitions ---

class Customer:
    &amp;quot;&amp;quot;&amp;quot;
    A regular Python class to represent a customer record.
    This class uses a default __dict__ to store attributes.
    &amp;quot;&amp;quot;&amp;quot;
    def __init__(self, customer_id: int, age: int, monthly_spend: float, churned: bool):
        self.customer_id = customer_id
        self.age = age
        self.monthly_spend = monthly_spend
        self.churned = churned

class OptimizedCustomer:
    &amp;quot;&amp;quot;&amp;quot;
    An optimized customer class using __slots__ for memory efficiency.
    This class explicitly defines its attributes, eliminating the __dict__ overhead.
    &amp;quot;&amp;quot;&amp;quot;
    __slots__ = [&amp;#39;customer_id&amp;#39;, &amp;#39;age&amp;#39;, &amp;#39;monthly_spend&amp;#39;, &amp;#39;churned&amp;#39;]

    def __init__(self, customer_id: int, age: int, monthly_spend: float, churned: bool):
        self.customer_id = customer_id
        self.age = age
        self.monthly_spend = monthly_spend
        self.churned = churned
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Data Generation Functions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# --- Part 2: Data Generation and Object Creation ---

def generate_customer_data(num_records: int) -&amp;gt; List[tuple]:
    &amp;quot;&amp;quot;&amp;quot;Generates a list of tuples representing raw customer data.&amp;quot;&amp;quot;&amp;quot;
    data = []
    for i in range(num_records):
        customer_id = i
        age = random.randint(20, 70)
        monthly_spend = round(random.uniform(25.0, 500.0), 2)
        churned = random.choice([True, False])
        data.append((customer_id, age, monthly_spend, churned))
    return data

def create_objects(data: List[tuple], class_type: type) -&amp;gt; List:
    &amp;quot;&amp;quot;&amp;quot;Creates a list of objects from raw data using the specified class.&amp;quot;&amp;quot;&amp;quot;
    return [class_type(*record) for record in data]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Performance Testing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# --- Part 3: Performance Comparison ---

def run_performance_test(num_records: int):
    &amp;quot;&amp;quot;&amp;quot;
    Runs a performance test to compare memory and time for both classes.
    &amp;quot;&amp;quot;&amp;quot;
    print(f&amp;quot;--- Running performance test with {num_records:,} records ---&amp;quot;)
    raw_data = generate_customer_data(num_records)

    # Test the regular class
    start_time_regular = time.time()
    regular_customers = create_objects(raw_data, Customer)
    end_time_regular = time.time()
    memory_regular = sum(sys.getsizeof(c) for c in regular_customers) + sys.getsizeof(regular_customers)

    # Test the optimized class
    start_time_slotted = time.time()
    slotted_customers = create_objects(raw_data, OptimizedCustomer)
    end_time_slotted = time.time()
    memory_slotted = sum(sys.getsizeof(c) for c in slotted_customers) + sys.getsizeof(slotted_customers)

    # Print results
    print(&amp;quot;\nRegular Class Performance:&amp;quot;)
    print(f&amp;quot;  - Total Memory: {memory_regular / (1024**2):.2f} MB&amp;quot;)
    print(f&amp;quot;  - Time Taken: {end_time_regular - start_time_regular:.4f} seconds&amp;quot;)

    print(&amp;quot;\nSlotted Class Performance:&amp;quot;)
    print(f&amp;quot;  - Total Memory: {memory_slotted / (1024**2):.2f} MB&amp;quot;)
    print(f&amp;quot;  - Time Taken: {end_time_slotted - start_time_slotted:.4f} seconds&amp;quot;)

    # Calculate and print the savings
    memory_saved_mb = (memory_regular - memory_slotted) / (1024**2)
    time_saved_s = (end_time_regular - start_time_slotted)

    print(&amp;quot;\n--- Summary of Savings ---&amp;quot;)
    print(f&amp;quot;Memory Saved: {memory_saved_mb:.2f} MB ({ (memory_saved_mb / (memory_regular / (1024**2))) * 100:.2f}%)&amp;quot;)
    print(f&amp;quot;⏱Slotted Class Creation Time is faster by: {time_saved_s:.4f} seconds&amp;quot;)

    # Optional: Demonstrate a simple MLOps task like converting to a DataFrame
    print(&amp;quot;\n--- MLOps Task: Converting to a Pandas DataFrame ---&amp;quot;)
    # This task is just for demonstration and doesn&amp;#39;t show a direct slots benefit here
    df_regular = pd.DataFrame([c.__dict__ for c in regular_customers])
    df_slotted = pd.DataFrame([[c.customer_id, c.age, c.monthly_spend, c.churned] for c in slotted_customers],
                               columns=[&amp;#39;customer_id&amp;#39;, &amp;#39;age&amp;#39;, &amp;#39;monthly_spend&amp;#39;, &amp;#39;churned&amp;#39;])
    print(&amp;quot;Successfully converted both object lists to Pandas DataFrames.&amp;quot;)
    print(f&amp;quot;DataFrame head from Slotted Class:\n{df_slotted.head()}&amp;quot;)

# --- Part 4: Execution ---
if __name__ == &amp;quot;__main__&amp;quot;:
    NUM_RECORDS = 1_000_000 # 1 million records
    run_performance_test(NUM_RECORDS)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Results and Analysis&lt;/h2&gt;
&lt;h3&gt;Performance Comparison&lt;/h3&gt;
&lt;p&gt;This code snippet demonstrates how to optimize memory usage in Python by using &lt;code&gt;__slots__&lt;/code&gt; in a data-heavy application, specifically in an MLOps context. It compares the memory and performance of a regular class versus an optimized class with &lt;code&gt;__slots__&lt;/code&gt;, showing significant savings in both memory and processing time when handling large datasets.&lt;/p&gt;
&lt;h3&gt;Practical Benefits&lt;/h3&gt;
&lt;p&gt;The implementation shows measurable improvements in memory usage and object creation speed, making it ideal for big data processing and MLOps workflows.&lt;/p&gt;
</content:encoded><category>python</category><category>productivity</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0004-python-slots-optimization/hero.png" length="0" type="image/jpeg"/></item><item><title>List S3 Buckets</title><link>https://mkabumattar.com/codesnippets/post/python-list-s3-buckets/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/python-list-s3-buckets/</guid><description>Automate AWS S3 interactions. This Python snippet uses Boto3 to easily list all S3 buckets in your account.</description><pubDate>Wed, 25 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;h3&gt;Multi-Profile S3 Management&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Multi-Profile S3 Safari!&lt;/strong&gt; Ever juggled multiple AWS accounts and needed a quick S3 bucket inventory across all of them? This Python script is your guide!&lt;/p&gt;
&lt;h3&gt;Use Case&lt;/h3&gt;
&lt;p&gt;Perfect for organizations managing multiple AWS accounts or developers working with different IAM roles and profiles.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Multi-Profile Complexity&lt;/h3&gt;
&lt;p&gt;When you&amp;#39;re working with multiple AWS accounts or IAM roles through different profiles, getting a consolidated view of your S3 buckets can be a hassle. Switching contexts or running commands repeatedly for each profile is inefficient and prone to oversight, making comprehensive inventory or auditing a chore.&lt;/p&gt;
&lt;h3&gt;Manual Process Inefficiencies&lt;/h3&gt;
&lt;p&gt;Traditional approaches require manual switching between profiles and running separate commands for each account.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Automated Profile Discovery&lt;/h3&gt;
&lt;p&gt;This Python script, powered by Boto3, automates the discovery of all your configured AWS CLI profiles. It then iterates through each profile, attempting to list its S3 buckets. The script provides a clear, profile-by-profile breakdown and includes robust error handling for issues like missing credentials or access denied for specific profiles, ensuring it runs smoothly even in complex setups.&lt;/p&gt;
&lt;h3&gt;Comprehensive Error Handling&lt;/h3&gt;
&lt;p&gt;Gracefully handles various error conditions including missing credentials, access denied, and network issues.&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;h3&gt;Key Capabilities&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lists S3 buckets across ALL configured AWS CLI profiles in one go.&lt;/li&gt;
&lt;li&gt;Automatically discovers available AWS profiles.&lt;/li&gt;
&lt;li&gt;Provides a clear, per-profile output of S3 buckets.&lt;/li&gt;
&lt;li&gt;Gracefully handles errors for individual profiles (e.g., credential issues, access denied).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Output Format&lt;/h3&gt;
&lt;p&gt;Organized display showing buckets grouped by AWS profile with clear visual separation.&lt;/p&gt;
&lt;h2&gt;Code Implementation&lt;/h2&gt;
&lt;h3&gt;Dependencies and Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import boto3
from botocore.exceptions import NoCredentialsError, ClientError
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Profile Discovery Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_aws_profiles():
    &amp;quot;&amp;quot;&amp;quot;Get all configured AWS profiles from the AWS CLI configuration.&amp;quot;&amp;quot;&amp;quot;
    try:
        session = boto3.Session()
        return session.available_profiles
    except Exception as e:
        print(f&amp;quot;Error getting AWS profiles: {str(e)}&amp;quot;)
        return []
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bucket Listing Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def list_s3_buckets_for_profile(profile_name):
    &amp;quot;&amp;quot;&amp;quot;
    Lists all S3 buckets for a specific AWS profile.
    Returns a list of bucket names or an empty list if an error occurs.
    &amp;quot;&amp;quot;&amp;quot;
    buckets = []
    try:
        session = boto3.Session(profile_name=profile_name)
        s3_client = session.client(&amp;#39;s3&amp;#39;)
        response = s3_client.list_buckets()
        if response[&amp;#39;Buckets&amp;#39;]:
            for bucket in response[&amp;#39;Buckets&amp;#39;]:
                buckets.append(bucket[&amp;#39;Name&amp;#39;])
    except NoCredentialsError:
        print(f&amp;quot;  Warning: No credentials found for profile &amp;#39;{profile_name}&amp;#39;. Skipping.&amp;quot;)
    except ClientError as e:
        error_code = e.response.get(&amp;quot;Error&amp;quot;, {}).get(&amp;quot;Code&amp;quot;)
        error_message = e.response.get(&amp;quot;Error&amp;quot;, {}).get(&amp;quot;Message&amp;quot;)
        print(f&amp;quot;  Warning: AWS Client Error for profile &amp;#39;{profile_name}&amp;#39; ({error_code}): {error_message}. Skipping.&amp;quot;)
    except Exception as e:
        print(f&amp;quot;  Warning: An unexpected error occurred for profile &amp;#39;{profile_name}&amp;#39;: {e}. Skipping.&amp;quot;)
    return buckets
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Main Display Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def display_all_s3_buckets_by_profile():
    &amp;quot;&amp;quot;&amp;quot;
    Fetches and displays S3 buckets for all configured AWS profiles.
    &amp;quot;&amp;quot;&amp;quot;
    profiles = get_aws_profiles()

    if not profiles:
        print(&amp;quot;No AWS profiles found. Please configure your AWS CLI.&amp;quot;)
        return

    print(f&amp;quot;\nChecking S3 Buckets across {len(profiles)} AWS Profiles:&amp;quot;)
    print(&amp;quot;-&amp;quot; * 40)

    for profile in sorted(profiles):
        print(f&amp;quot;\nProfile: {profile}&amp;quot;)
        print(&amp;quot;  S3 Buckets:&amp;quot;)
        buckets = list_s3_buckets_for_profile(profile)
        if buckets:
            for bucket_name in buckets:
                print(f&amp;quot;    - {bucket_name}&amp;quot;)
        else:
            print(&amp;quot;    No buckets or inaccessible for this profile.&amp;quot;)
        print(&amp;quot;-&amp;quot; * 40)

    if not any(list_s3_buckets_for_profile(p) for p in profiles):
        print(&amp;quot;\nNo S3 buckets found across any configured profiles or all were inaccessible.&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    display_all_s3_buckets_by_profile()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benefits and Usage&lt;/h2&gt;
&lt;h3&gt;Operational Advantages&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This script is a game-changer for anyone managing multiple AWS environments. It drastically simplifies S3 bucket auditing, inventory tasks, and compliance checks across your entire AWS footprint. Get a unified view without the manual grind, making your cloud operations more efficient and less error-prone.&lt;/p&gt;
&lt;h3&gt;Use Cases&lt;/h3&gt;
&lt;p&gt;Ideal for inventory management, compliance auditing, and multi-account AWS operations.&lt;/p&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;h3&gt;Your S3 Management Approaches&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your Turn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How do you manage S3 bucket information in your Python projects? Any favorite Boto3 tricks for S3 you&amp;#39;d like to share?&lt;/p&gt;
&lt;h3&gt;Alternative Tools&lt;/h3&gt;
&lt;p&gt;Share your preferred methods for managing S3 resources across multiple AWS accounts.&lt;/p&gt;
</content:encoded><category>aws</category><category>python</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0003-python-list-s3-buckets/hero.png" length="0" type="image/jpeg"/></item><item><title>AWS Secrets Manager</title><link>https://mkabumattar.com/codesnippets/post/nodejs-aws-secrets-manager/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/nodejs-aws-secrets-manager/</guid><description>Access secrets securely in your Node.js apps. This snippet demonstrates fetching sensitive data from AWS Secrets Manager.</description><pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Need to load secrets in your Node.js app without exposing them? Here&amp;#39;s how.&lt;/h3&gt;
&lt;p&gt;If you&amp;#39;re still storing API keys or database credentials in &lt;code&gt;.env&lt;/code&gt; files or hardcoding them into your codebase, it&amp;#39;s time for a better approach. Secrets should stay secret especially in production.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Managing Sensitive Values&lt;/h3&gt;
&lt;p&gt;Managing sensitive values across environments can get messy fast. Hardcoded secrets are risky, and &lt;code&gt;.env&lt;/code&gt; files aren&amp;#39;t ideal when you&amp;#39;re working with teams or deploying to the cloud. It&amp;#39;s easy to lose control over where that information ends up.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;AWS Secrets Manager Overview&lt;/h3&gt;
&lt;p&gt;AWS Secrets Manager gives you a safe, centralized place to store secrets. You can fetch them at runtime using the AWS SDK, so you don’t need to keep secrets on disk or in code. This snippet shows how to pull them securely in a Node.js app using SDK v3.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use AWS Secrets Manager to securely access secrets in Node.js.&lt;/li&gt;
&lt;li&gt;Avoid hardcoding secrets or using local &lt;code&gt;.env&lt;/code&gt; files in production.&lt;/li&gt;
&lt;li&gt;This snippet helps fetch secrets using the AWS SDK v3.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Code Snippet (TypeScript):&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  SecretsManagerClient,
  GetSecretValueCommand,
  ResourceNotFoundException,
  SecretsManagerServiceException,
} from &amp;#39;@aws-sdk/client-secrets-manager&amp;#39;;
import {config} from &amp;#39;dotenv&amp;#39;;

config();

interface CachedSecret {
  value: string;
  expiry: number;
}

const secretCache = new Map&amp;lt;string, CachedSecret&amp;gt;();

const DEFAULT_CACHE_TTL = 5 * 60 * 1000;

const defaultRegion = process.env.AWS_REGION;

/**
 * Custom error for when a secret is not found in AWS Secrets Manager.
 * This is thrown when the secret does not exist or cannot be accessed.
 */
class SecretNotFoundError extends Error {
  constructor(secretName: string) {
    super(`Secret &amp;quot;${secretName}&amp;quot; not found in AWS Secrets Manager.`);
    this.name = &amp;#39;SecretNotFoundError&amp;#39;;
  }
}

/**
 * Custom error for when a secret&amp;#39;s value is invalid.
 * This can happen if the secret is binary, empty, or not a string.
 */
class InvalidSecretValueError extends Error {
  constructor(secretName: string) {
    super(
      `Secret &amp;quot;${secretName}&amp;quot; is binary or empty, or does not contain a string value.`,
    );
    this.name = &amp;#39;InvalidSecretValueError&amp;#39;;
  }
}

let secretsManagerClient: SecretsManagerClient | null = null;

/**
 * Initializes and returns a singleton SecretsManagerClient.
 * This prevents recreating the client on every `fetchSecret` call.
 * @param region - The AWS region to use for the client.
 * @returns An initialized SecretsManagerClient instance.
 */
function getSecretsManagerClient(region: string): SecretsManagerClient {
  if (!region) {
    throw new Error(
      &amp;#39;AWS_REGION is not defined. Please set it in your .env file or pass it as an argument.&amp;#39;,
    );
  }
  if (!secretsManagerClient) {
    secretsManagerClient = new SecretsManagerClient({region});
  }
  return secretsManagerClient;
}

/**
 * Fetches a secret&amp;#39;s string value from AWS Secrets Manager with optional caching.
 * @param secretName - The name or ARN of the secret.
 * @param options - Optional configuration for fetching the secret.
 * @param options.region - Overrides the default AWS region for this fetch operation.
 * @param options.cacheTTL - Time-to-live for the cached secret in milliseconds. Set to 0 to disable caching for this call.
 * @returns A promise that resolves to the secret string.
 * @throws {SecretNotFoundError} If the secret does not exist.
 * @throws {InvalidSecretValueError} If the secret&amp;#39;s value is binary or empty.
 * @throws {Error} For other AWS SDK or network-related errors.
 */
export async function fetchSecret(
  secretName: string,
  options?: {region?: string; cacheTTL?: number},
): Promise&amp;lt;string&amp;gt; {
  const region = options?.region || defaultRegion;
  const cacheTTL =
    options?.cacheTTL !== undefined ? options.cacheTTL : DEFAULT_CACHE_TTL;

  if (!region) {
    throw new Error(
      &amp;quot;AWS_REGION is not defined. Ensure it&amp;#39;s in your .env file or passed in options.&amp;quot;,
    );
  }

  if (cacheTTL &amp;gt; 0) {
    const cached = secretCache.get(secretName);
    if (cached &amp;amp;&amp;amp; Date.now() &amp;lt; cached.expiry) {
      return cached.value;
    }
  }

  const client = getSecretsManagerClient(region);

  try {
    const command = new GetSecretValueCommand({SecretId: secretName});
    const response = await client.send(command);

    if (response.SecretString) {
      const secretValue = response.SecretString;
      if (cacheTTL &amp;gt; 0) {
        secretCache.set(secretName, {
          value: secretValue,
          expiry: Date.now() + cacheTTL,
        });
      }
      return secretValue;
    } else if (response.SecretBinary) {
      throw new InvalidSecretValueError(secretName);
    } else {
      throw new InvalidSecretValueError(secretName);
    }
  } catch (error: any) {
    console.error(`[ERROR] Failed to fetch secret &amp;quot;${secretName}&amp;quot;:`, error);
    if (error instanceof ResourceNotFoundException) {
      throw new SecretNotFoundError(secretName);
    } else if (error instanceof SecretsManagerServiceException) {
      throw new Error(
        `AWS Secrets Manager error for &amp;quot;${secretName}&amp;quot;: ${error.message} (Code: ${error.name})`,
      );
    } else {
      throw new Error(
        `An unexpected error occurred while fetching secret &amp;quot;${secretName}&amp;quot;: ${error.message}`,
      );
    }
  } finally {
    // Add any cleanup or finalization logic here.
    // For example, if you had an active connection or resource to close.
    // In this specific code, there isn&amp;#39;t an obvious resource to clean up
    // within the fetchSecret function itself, as the client is a singleton
    // and caching is handled by a Map.
    // However, for demonstration, you could log something:
    console.log(`[INFO] Finished attempting to fetch secret &amp;quot;${secretName}&amp;quot;.`);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;This setup helps keep your apps more secure and your secrets off disk. It fits perfectly into cloud-native workflows, especially when you’re using IAM roles or CI/CD pipelines. It’s simple, flexible, and secure.&lt;/p&gt;
&lt;h3&gt;Over to you&lt;/h3&gt;
&lt;p&gt;How do you handle secrets in your projects? Tried this approach before?&lt;/p&gt;
</content:encoded><category>aws</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0002-nodejs-aws-secrets-manager/hero.png" length="0" type="image/jpeg"/></item><item><title>Check S3 Bucket Existence</title><link>https://mkabumattar.com/codesnippets/post/bash-s3-bucket-exists/</link><guid isPermaLink="true">https://mkabumattar.com/codesnippets/post/bash-s3-bucket-exists/</guid><description>Validate AWS S3 bucket presence in your scripts. This Bash snippet efficiently checks if a bucket exists before proceeding with operations.</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don’t let your deployment blow up because of a missing S3 bucket. This Bash script lets you check if a bucket exists &lt;em&gt;before&lt;/em&gt; anything fails clean and simple.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;Missing Bucket Failures&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If your CI/CD pipeline tries to upload files or sync data to an S3 bucket that doesn&amp;#39;t exist, things will break fast. You&amp;#39;ll waste time, pipeline minutes, and possibly miss something obvious. A quick pre-check could prevent all that.&lt;/p&gt;
&lt;h3&gt;Impact on Deployments&lt;/h3&gt;
&lt;p&gt;Failed deployments due to missing S3 buckets can waste valuable CI/CD time and delay releases.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Bash Script Overview&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This script checks if an S3 bucket exists using the AWS CLI. It accepts &lt;code&gt;--bucket&lt;/code&gt; or &lt;code&gt;--b&lt;/code&gt; flags, gives color-coded logs, and adds a bit of fun to your terminal. Perfect for pre-checks in CI/CD jobs or local scripts.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use this script to check if an S3 bucket exists before your pipeline tries to use it.&lt;/li&gt;
&lt;li&gt;Accepts &lt;code&gt;--bucket&lt;/code&gt; or &lt;code&gt;--b&lt;/code&gt; to pass the bucket name.&lt;/li&gt;
&lt;li&gt;Includes clean logs, color output, and fun formatting for better readability.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Script Implementation&lt;/h2&gt;
&lt;h3&gt;Color Definitions and Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash

# Color definitions
GREEN=&amp;#39;\033[0;32m&amp;#39;
RED=&amp;#39;\033[0;31m&amp;#39;
YELLOW=&amp;#39;\033[1;33m&amp;#39;
CYAN=&amp;#39;\033[0;36m&amp;#39;
NC=&amp;#39;\033[0m&amp;#39; # No Color
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ASCII Banner Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#######################################
# Prints a fun ASCII banner header.
# Globals:
#   CYAN
#   NC
# Arguments:
#   None
# Outputs:
#   Writes header text to stdout.
#######################################
function print_fun() {
  echo -e &amp;quot;${CYAN}&amp;quot;
  echo &amp;quot;╭──────────────────────────────╮&amp;quot;
  echo &amp;quot;│   S3 Bucket Checker Bash  │&amp;quot;
  echo &amp;quot;╰──────────────────────────────╯&amp;quot;
  echo -e &amp;quot;${NC}&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Logging Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#######################################
# Prints a log message with timestamp and log level.
# Globals:
#   CYAN, GREEN, RED, NC
# Arguments:
#   $1 - Log level (INFO, SUCCESS, ERROR)
#   $2 - Log message
# Outputs:
#   Writes formatted log message to stdout.
#######################################
function log() {
  local type=&amp;quot;$1&amp;quot;
  local message=&amp;quot;$2&amp;quot;
  local timestamp
  timestamp=$(date &amp;quot;+%Y-%m-%d %H:%M:%S&amp;quot;)

  case &amp;quot;$type&amp;quot; in
    INFO)
      echo -e &amp;quot;${CYAN}[${timestamp}] [INFO]:${NC} $message&amp;quot;
      ;;
    SUCCESS)
      echo -e &amp;quot;${GREEN}[${timestamp}] [SUCCESS]:${NC} $message&amp;quot;
      ;;
    ERROR)
      echo -e &amp;quot;${RED}[${timestamp}] [ERROR]:${NC} $message&amp;quot;
      ;;
    *)
      echo -e &amp;quot;[${timestamp}] [LOG]: $message&amp;quot;
      ;;
  esac
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Initialization and Argument Parsing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#######################################
# Initializes script by parsing CLI arguments.
# Exits with error if bucket name is not provided.
# Globals:
#   BUCKET_NAME, YELLOW, NC
# Arguments:
#   --bucket | --b &amp;lt;bucket-name&amp;gt;
# Outputs:
#   Sets BUCKET_NAME variable.
#######################################
function init() {
  while [[ &amp;quot;$#&amp;quot; -gt 0 ]]; do
    case &amp;quot;$1&amp;quot; in
      --bucket|--b)
        BUCKET_NAME=&amp;quot;$2&amp;quot;
        shift 2
        ;;
      *)
        log ERROR &amp;quot;Unknown argument: $1&amp;quot;
        exit 1
        ;;
    esac
  done

  if [[ -z &amp;quot;$BUCKET_NAME&amp;quot; ]]; then
    log ERROR &amp;quot;Bucket name not provided. Use --bucket or --b to specify it.&amp;quot;
    echo -e &amp;quot;${YELLOW}Example:${NC} ./check_bucket.sh --bucket my-bucket-name&amp;quot;
    exit 1
  fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bucket Existence Check&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#######################################
# Checks if the specified S3 bucket exists.
# Globals:
#   BUCKET_NAME
# Outputs:
#   Success or error message to stdout.
# Returns:
#   0 if bucket exists, exits with 1 otherwise.
#######################################
function check_bucket_exists() {
  log INFO &amp;quot;Checking if bucket &amp;#39;$BUCKET_NAME&amp;#39; exists...&amp;quot;

  if aws s3api head-bucket --bucket &amp;quot;$BUCKET_NAME&amp;quot; 2&amp;gt;/dev/null; then
    log SUCCESS &amp;quot;Bucket &amp;#39;$BUCKET_NAME&amp;#39; exists and is accessible.&amp;quot;
  else
    log ERROR &amp;quot;Bucket &amp;#39;$BUCKET_NAME&amp;#39; does not exist or access is denied.&amp;quot;
    exit 1
  fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Main Function&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#######################################
# Main function that runs the script.
# Calls init and check_bucket_exists in order.
# Arguments:
#   Passes all script arguments to init.
#######################################
function main() {
  print_fun
  init &amp;quot;$@&amp;quot;
  check_bucket_exists
}

main &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage and Benefits&lt;/h2&gt;
&lt;h3&gt;Integration with CI/CD&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Adding this check to your CI/CD steps helps avoid silly failures and gives you faster feedback if something&amp;#39;s missing or misconfigured. It&amp;#39;s quick, readable, and reusable and you&amp;#39;ll thank yourself later.&lt;/p&gt;
&lt;h3&gt;Command Line Usage&lt;/h3&gt;
&lt;p&gt;Run the script with: &lt;code&gt;./check_bucket.sh --bucket my-bucket-name&lt;/code&gt; or &lt;code&gt;./check_bucket.sh --b my-bucket-name&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;h3&gt;Your Approaches&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your Turn&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How do you handle S3 checks in your automation? Got a trick or tool you use often? Let&amp;#39;s swap ideas.&lt;/p&gt;
&lt;h3&gt;Alternative Solutions&lt;/h3&gt;
&lt;p&gt;Share your favorite methods for validating S3 bucket existence in different environments.&lt;/p&gt;
</content:encoded><category>aws</category><category>shell-scripting</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/codesnippets/0001-bash-s3-bucket-exists/hero.png" length="0" type="image/jpeg"/></item></channel></rss>