<?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://codesnippets.mkabumattar.com/</link><item><title>Bash Script Locking: Prevent Concurrent Runs with a PID File</title><link>https://codesnippets.mkabumattar.com/post/bash-script-locking-pid-file/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/bash-script-locking-pid-file/</guid><description>A Bash snippet that uses a PID file so only one instance of a script runs at a time. Essential for cron jobs and automation scripts that must not overlap. Includes stale lock detection and cleanup on exit.</description><pubDate>Tue, 18 Aug 2026 13:48:21 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wrap any script that must not run twice at once in a PID-file lock, and a second copy simply exits instead of corrupting your data.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;The Problem&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Cron does not care whether your last run finished. If a job is scheduled every five minutes but one run takes seven, cron cheerfully starts a second copy while the first is still going. Now two processes are reading the same input, writing the same output file, and racing each other to a half-written mess.&lt;/p&gt;
&lt;p&gt;The same thing happens with a webhook that fires twice, a retry that overlaps the original, or an impatient human running the script by hand while the scheduled run is live.&lt;/p&gt;
&lt;h3&gt;Real-world impact&lt;/h3&gt;
&lt;p&gt;Overlapping runs are the kind of bug that only shows up under load, which is exactly when you can least afford it. You get truncated files, doubled database rows, and log output interleaved from two processes so you cannot even tell what happened. The flow below shows what a single guarded run should do instead.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/codesnippets/0018-bash-script-locking-pid-file/lock-acquisition-flow.drawio&quot;
  title=&quot;The lock-acquisition decision flow&quot;
  caption=&quot;On startup the script checks for a PID file. If one exists and its process is alive, it exits. If the process is gone, the lock is stale and gets cleared. Either way it then writes its own PID and registers a trap to remove the file on exit.&quot;
  height={720}
/&gt;&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;The Fix&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Before doing any work, the script writes its own process ID into a lock file. If that file already exists, it checks whether the process it names is still alive. If it is, this run backs off and exits. If the process is gone, the lock is stale (a previous run crashed) so it clears it and carries on. A &lt;code&gt;trap&lt;/code&gt; removes the lock file on the way out, whether the script finished cleanly or died.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One PID file per script decides who gets to run.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill -0 $pid&lt;/code&gt; tells you if the previous owner is still alive without actually signalling it.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;trap ... EXIT&lt;/code&gt; guarantees the lock is released even on error.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When two runs overlap, the second one loses cleanly:&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/codesnippets/0018-bash-script-locking-pid-file/concurrent-runs-sequence.drawio&quot;
  title=&quot;Two overlapping runs contending for the lock&quot;
  caption=&quot;Cron fires the script twice. Run A creates the PID file and works. Run B sees the file, confirms A&apos;s process is still alive, and exits cleanly. When A finishes, its trap removes the lock so the next run can proceed.&quot;
  height={520}
/&gt;&lt;/p&gt;
&lt;h2&gt;Script Implementation&lt;/h2&gt;
&lt;h3&gt;Setup&lt;/h3&gt;
&lt;p&gt;Start strict, and decide where the lock lives. Naming it after the script keeps different jobs from stepping on each other&amp;#39;s locks.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/usr/bin/env bash
set -euo pipefail

# One lock per script name, in a writable runtime dir.
LOCK_FILE=&amp;quot;${TMPDIR:-/tmp}/$(basename &amp;quot;$0&amp;quot;).pid&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Acquiring the lock&lt;/h3&gt;
&lt;p&gt;This is the core. If a lock file exists and names a live process, exit. If it exists but the process is gone, treat it as stale and reclaim it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;acquire_lock() {
  if [[ -e &amp;quot;$LOCK_FILE&amp;quot; ]]; then
    local old_pid
    old_pid=$(cat &amp;quot;$LOCK_FILE&amp;quot; 2&amp;gt;/dev/null || echo &amp;quot;&amp;quot;)

    # kill -0 checks the process exists without sending a real signal.
    if [[ -n &amp;quot;$old_pid&amp;quot; ]] &amp;amp;&amp;amp; kill -0 &amp;quot;$old_pid&amp;quot; 2&amp;gt;/dev/null; then
      echo &amp;quot;Already running as PID $old_pid, exiting.&amp;quot; &amp;gt;&amp;amp;2
      exit 1
    fi

    # The owner is gone: stale lock from a crashed run. Reclaim it.
    echo &amp;quot;Clearing stale lock from PID ${old_pid:-unknown}.&amp;quot; &amp;gt;&amp;amp;2
  fi

  echo $$ &amp;gt; &amp;quot;$LOCK_FILE&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Releasing on exit&lt;/h3&gt;
&lt;p&gt;Register the cleanup once, right after acquiring, so it runs on normal exit, on error (thanks to &lt;code&gt;set -e&lt;/code&gt;), and on Ctrl-C.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;release_lock() {
  rm -f &amp;quot;$LOCK_FILE&amp;quot;
}

acquire_lock
trap release_lock EXIT
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Entry point&lt;/h3&gt;
&lt;p&gt;Everything after the trap is your real work. It runs knowing no other copy is active.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;main() {
  echo &amp;quot;Running with lock held (PID $$)...&amp;quot;
  # ... the actual job goes here ...
  sleep 5
  echo &amp;quot;Done.&amp;quot;
}

main &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Usage and Benefits&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;Why This Helps&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The whole guard is about fifteen lines and needs nothing outside of Bash itself. Drop it at the top of any script and overlapping runs stop being a problem: the first run works, the rest exit immediately, and a crash never leaves a lock that blocks every future run.&lt;/p&gt;
&lt;h3&gt;Running it&lt;/h3&gt;
&lt;p&gt;Prove it to yourself by starting one in the background and immediately launching a second:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;./with_lock.sh &amp;amp;   # first run grabs the lock
./with_lock.sh     # second run prints &amp;quot;Already running...&amp;quot; and exits 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first run holds the lock for its full duration; the second sees a live PID and steps aside. Kill the first mid-run and the next invocation clears the stale lock instead of getting stuck.&lt;/p&gt;
&lt;h2&gt;Community Discussion&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;Your Turn&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;How do you keep your cron jobs from overlapping? PID files, lock directories, or something your scheduler does for you? I would like to hear what has held up in production.&lt;/p&gt;
&lt;h3&gt;Alternative approaches&lt;/h3&gt;
&lt;p&gt;If you are on Linux, &lt;code&gt;flock&lt;/code&gt; is worth a look. It locks a file descriptor at the kernel level, so there is no stale-PID logic to write yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;exec 9&amp;gt;&amp;quot;${TMPDIR:-/tmp}/$(basename &amp;quot;$0&amp;quot;).lock&amp;quot;
flock -n 9 || { echo &amp;quot;Already running, exiting.&amp;quot; &amp;gt;&amp;amp;2; exit 1; }
# lock is released automatically when fd 9 closes on exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The PID-file version is more portable (it works the same on macOS and older shells) and it records who holds the lock, which is handy for debugging. &lt;code&gt;flock&lt;/code&gt; is simpler and race-free where you have it. Pick whichever matches where your scripts run.&lt;/p&gt;
</content:encoded><category>bash</category><category>automation</category><category>devops</category><author>Mohammad Abu Mattar</author><enclosure url="https://codesnippets.mkabumattar.com/assets/codesnippets/0018-bash-script-locking-pid-file/hero.png" length="0" type="image/jpeg"/></item><item><title>AWS DynamoDB CRUD Operations in Node.js with the AWS SDK v3</title><link>https://codesnippets.mkabumattar.com/post/nodejs-dynamodb-crud-aws-sdk-v3/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/nodejs-dynamodb-crud-aws-sdk-v3/</guid><description>A practical Node.js snippet covering DynamoDB put, get, update, delete, and query operations using the modern AWS SDK v3. Includes the DocumentClient pattern, single-table design basics, and error handling.</description><pubDate>Wed, 05 Aug 2026 15:18:55 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wrap the low-level DynamoDB client in a &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt; so you pass plain JavaScript objects in and get plain objects back, then guard your writes with &lt;code&gt;ConditionExpression&lt;/code&gt; and read by key with &lt;code&gt;QueryCommand&lt;/code&gt;.&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;The low-level DynamoDB API doesn&amp;#39;t speak JavaScript. Every value has to be wrapped in a typed attribute map, so a simple &lt;code&gt;{ id: &amp;quot;42&amp;quot;, count: 3 }&lt;/code&gt; turns into &lt;code&gt;{ id: { S: &amp;quot;42&amp;quot; }, count: { N: &amp;quot;3&amp;quot; } }&lt;/code&gt; on the way in, and you have to unwrap all of it on the way out. Write that marshalling by hand across a codebase and it becomes a steady drip of &lt;code&gt;ValidationException&lt;/code&gt; errors and off-by-one type bugs.&lt;/p&gt;
&lt;h3&gt;Why raw attribute maps hurt&lt;/h3&gt;
&lt;p&gt;The attribute-map format leaks into every call site. You can&amp;#39;t just hand DynamoDB the object your app already has, you first translate it, then translate the response back, and you repeat that boilerplate for put, get, update, and query. It&amp;#39;s easy to send a number as a string or forget a nested map, and DynamoDB rejects the whole request rather than coercing anything for you.&lt;/p&gt;
&lt;h3&gt;The real-world impact&lt;/h3&gt;
&lt;p&gt;That friction shows up as slow feature work and brittle data access. New team members trip over the &lt;code&gt;{ S: ... }&lt;/code&gt; wrappers, updates accidentally overwrite fields they never meant to touch, and someone reaches for &lt;code&gt;Scan&lt;/code&gt; because it &amp;quot;just works,&amp;quot; quietly reading the entire table on every call. The result is code that&amp;#39;s harder to review and a bill that grows faster than the data.&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;Use the AWS SDK v3 &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt;. It wraps the base &lt;code&gt;DynamoDBClient&lt;/code&gt; and handles marshalling in both directions, so your CRUD code deals in plain objects. Pair it with a single table, address items by their partition and sort keys, use &lt;code&gt;UpdateCommand&lt;/code&gt; to change only the fields you name, and add a &lt;code&gt;ConditionExpression&lt;/code&gt; when a write must not clobber existing data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wrap &lt;code&gt;DynamoDBClient&lt;/code&gt; in &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt; so you never touch raw attribute maps.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;PutCommand&lt;/code&gt;, &lt;code&gt;GetCommand&lt;/code&gt;, &lt;code&gt;UpdateCommand&lt;/code&gt;, and &lt;code&gt;DeleteCommand&lt;/code&gt; for CRUD, with &lt;code&gt;ConditionExpression&lt;/code&gt; to keep writes safe.&lt;/li&gt;
&lt;li&gt;Read with &lt;code&gt;QueryCommand&lt;/code&gt; on the partition key instead of scanning the whole table.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Script Implementation&lt;/h2&gt;
&lt;h3&gt;Setup and the DocumentClient&lt;/h3&gt;
&lt;p&gt;Create the base client once, wrap it in a &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt;, and turn on the marshalling options that smooth over DynamoDB&amp;#39;s quirks. Reuse this single instance across your app so connections get pooled.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// dynamo.ts - CRUD helpers for a single DynamoDB table with the AWS SDK v3.
import {DynamoDBClient} from &amp;#39;@aws-sdk/client-dynamodb&amp;#39;;
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
  UpdateCommand,
  DeleteCommand,
  QueryCommand,
} from &amp;#39;@aws-sdk/lib-dynamodb&amp;#39;;

const TABLE_NAME = process.env.TABLE_NAME ?? &amp;#39;AppTable&amp;#39;;

// One low-level client for the process; region comes from the environment.
const baseClient = new DynamoDBClient({});

// The DocumentClient marshals plain JS objects to/from attribute maps.
export const ddb = DynamoDBDocumentClient.from(baseClient, {
  marshallOptions: {
    // Drop undefined fields instead of erroring on them.
    removeUndefinedValues: true,
    // Store empty strings and sets as-is rather than converting to null.
    convertEmptyValues: false,
  },
});

// A tiny item shape for the examples: single-table keys plus data.
export interface User {
  pk: string; // partition key, e.g. &amp;quot;USER#42&amp;quot;
  sk: string; // sort key, e.g. &amp;quot;PROFILE&amp;quot;
  name: string;
  email: string;
  loginCount?: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create and read: put and get&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PutCommand&lt;/code&gt; writes a whole item, overwriting any existing item with the same key. &lt;code&gt;GetCommand&lt;/code&gt; fetches one item by its full primary key and returns a plain object, or &lt;code&gt;undefined&lt;/code&gt; when nothing matches.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Create or overwrite an item. Pass the object as-is, no attribute maps.
export async function putUser(user: User): Promise&amp;lt;void&amp;gt; {
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: user,
    }),
  );
}

// Fetch a single item by its partition + sort key.
export async function getUser(
  pk: string,
  sk: string,
): Promise&amp;lt;User | undefined&amp;gt; {
  const {Item} = await ddb.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: {pk, sk},
    }),
  );
  // Item is already a plain object, cast it to the known shape.
  return Item as User | undefined;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Update and delete safely&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;UpdateCommand&lt;/code&gt; changes only the fields you name in the &lt;code&gt;UpdateExpression&lt;/code&gt;, so you never overwrite the rest of the item. The &lt;code&gt;ConditionExpression&lt;/code&gt; makes the update fail loudly if the item doesn&amp;#39;t already exist, which stops accidental &amp;quot;upserts.&amp;quot; &lt;code&gt;DeleteCommand&lt;/code&gt; removes an item by key.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Update named fields only, and require that the item already exists.
export async function bumpLoginCount(pk: string, sk: string): Promise&amp;lt;User&amp;gt; {
  const {Attributes} = await ddb.send(
    new UpdateCommand({
      TableName: TABLE_NAME,
      Key: {pk, sk},
      // ADD creates loginCount at 1 if missing, otherwise increments it.
      UpdateExpression: &amp;#39;ADD loginCount :one&amp;#39;,
      ConditionExpression: &amp;#39;attribute_exists(pk)&amp;#39;,
      ExpressionAttributeValues: {&amp;#39;:one&amp;#39;: 1},
      ReturnValues: &amp;#39;ALL_NEW&amp;#39;,
    }),
  );
  return Attributes as User;
}

// Delete an item by key. Idempotent: deleting a missing key is a no-op.
export async function deleteUser(pk: string, sk: string): Promise&amp;lt;void&amp;gt; {
  await ddb.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: {pk, sk},
    }),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query by key instead of scanning&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;QueryCommand&lt;/code&gt; reads every item that shares a partition key, optionally narrowed by the sort key. This is the read you want almost every time, it touches only the matching items instead of the whole table the way &lt;code&gt;Scan&lt;/code&gt; does.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Fetch every item under one partition key, e.g. all records for a user.
export async function queryByUser(pk: string): Promise&amp;lt;User[]&amp;gt; {
  const items: User[] = [];
  let ExclusiveStartKey: Record&amp;lt;string, unknown&amp;gt; | undefined;

  // Loop to follow pagination until DynamoDB stops returning a cursor.
  do {
    const page = await ddb.send(
      new QueryCommand({
        TableName: TABLE_NAME,
        KeyConditionExpression: &amp;#39;pk = :pk&amp;#39;,
        ExpressionAttributeValues: {&amp;#39;:pk&amp;#39;: pk},
        ExclusiveStartKey,
      }),
    );
    items.push(...((page.Items ?? []) as User[]));
    ExclusiveStartKey = page.LastEvaluatedKey;
  } while (ExclusiveStartKey);

  return items;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Entry point with error handling&lt;/h3&gt;
&lt;p&gt;Tie the helpers together in a small demo and catch the one error you actually expect: a failed &lt;code&gt;ConditionExpression&lt;/code&gt;. DynamoDB reports it as a &lt;code&gt;ConditionalCheckFailedException&lt;/code&gt;, which you handle rather than crash on.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {ConditionalCheckFailedException} from &amp;#39;@aws-sdk/client-dynamodb&amp;#39;;

async function main(): Promise&amp;lt;void&amp;gt; {
  const key = {pk: &amp;#39;USER#42&amp;#39;, sk: &amp;#39;PROFILE&amp;#39;};

  await putUser({...key, name: &amp;#39;Ada&amp;#39;, email: &amp;#39;ada@example.com&amp;#39;});
  console.log(&amp;#39;created:&amp;#39;, await getUser(key.pk, key.sk));

  try {
    const updated = await bumpLoginCount(key.pk, key.sk);
    console.log(&amp;#39;login count is now&amp;#39;, updated.loginCount);
  } catch (err) {
    if (err instanceof ConditionalCheckFailedException) {
      console.error(&amp;#39;update skipped: that user does not exist yet&amp;#39;);
    } else {
      throw err; // Anything else is unexpected, let it surface.
    }
  }

  console.log(&amp;#39;all records:&amp;#39;, await queryByUser(key.pk));
  await deleteUser(key.pk, key.sk);
}

main().catch((err) =&amp;gt; {
  console.error(err);
  process.exit(1);
});
&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;The &lt;code&gt;DocumentClient&lt;/code&gt; deletes an entire category of bugs by letting you work in plain objects, and the CRUD helpers give every call site the same safe pattern: named updates, conditional writes, and key-based reads. You stop hand-writing attribute maps, stop accidentally overwriting fields, and stop reaching for &lt;code&gt;Scan&lt;/code&gt; when a &lt;code&gt;Query&lt;/code&gt; is right there.&lt;/p&gt;
&lt;h3&gt;Real invocations&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Install the SDK v3 packages.
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

# Set the table and region, then run the compiled script.
export AWS_REGION=us-east-1
export TABLE_NAME=AppTable
node dynamo.js

# Or run the TypeScript directly during development.
npx tsx dynamo.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tuning for single-table design&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;pk&lt;/code&gt;/&lt;code&gt;sk&lt;/code&gt; pair is the heart of single-table design: you overload one table with many item types by encoding the type into the key. A user profile might use &lt;code&gt;pk = &amp;quot;USER#42&amp;quot;&lt;/code&gt;, &lt;code&gt;sk = &amp;quot;PROFILE&amp;quot;&lt;/code&gt;, while their orders use the same &lt;code&gt;pk&lt;/code&gt; with &lt;code&gt;sk = &amp;quot;ORDER#1001&amp;quot;&lt;/code&gt;. One &lt;code&gt;QueryCommand&lt;/code&gt; on &lt;code&gt;USER#42&lt;/code&gt; then pulls the profile and every order in a single call.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Same partition key, different sort keys, one query returns them all.
await putUser({
  pk: &amp;#39;USER#42&amp;#39;,
  sk: &amp;#39;PROFILE&amp;#39;,
  name: &amp;#39;Ada&amp;#39;,
  email: &amp;#39;ada@example.com&amp;#39;,
});
await putUser({pk: &amp;#39;USER#42&amp;#39;, sk: &amp;#39;ORDER#1001&amp;#39;, name: &amp;#39;order&amp;#39;, email: &amp;#39;-&amp;#39;});

// begins_with narrows a query to just the orders under this user.
const orders = await ddb.send(
  new QueryCommand({
    TableName: TABLE_NAME,
    KeyConditionExpression: &amp;#39;pk = :pk AND begins_with(sk, :prefix)&amp;#39;,
    ExpressionAttributeValues: {&amp;#39;:pk&amp;#39;: &amp;#39;USER#42&amp;#39;, &amp;#39;:prefix&amp;#39;: &amp;#39;ORDER#&amp;#39;},
  }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;How the SDK v3 &lt;code&gt;DocumentClient&lt;/code&gt; stacks up against the other common ways to talk to DynamoDB from Node.js.&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;Marshalling&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;API style&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Maintained&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;SDK v3 &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Automatic&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Command objects&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;New Node.js apps on the SDK v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;SDK v3 low-level &lt;code&gt;DynamoDBClient&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Manual maps&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Command objects&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Fine-grained control, edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;SDK v2 &lt;code&gt;DynamoDB.DocumentClient&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Automatic&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Method calls&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Maintenance only&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Legacy code still on v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;An ORM/ODM like Dynamoose&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Automatic + schema&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Model methods&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Teams wanting model abstractions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The low-level client is worth dropping to only when you need something the &lt;code&gt;DocumentClient&lt;/code&gt; doesn&amp;#39;t expose. For everyday CRUD, the document client&amp;#39;s automatic marshalling is the reason it exists.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What&amp;#39;s the difference between DynamoDBClient and DynamoDBDocumentClient?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;DynamoDBClient&lt;/code&gt; is the low-level client: it speaks DynamoDB&amp;#39;s native format, so every value is a typed attribute map like &lt;code&gt;{ S: &amp;quot;text&amp;quot; }&lt;/code&gt; or &lt;code&gt;{ N: &amp;quot;3&amp;quot; }&lt;/code&gt;. &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt; wraps it and adds marshalling in both directions, so you pass and receive plain JavaScript objects. You still create the base client, then build the document client from it with &lt;code&gt;DynamoDBDocumentClient.from(baseClient)&lt;/code&gt;. Use the document client for CRUD and drop to the base client only for the rare feature it doesn&amp;#39;t cover.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Why should I prefer Query over Scan?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Query&lt;/code&gt; reads only the items that share a partition key, using the index directly, so it stays fast and cheap as the table grows. &lt;code&gt;Scan&lt;/code&gt; reads every item in the table and then filters, which means its cost and latency scale with the whole dataset, not with the rows you want. Design your keys so the reads you need are &lt;code&gt;Query&lt;/code&gt; calls. Keep &lt;code&gt;Scan&lt;/code&gt; for genuine full-table jobs like exports or backfills, and even then paginate it.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I update a single field without overwriting the whole item?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use &lt;code&gt;UpdateCommand&lt;/code&gt; with an &lt;code&gt;UpdateExpression&lt;/code&gt; that names only the fields you&amp;#39;re changing, such as &lt;code&gt;SET #n = :name&lt;/code&gt; or &lt;code&gt;ADD loginCount :one&lt;/code&gt;. That leaves every other attribute on the item untouched. &lt;code&gt;PutCommand&lt;/code&gt;, by contrast, replaces the entire item, so any field you don&amp;#39;t include is dropped. When a name collides with a DynamoDB reserved word, alias it through &lt;code&gt;ExpressionAttributeNames&lt;/code&gt; (the &lt;code&gt;#n&lt;/code&gt; above) so the expression still parses.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What does ConditionExpression protect against?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;A &lt;code&gt;ConditionExpression&lt;/code&gt; makes a write happen only if the condition holds, and fail otherwise. &lt;code&gt;attribute_exists(pk)&lt;/code&gt; blocks an update from silently creating a new item, &lt;code&gt;attribute_not_exists(pk)&lt;/code&gt; blocks a put from overwriting an existing one, and a version check like &lt;code&gt;version = :expected&lt;/code&gt; gives you optimistic locking. When the condition fails, DynamoDB throws &lt;code&gt;ConditionalCheckFailedException&lt;/code&gt;, which you catch and handle instead of letting a bad write through.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I handle results that span multiple pages?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;DynamoDB returns at most 1 MB of data per &lt;code&gt;Query&lt;/code&gt; or &lt;code&gt;Scan&lt;/code&gt;. When there&amp;#39;s more, the response includes a &lt;code&gt;LastEvaluatedKey&lt;/code&gt; cursor. Pass it back as &lt;code&gt;ExclusiveStartKey&lt;/code&gt; on the next call and loop until the response stops returning one, which is exactly what &lt;code&gt;queryByUser&lt;/code&gt; does. Skip the loop and you silently read only the first page, a bug that hides until your data crosses the 1 MB boundary in production.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Do I still need the AWS SDK v2 DocumentClient?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Only for code that already runs on it. The SDK v2 is in maintenance mode, so new work should use v3, which is modular (you install just &lt;code&gt;@aws-sdk/client-dynamodb&lt;/code&gt; and &lt;code&gt;@aws-sdk/lib-dynamodb&lt;/code&gt;), tree-shakeable, and actively developed. The v3 &lt;code&gt;DynamoDBDocumentClient&lt;/code&gt; gives you the same automatic marshalling the v2 &lt;code&gt;DocumentClient&lt;/code&gt; did, just with the command-object API. Migrate when you can, but there&amp;#39;s no need to rush a rewrite that works.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/&quot;&gt;AWS SDK v3: @aws-sdk/lib-dynamodb (DocumentClient)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-dynamodb/&quot;&gt;AWS SDK v3: @aws-sdk/client-dynamodb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html&quot;&gt;DynamoDB Developer Guide: Query operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html&quot;&gt;DynamoDB Developer Guide: Condition expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html&quot;&gt;DynamoDB Developer Guide: Single-table design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/welcome.html&quot;&gt;AWS SDK for JavaScript v3 Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>nodejs</category><category>aws</category><category>database</category><author>Mohammad Abu Mattar</author><enclosure url="https://codesnippets.mkabumattar.com/assets/codesnippets/0017-nodejs-dynamodb-crud-aws-sdk-v3/hero.png" length="0" type="image/jpeg"/></item><item><title>Python Async HTTP Requests with aiohttp: Fetch Multiple URLs Concurrently</title><link>https://codesnippets.mkabumattar.com/post/python-async-http-aiohttp-concurrent-requests/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/python-async-http-aiohttp-concurrent-requests/</guid><description>A Python snippet demonstrating concurrent HTTP requests using aiohttp and asyncio. Covers creating sessions, fetching multiple URLs in parallel, handling errors gracefully, and controlling concurrency with semaphores.</description><pubDate>Sun, 26 Jul 2026 14:40:06 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Quick Tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Reuse one &lt;code&gt;aiohttp&lt;/code&gt; session, fan your requests out with &lt;code&gt;asyncio.gather&lt;/code&gt;, and cap them with a semaphore to fetch hundreds of URLs in the time one loop would take.&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;A script that fetches a list of URLs with &lt;code&gt;requests&lt;/code&gt; in a plain &lt;code&gt;for&lt;/code&gt; loop spends almost all of its time doing nothing. Each call opens a connection, sends the request, and then blocks until the response comes back before the next one even starts. The network is slow and your CPU is fast, so the program just sits and waits, one round trip at a time.&lt;/p&gt;
&lt;h3&gt;Why sequential requests waste time&lt;/h3&gt;
&lt;p&gt;HTTP is I/O bound. The bottleneck isn&amp;#39;t your code, it&amp;#39;s the latency of each request. If a single call takes 200ms and you have 200 URLs, a sequential loop takes 40 seconds, and 39 of those seconds are pure waiting. Nothing about that work needs to happen in order, yet the loop forces it to.&lt;/p&gt;
&lt;h3&gt;The real-world impact&lt;/h3&gt;
&lt;p&gt;That waiting shows up as slow batch jobs, sluggish data-collection scripts, and API clients that time out because they can&amp;#39;t keep up. Scale the list up and it gets worse linearly: a report that pulls from a few hundred endpoints turns a quick task into a coffee break, and a service that fans out to several upstreams makes every user wait for the slowest chain of calls.&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;Use &lt;code&gt;aiohttp&lt;/code&gt; with &lt;code&gt;asyncio&lt;/code&gt; so the requests overlap. While one call waits on the network, the event loop starts the next one. Reuse a single &lt;code&gt;ClientSession&lt;/code&gt; so connections get pooled, launch everything with &lt;code&gt;asyncio.gather&lt;/code&gt;, and put a &lt;code&gt;Semaphore&lt;/code&gt; in front to keep the number of in-flight requests sane.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create one &lt;code&gt;aiohttp.ClientSession&lt;/code&gt; and reuse it for every request.&lt;/li&gt;
&lt;li&gt;Fire all the fetches concurrently with &lt;code&gt;asyncio.gather&lt;/code&gt; instead of a blocking loop.&lt;/li&gt;
&lt;li&gt;Cap concurrency with &lt;code&gt;asyncio.Semaphore&lt;/code&gt; so you don&amp;#39;t overwhelm the server or get rate-limited.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Script Implementation&lt;/h2&gt;
&lt;h3&gt;Setup and imports&lt;/h3&gt;
&lt;p&gt;Start with the async pieces you need: &lt;code&gt;asyncio&lt;/code&gt; for the event loop and &lt;code&gt;aiohttp&lt;/code&gt; for the HTTP calls. A small result type keeps the return values easy to read.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&amp;quot;&amp;quot;&amp;quot;fetch_urls.py - fetch many URLs concurrently with aiohttp.&amp;quot;&amp;quot;&amp;quot;
import asyncio
import time
from dataclasses import dataclass

import aiohttp

# Cap how many requests run at once. Tune to the server and your rate limits.
MAX_CONCURRENCY = 10
REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=15)


@dataclass
class Result:
    url: str
    status: int | None  # HTTP status, or None if the request never completed
    body: str | None  # response text on success
    error: str | None  # error message on failure
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fetching one URL safely&lt;/h3&gt;
&lt;p&gt;Each fetch takes the shared session and the semaphore. The semaphore blocks once &lt;code&gt;MAX_CONCURRENCY&lt;/code&gt; requests are already in flight, so the rest queue up instead of stampeding the server. The &lt;code&gt;try/except&lt;/code&gt; keeps a single bad URL from crashing the whole batch.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def fetch_one(
    session: aiohttp.ClientSession,
    sem: asyncio.Semaphore,
    url: str,
) -&amp;gt; Result:
    # The semaphore is the throttle: only MAX_CONCURRENCY of these run at once.
    async with sem:
        try:
            async with session.get(url) as response:
                # Read the body while the connection is still open.
                text = await response.text()
                return Result(url, response.status, text, None)
        except aiohttp.ClientError as exc:
            # Network, DNS, or connection errors land here.
            return Result(url, None, None, f&amp;quot;client error: {exc}&amp;quot;)
        except asyncio.TimeoutError:
            return Result(url, None, None, &amp;quot;timed out&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fetching the whole batch&lt;/h3&gt;
&lt;p&gt;Create one session and one semaphore, then build a task per URL and hand them all to &lt;code&gt;asyncio.gather&lt;/code&gt;. Because every &lt;code&gt;fetch_one&lt;/code&gt; already catches its own errors, &lt;code&gt;gather&lt;/code&gt; returns a clean list of &lt;code&gt;Result&lt;/code&gt; objects with no partial failures to unpack.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def fetch_all(urls: list[str]) -&amp;gt; list[Result]:
    sem = asyncio.Semaphore(MAX_CONCURRENCY)
    # One session for the whole run so connections get pooled and reused.
    async with aiohttp.ClientSession(timeout=REQUEST_TIMEOUT) as session:
        tasks = [fetch_one(session, sem, url) for url in urls]
        # gather runs them concurrently and preserves input order.
        return await asyncio.gather(*tasks)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Entry point&lt;/h3&gt;
&lt;p&gt;Wrap the run in &lt;code&gt;asyncio.run&lt;/code&gt;, then print a short summary. Timing the run makes the win obvious the first time you compare it to a sequential loop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def main() -&amp;gt; None:
    urls = [f&amp;quot;https://httpbin.org/delay/1?id={i}&amp;quot; for i in range(20)]

    start = time.perf_counter()
    results = asyncio.run(fetch_all(urls))
    elapsed = time.perf_counter() - start

    ok = sum(1 for r in results if r.status == 200)
    failed = [r for r in results if r.error]
    print(f&amp;quot;fetched {ok}/{len(results)} in {elapsed:.2f}s&amp;quot;)
    for r in failed:
        print(f&amp;quot;  failed: {r.url} ({r.error})&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&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 session, one semaphore, and one &lt;code&gt;gather&lt;/code&gt; call turn a linear wait into an overlapping one. The requests still take the same time individually, but they no longer take that time one after another, so a batch that scaled with the number of URLs now scales with your concurrency limit instead.&lt;/p&gt;
&lt;h3&gt;Real invocations&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Install the dependency first.
pip install aiohttp

# Run the script. Twenty 1-second requests finish in about 2-3 seconds,
# not 20, because up to MAX_CONCURRENCY of them run at the same time.
python fetch_urls.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tuning concurrency for the target&lt;/h3&gt;
&lt;p&gt;There&amp;#39;s no universal number for &lt;code&gt;MAX_CONCURRENCY&lt;/code&gt;. A generous internal API might handle 50 in-flight requests; a rate-limited public one might block you above 5. Start low, watch for &lt;code&gt;429&lt;/code&gt; responses, and raise it until you hit the server&amp;#39;s comfort zone.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Lower the limit for a strict public API to avoid 429s.
MAX_CONCURRENCY = 5

# Raise it for a fast internal service that can take the load.
MAX_CONCURRENCY = 50
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;How the concurrent approach stacks up against the other common ways to fetch a list of URLs.&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;Concurrent&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Connection reuse&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Concurrency control&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Best for&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;aiohttp&lt;/code&gt; + &lt;code&gt;asyncio.gather&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes, pooled&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes, semaphore&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Large async I/O-bound fetches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;requests&lt;/code&gt; in a &lt;code&gt;for&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;Per session only&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;A handful of simple calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;requests&lt;/code&gt; + &lt;code&gt;ThreadPool&lt;/code&gt;&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;td align=&quot;left&quot;&gt;Pool size&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Mixing with blocking code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;httpx.AsyncClient&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes, pooled&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Yes, limits/semaphore&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Async with a requests-like API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The threaded pool is a fine middle ground when the rest of your code is synchronous, but an event loop scales to far more concurrent connections without spinning up a thread per request.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Why reuse one ClientSession instead of creating one per request?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;A &lt;code&gt;ClientSession&lt;/code&gt; owns a connection pool. Reusing it lets aiohttp keep TCP and TLS connections alive across requests, which skips the handshake cost on every call. Creating a fresh session per request throws that away and leaks connections. Open one session for the batch, pass it to every fetch, and close it with &lt;code&gt;async with&lt;/code&gt; when you&amp;#39;re done.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;What does the Semaphore actually protect against?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Without a limit, &lt;code&gt;asyncio.gather&lt;/code&gt; starts every request at once. A thousand URLs means a thousand simultaneous connections, which can exhaust file descriptors on your side and trip rate limits or overload the server on the other. The semaphore caps how many run concurrently: each &lt;code&gt;async with sem&lt;/code&gt; acquires a slot and releases it when the request finishes, so the rest wait their turn.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I stop one failing URL from crashing the whole batch?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Catch the errors inside each task, which is what &lt;code&gt;fetch_one&lt;/code&gt; does with its &lt;code&gt;try/except&lt;/code&gt;. Because every coroutine returns a &lt;code&gt;Result&lt;/code&gt; instead of raising, &lt;code&gt;asyncio.gather&lt;/code&gt; never sees an exception and returns a full list. If you&amp;#39;d rather let exceptions propagate, use &lt;code&gt;asyncio.gather(*tasks, return_exceptions=True)&lt;/code&gt; and inspect the returned values, some of which will be exception objects.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;Does asyncio.gather preserve the order of my URLs?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes. &lt;code&gt;gather&lt;/code&gt; returns results in the same order you passed the awaitables, regardless of which requests finish first. So &lt;code&gt;results[i]&lt;/code&gt; always corresponds to &lt;code&gt;urls[i]&lt;/code&gt;, even though the fetches complete out of order. If you don&amp;#39;t care about order and want to process results as they arrive, look at &lt;code&gt;asyncio.as_completed&lt;/code&gt; instead.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;When should I use threads or httpx instead of aiohttp?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Reach for a thread pool with &lt;code&gt;requests&lt;/code&gt; when the surrounding code is synchronous and you don&amp;#39;t want to convert it to async just for a few calls. Reach for &lt;code&gt;httpx.AsyncClient&lt;/code&gt; when you want an async client with an API closer to &lt;code&gt;requests&lt;/code&gt;, or you need HTTP/2. &lt;code&gt;aiohttp&lt;/code&gt; shines when you&amp;#39;re already in an async application and need to fan out to many endpoints at high concurrency.&lt;/p&gt;
&lt;/Accordion&gt;&lt;p&gt;&amp;lt;Accordion
  client:load
  title=&amp;quot;How do I add a timeout so slow requests don&amp;#39;t hang forever?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pass an &lt;code&gt;aiohttp.ClientTimeout&lt;/code&gt; to the session, as the snippet does with &lt;code&gt;REQUEST_TIMEOUT&lt;/code&gt;. A &lt;code&gt;total&lt;/code&gt; timeout bounds the whole request, including connect and read. When it&amp;#39;s exceeded, aiohttp raises &lt;code&gt;asyncio.TimeoutError&lt;/code&gt;, which the &lt;code&gt;except&lt;/code&gt; in &lt;code&gt;fetch_one&lt;/code&gt; turns into a clean failed &lt;code&gt;Result&lt;/code&gt; instead of a hung coroutine.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aiohttp.org/en/stable/client_quickstart.html&quot;&gt;aiohttp documentation: client quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aiohttp.org/en/stable/client_reference.html&quot;&gt;aiohttp documentation: client reference and ClientSession&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html#asyncio.gather&quot;&gt;Python docs: asyncio.gather&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore&quot;&gt;Python docs: asyncio.Semaphore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html#asyncio.as_completed&quot;&gt;Python docs: asyncio.as_completed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python-httpx.org/async/&quot;&gt;httpx documentation: async client&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>python</category><category>async</category><category>networking</category><author>Mohammad Abu Mattar</author><enclosure url="https://codesnippets.mkabumattar.com/assets/codesnippets/0016-python-async-http-aiohttp-concurrent-requests/hero.png" length="0" type="image/jpeg"/></item><item><title>Bash Retry Function: Automatically Retry Failing Commands with Exponential Backoff</title><link>https://codesnippets.mkabumattar.com/post/bash-retry-function-exponential-backoff/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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://codesnippets.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://codesnippets.mkabumattar.com/post/nodejs-env-validation-zod-startup/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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; In most Node.js apps it is normal 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 checks the values and 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.&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://codesnippets.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://codesnippets.mkabumattar.com/post/aws-ec2-instance-management-boto3-python/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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.&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;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. 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://codesnippets.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://codesnippets.mkabumattar.com/post/redis-caching-patterns-architecture/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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? Start with Redis caching patterns.&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. Serving data from memory 99% of the time with Redis fixes it instead.&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, so results come back in microseconds instead of milliseconds. The caching pattern you choose decides whether the 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, so stale data clears without manual invalidation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache Invalidation&lt;/strong&gt;: Delete the cache entry when data updates so the next request refreshes it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stampede Prevention&lt;/strong&gt;: Stop the thundering herd that follows a popular key expiring&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 it&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;Redis caching is the difference between a responsive application and a slow one.&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 is what keeps a high-traffic system fast.&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://codesnippets.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://codesnippets.mkabumattar.com/post/postgresql-query-optimization-indexes-explain/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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? Use EXPLAIN ANALYZE and targeted indexing.&lt;/h3&gt;
&lt;p&gt;Slow database queries kill application performance. Most developers don&amp;#39;t know where the actual bottleneck is, so they guess at fixes. EXPLAIN ANALYZE shows what is 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 sequential table scans start to cost real time. 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;Reading EXPLAIN ANALYZE&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;Set up connection pooling so connections get reused instead of reopened&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 and 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 a 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: 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://codesnippets.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://codesnippets.mkabumattar.com/post/hashicorp-vault-multi-environment-secrets/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;Managing secrets safely across multiple environments 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 the 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://codesnippets.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://codesnippets.mkabumattar.com/post/top-7-open-source-ocr-models/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/top-7-open-source-ocr-models/</guid><description>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 examples.</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 accurate digital copies with these open source OCR models. Instead of fighting messy text extraction, you get clean markdown from PDFs, images, and scanned documents.&lt;/p&gt;
&lt;h2&gt;What modern OCR models do&lt;/h2&gt;
&lt;h3&gt;Beyond basic text extraction&lt;/h3&gt;
&lt;p&gt;These models do more than read text. They recognize document structure, tables, diagrams, math equations, and multiple languages, and turn all of it into well-formatted markdown.&lt;/p&gt;
&lt;h3&gt;Local processing&lt;/h3&gt;
&lt;p&gt;Run these models on your own machine without sending sensitive documents to cloud services. You keep control over your data and get accuracy close to enterprise services.&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;Allen Institute&amp;#39;s flagship OCR model. It is fine-tuned from Qwen2.5-VL-7B-Instruct using GRPO reinforcement learning and scores 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&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;An ultra-compact vision-language model. It integrates the NaViT visual encoder with the ERNIE language model and supports 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;: Strong 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 use of memory and compute&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;A preview release from ChatDOC, 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;High accuracy&lt;/strong&gt;: Strong 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;The latest model in the MiniCPM-V series. Built on Qwen3-8B and SigLIP2-400M, it handles text recognition in images, documents, and video.&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;Benchmark results&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;An efficient vision-language model. It combines the InternViT vision encoder with the Qwen2.5 language model for OCR and document 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;IBM&amp;#39;s vision-language model, built on Granite 3.1-2b-instruct with a 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;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;A transformer-based OCR model. Its encoder-decoder architecture combines the BEiT image transformer with the 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;: Strong 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;: Uses 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;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;Hardware is usually the limiting factor.&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;Resize oversized images before inference so they fit the model&amp;#39;s input budget.&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;Catch failures per document so one bad file does not stop a batch.&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;Wrap the extracted text with metadata so downstream code knows which model produced it.&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 cover most document processing needs today. Choose based on what your workload demands: accuracy, speed, language coverage, or the hardware you have available.&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://codesnippets.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://codesnippets.mkabumattar.com/post/printf-beats-echo-linux-scripts/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/printf-beats-echo-linux-scripts/</guid><description>Why printf is more reliable than echo for output in Linux scripts. The portability problems with echo, what printf gives you instead, and when each command is the right choice.</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;A script that works on your machine can produce different output on another system. The output command is often the reason. &lt;code&gt;printf&lt;/code&gt; behaves the same way across shells, and &lt;code&gt;echo&lt;/code&gt; does not.&lt;/p&gt;
&lt;h2&gt;The problem with echo&lt;/h2&gt;
&lt;h3&gt;Unpredictable behavior&lt;/h3&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;A script that runs fine locally starts misbehaving once you deploy it or hand it to a colleague. Those inconsistencies produce bugs that are hard to track down.&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;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&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, set numeric precision, and print several values from one 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;Instead of &lt;code&gt;echo &amp;quot;Hello, world&amp;quot;&lt;/code&gt;, write &lt;code&gt;printf &amp;quot;Hello, world\n&amp;quot;&lt;/code&gt;. The newline is spelled out in the command, so you always know what you are getting.&lt;/p&gt;
&lt;h3&gt;Suppressing newlines&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;echo -n &amp;quot;Processing...&amp;quot;&lt;/code&gt; behaves differently from shell to shell. With &lt;code&gt;printf&lt;/code&gt; you leave the newline out of the format string instead: &lt;code&gt;printf &amp;quot;Processing...&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Variable output safety&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;echo $VARIABLE&lt;/code&gt; breaks on spaces and special characters. Use &lt;code&gt;printf &amp;quot;%s\n&amp;quot; &amp;quot;$VARIABLE&amp;quot;&lt;/code&gt; instead. It is 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;code&gt;printf&lt;/code&gt; always handles escape sequences correctly. &lt;code&gt;echo&lt;/code&gt; may 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;You can build aligned tables and structured output that other programs can 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;Generate CSV files that format the same way 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;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;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;code&gt;printf&lt;/code&gt; respects your system&amp;#39;s locale settings. In some locales, decimal points become commas, which can break scripts that parse the output.&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;For scripts that need to work everywhere, set &lt;code&gt;LC_NUMERIC=C&lt;/code&gt; or handle numbers explicitly so the locale cannot change the output.&lt;/p&gt;
&lt;h2&gt;Making the switch&lt;/h2&gt;
&lt;h3&gt;Gradual migration&lt;/h3&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;Always test your scripts on different systems and shells to confirm the output is the same 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 that scripts need. &lt;code&gt;echo&lt;/code&gt; works for quick tasks, but &lt;code&gt;printf&lt;/code&gt; behaves predictably no matter where it runs. Make &lt;code&gt;printf&lt;/code&gt; the default 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://codesnippets.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://codesnippets.mkabumattar.com/post/essential-bash-variables/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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 reliable 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. Built-in Bash variables fix that.&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 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, a kind of 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: filenames, options, or configuration values. Bash makes this pretty straightforward, though the syntax looks a bit weird at first.&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;. 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, use &lt;code&gt;$EUID&lt;/code&gt; for permission checks. That is the one that matters.&lt;/p&gt;
&lt;/Notice&gt;&lt;h3&gt;Use the XDG directory specification&lt;/h3&gt;
&lt;p&gt;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 respects how users organize their systems, not just the spec. 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;Here&amp;#39;s what a script looks like when you use all these variables properly. It&amp;#39;s got error handling, logging, argument processing, and follows XDG standards. Use it 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;Do these things&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;Avoid these mistakes&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. That is what actually matters for access control.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;References&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://codesnippets.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://codesnippets.mkabumattar.com/post/bash-per-app-history/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;Organize your Bash history per terminal app.&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, which makes context hard to keep.&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. It all happens on shell startup.&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 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 work. You can switch between terminals without worrying about command pollution or losing context. It helps most if you use different terminals for work, personal projects, or testing, since 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://codesnippets.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://codesnippets.mkabumattar.com/post/zsh-per-app-history/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;Organize your shell history per terminal app.&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, which makes context hard to keep.&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. It all happens on shell startup.&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 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 work. You can switch between terminals without worrying about command pollution or losing context. It helps most if you use different terminals for work, personal projects, or testing, since 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://codesnippets.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://codesnippets.mkabumattar.com/post/python-slots-optimization/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;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 spend extra memory on the hidden &lt;code&gt;__dict__&lt;/code&gt; attribute. That overhead turns into memory bottlenecks and slower processing, especially in 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;&lt;code&gt;__slots__&lt;/code&gt; reduces 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 instead of a dynamic &lt;code&gt;__dict__&lt;/code&gt;. Objects get smaller and attribute access gets faster, which matters 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;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 regular class, 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 grows large enough to crash your application or push you onto 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 cuts memory consumption, so you process more data with the same resources and the pipeline runs faster. In MLOps that matters, because resource use drives both your cloud bill and how far the pipeline can scale.&lt;/p&gt;
&lt;h3&gt;Big data processing benefits&lt;/h3&gt;
&lt;h3&gt;An 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 what keeps the job from crashing and the cloud bill low. Using &lt;code&gt;__slots__&lt;/code&gt; reduces the memory footprint of these data objects, which makes the pipeline more reliable and easier to scale.&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 then compares the memory usage and the time taken to create a million instances of each.&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;. On large datasets both memory use and creation time drop.&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, which suits 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://codesnippets.mkabumattar.com/assets/codesnippets/0004-python-slots-optimization/hero.png" length="0" type="image/jpeg"/></item><item><title>List S3 Buckets</title><link>https://codesnippets.mkabumattar.com/post/python-list-s3-buckets/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;Ever juggled multiple AWS accounts and needed a quick S3 bucket inventory across all of them? This Python script handles it.&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 slow and easy to get wrong. A full inventory or audit turns into 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 uses Boto3 to discover all your configured AWS CLI profiles. It then iterates through each profile, attempting to list its S3 buckets. It prints a profile-by-profile breakdown and handles errors like missing credentials or access denied on a single profile, so the run finishes even in complex setups.&lt;/p&gt;
&lt;h3&gt;Detailed 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;Prints a per-profile listing of S3 buckets.&lt;/li&gt;
&lt;li&gt;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 saves time for anyone managing multiple AWS environments. It covers S3 bucket auditing, inventory tasks, and compliance checks across your whole AWS footprint in a single run. You get one view without switching profiles by hand.&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://codesnippets.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://codesnippets.mkabumattar.com/post/nodejs-aws-secrets-manager/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/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;Loading secrets in a Node.js app without exposing them&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&amp;#39;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 well into cloud-native workflows, especially when you&amp;#39;re using IAM roles or CI/CD pipelines.&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://codesnippets.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://codesnippets.mkabumattar.com/post/bash-s3-bucket-exists/</link><guid isPermaLink="true">https://codesnippets.mkabumattar.com/post/bash-s3-bucket-exists/</guid><description>Validate AWS S3 bucket presence in your scripts. This Bash snippet 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&amp;#39;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.&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;A deployment that fails on a missing S3 bucket wastes CI/CD time and delays the release.&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 to add and easy to reuse across projects.&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://codesnippets.mkabumattar.com/assets/codesnippets/0001-bash-s3-bucket-exists/hero.png" length="0" type="image/jpeg"/></item></channel></rss>