<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dorrabbkhan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dorrabbkhan.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-11-12T14:38:30+00:00</updated><id>https://dorrabbkhan.github.io/feed.xml</id><title type="html">Dorrabb Khan</title><subtitle>Senior DevOps &amp; Cloud Engineer in Munich. Writing about AWS, FinOps, observability and developer experience.</subtitle><author><name>Dorrabb Khan</name></author><entry><title type="html">Running my life like a small platform</title><link href="https://dorrabbkhan.github.io/blog/2026/i-run-my-life-like-a-small-platform/" rel="alternate" type="text/html" title="Running my life like a small platform" /><published>2026-11-11T00:00:00+00:00</published><updated>2026-11-11T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/i-run-my-life-like-a-small-platform</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/i-run-my-life-like-a-small-platform/"><![CDATA[<p><strong>TL;DR:</strong> A second brain isn’t a pile of notes - it’s a platform with exactly one user. Mine has an inbox for ingest, skills that act as jobs, cron for scheduled runs, a guard around the AI agents and git for rollback. Its structure keeps changing as new use cases show up, and that is by design. Treating it like infrastructure is why it stays trustworthy, and the same lens shows where the comparison breaks.</p>

<p>For years I hoarded notes the way I hoarded half-written blog drafts: plenty of capture, no system behind it. Then life got busy and things became too much to track in my head, with no note I trusted enough to track it for me.</p>

<p>That’s when I came across the concept of a second brain: a collection of notes with an agent sitting on top, giving a single user the features I’d expect from any platform - ingest, ownership, sync and a rollback plan. If I wouldn’t run production without those, why would I run my own knowledge that way?</p>

<h2 id="the-platform-mapped">The Platform, Mapped</h2>

<p>My second brain is a folder of plain Markdown files edited in Obsidian and versioned in git. AI agents maintain it. Every part has a platform equivalent:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>capture -&gt; Inbox -&gt; sort -&gt; Notes (Markdown + metadata)
                              |-&gt; skills (jobs)    -&gt; saved reports
                              |-&gt; scheduled runs   -&gt; briefing, calendar
                              '-&gt; git              -&gt; history, rollback
</code></pre></div></div>

<ul>
  <li><strong>Inbox is ingest:</strong> Everything lands in one place and gets sorted later. No decisions at capture time.</li>
  <li><strong>Syncs to my iPhone:</strong> One-way sync via Syncthing and the Synctrain mobile app.</li>
  <li><strong>Metadata is the schema:</strong> Every note carries <code class="language-plaintext highlighter-rouge">type</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">sensitivity</code>, <code class="language-plaintext highlighter-rouge">updated</code> and <code class="language-plaintext highlighter-rouge">sources</code>, so confirmed facts stay apart from wishes and open questions.</li>
  <li><strong>Folders are the architecture:</strong></li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>01 - Inbox/       quick captures
02 - Projects/    one folder per project
03 - Areas/       ongoing responsibilities
04 - Knowledge/   durable reference notes
06 - Journal/     briefings and saved reports
07 - Archives/    nothing is deleted, only archived
08 - System/      skills, templates, standards
</code></pre></div></div>

<h2 id="skills-and-schedules">Skills and Schedules</h2>

<p>A skill is a written workflow: purpose, trigger, what it reads, what it writes, steps and approval gates. A few I use:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">/daily-briefing</code>:</strong> Priorities, due and overdue tasks, project updates and deadlines on one page.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/inbox</code>:</strong> Sorts quick captures into the right notes.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/new-project</code>:</strong> Interviews me, then drafts a groomed project note.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/youtube-notes</code>:</strong> A timestamped summary of a video, shown in chat and saved only if I ask.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/study-quiz</code>:</strong> Quizzes me from a study note, explains misses and keeps the attempt history.</li>
</ul>

<p>Most follow the same pipeline: gather, draft, show, approve, write. This post went through it - ideas, an interview about what I actually did, a draft, and a file written only after my yes.</p>

<p>The boring parts run on cron:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0 7  * * *  github-progress   # project state, read-only on GitHub
30 7 * * *  calendar-sync     # dated tasks to my calendar
0 12 * * *  daily-briefing
0 20 * * *  evening-inbox     # report only, nothing filed
0 18 * * 0  weekly-changes
0 23 * * 0  backup
0 9  1 * *  monthly-audit
</code></pre></div></div>

<p>The calendar sync only creates events. It never edits or deletes one, and it skips anything already there.</p>

<p>Every run ends in a report, and each finding becomes a <code class="language-plaintext highlighter-rouge">#review</code> task tagged <code class="language-plaintext highlighter-rouge">fix: auto</code> or <code class="language-plaintext highlighter-rouge">fix: ask</code>. A <code class="language-plaintext highlighter-rouge">/fix</code> command applies the safe ones as one batch (links, stale dates), asks me about the rest one at a time and archives reports with nothing left open.</p>

<h2 id="guardrails-not-trust">Guardrails, Not Trust</h2>

<p>Scheduled agents write to my notes while I’m not looking, so each run is controlled:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lock -&gt; snapshot -&gt; agent run (scoped) -&gt; scope guard -&gt; logs
</code></pre></div></div>

<p>The lock keeps one writer in the vault at a time. A git snapshot comes first, so any automated change can be reverted. Then the agent may write only inside its job’s scope. Afterwards a script checks what changed, copies anything out of scope to quarantine and restores it from the snapshot. Nothing is deleted.</p>

<p>It’s also agent agnostic. The rules live in <code class="language-plaintext highlighter-rouge">AGENTS.md</code>, not in a tool. Claude Code runs my skills interactively through a thin <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> pointer, another agent (Devin) runs the scheduled jobs, and the slash commands are wrappers that name the same skill file. Switching the scheduler is one variable:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AGENT=devin   # or: AGENT=claude
</code></pre></div></div>

<p>The rules are the source of truth, and the agent is just the runtime.</p>

<h2 id="from-lecture-slides-to-study-notes">From Lecture Slides to Study Notes</h2>

<p>Lectures are where a lot of my learning happens, and their PDFs are the raw material. The <code class="language-plaintext highlighter-rouge">/ingest-pdf</code> skill turns a dropped PDF into a linked study note:</p>

<ul>
  <li>The original PDF is copied, unchanged, into a sources folder.</li>
  <li>The study note covers every page, and the skill checks that each content page is accounted for.</li>
  <li>Each page reference is a link to that page of the PDF, so I can jump from any claim to the slide behind it.</li>
  <li>On request, a one-page cheatsheet with the must-know items, formulas and pitfalls, each with a page link.</li>
</ul>

<p>The pieces compound. A course note keeps a week-by-week map, a quiz skill tests me on a study note and keeps the attempt history, and a prep skill builds each week’s cheatsheet from its deadlines. When a week needed two notes, I gave them letters (04a, 04b) so the order stayed clear.</p>

<h2 id="it-keeps-changing">It Keeps Changing</h2>

<p>The structure isn’t finished, and I don’t want it to be. Each new use case has pushed a change into the design, and the changes are visible in the vault’s own change log. A few examples:</p>

<ul>
  <li><strong>Folders moved from flat to numbered.</strong> Early on, notes sat in a few broad folders. Once there were more of them, I moved everything into numbered folders, so the order in the file tree matches how I use it.</li>
  <li><strong>Projects went from single files to folders.</strong> Each project got its own folder with a standard <code class="language-plaintext highlighter-rouge">Project.md</code> note and any research beside it.</li>
  <li><strong>Filenames got fixed.</strong> Dozens of notes shared generic names like <code class="language-plaintext highlighter-rouge">Project.md</code>, which made the graph view unreadable. Each note now takes its folder’s name.</li>
  <li><strong>Schedules get re-timed.</strong> The daily briefing started at 07:30 and now runs at noon, because noon is when I actually want to read it.</li>
  <li><strong>Features get dropped.</strong> A weekly review I’d built was replaced by a read-only changes report, because the inbox was enough.</li>
  <li><strong>Rules get added after something breaks.</strong> The scope guard exists because a sandbox test failed. The validator is the same story: each time a note broke a convention, a check went in to catch the next one. It now covers links, frontmatter, task formats and bare page references in study notes, and it has its own tests. A pre-commit hook runs it before any manual commit.</li>
</ul>

<p>The rule I’ve settled on: when a new use case shows up, I change the structure in the open, log the change, and keep the old version in git history. The brain is a working document, not a finished product.</p>

<h2 id="the-roadmap-is-a-backlog-too">The Roadmap Is a Backlog Too</h2>

<p>The brain has its own roadmap, split into Now, Next and Later, with one rule borrowed from Kanban: an item enters Now only when another one leaves, and Now holds at most three. Anything that connects a service or installs something needs my approval first. A few items I’m most interested in:</p>

<ul>
  <li><strong>Study engine:</strong> Spaced repetition built from my quiz history, so weak topics and exam countdowns show up in the daily briefing instead of in my memory.</li>
  <li><strong>Phone capture:</strong> A dated iOS Shortcut appends thoughts to a separate <code class="language-plaintext highlighter-rouge">phone-inbox/</code> folder, and the <code class="language-plaintext highlighter-rouge">inbox</code> skill files them. The catch is that Syncthing doesn’t nest folders, and iOS suspends the app, so captures wait until both devices are connected. Real-time capture was never the goal.</li>
  <li><strong>A UI layer:</strong> A site or app on top of the vault. It isn’t scoped yet, and I’d rather decide the data model first.</li>
  <li><strong>Cloud-hosted brain and cloud automation:</strong> The scheduled jobs would stop depending on my laptop being awake. The open questions are the host, encryption at rest and how Obsidian and Syncthing reach it, because the vault holds sensitive notes. That is why it sits in Later.</li>
  <li><strong>Agents, not just skills:</strong> Skills are fixed workflows. Agents would have defined roles and their own approval limits. I’d trial one runtime (Hermes Agent) in a separate test space with no access to the live vault first.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Now    (max 3)   in progress
Next             picked, not started
Later            ideas, with the open questions written down
</code></pre></div></div>

<p>The same habit applies to the roadmap as to production: write the constraint next to the feature, so the “why not yet” survives.</p>

<h2 id="where-the-metaphor-breaks">Where the Metaphor Breaks</h2>

<p>A platform serves many users. Mine serves one, and that user is me at 2 AM wanting a straight answer. That changes the rules. An agent can’t be trusted just because it’s capable, so the policy is strict: act freely on drafting and linking, act then report on small reversible edits, ask first for anything that deletes, publishes or leaves the vault. It never silently overwrites a fact, and it logs every change.</p>

<p>The other surprise: structure took longer than tooling. The tools were the easy part. Deciding the folders, the metadata and the rules took far more effort, and that’s where the value is.</p>

<p>One honest opinion: this is overkill for most people. If a plain notes app keeps you organized, use that. YMMV. I built mine because I had too much to track, and I’d rather maintain a system with proper tooling than trust my memory.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="devops" /><category term="productivity" /><category term="devex" /><summary type="html"><![CDATA[A second brain is a platform with one user: an inbox for ingest, skills as jobs, cron for scheduled runs, guardrails for the AI, and git for rollback. An intro to how mine works, where the metaphor breaks, and why its structure keeps changing.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">When a NAT Gateway costs more than your database</title><link href="https://dorrabbkhan.github.io/blog/2026/when-a-nat-gateway-costs-more-than-your-database/" rel="alternate" type="text/html" title="When a NAT Gateway costs more than your database" /><published>2026-10-21T00:00:00+00:00</published><updated>2026-10-21T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/when-a-nat-gateway-costs-more-than-your-database</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/when-a-nat-gateway-costs-more-than-your-database/"><![CDATA[<p><strong>TL;DR:</strong> NAT Gateway costs aren’t a networking bug - they’re an ownership failure. When infrastructure traffic is lumped into a shared networking line, nobody notices that gigabytes of internal AWS traffic are quietly paying a steep middleman tax. You can eliminate the vast majority of this spend using free Gateway Endpoints, deliberate interface routing, and a single rule in your Architecture Decision Records (ADRs).</p>

<p>The platform team created our NAT Gateway once in a base VPC module and never touched it again. Product teams generated a significant amount of egress every deploy, but never saw the cost because it landed on a shared infrastructure line. Everybody simply saw “EC2-Other” climbing on the monthly invoice without knowing what it meant.</p>

<h3 id="the-arithmetic-of-invisible-egress">The Arithmetic of Invisible Egress</h3>

<p>A managed NAT Gateway charges an hourly uptime fee (~$0.045/hour in us-east-1, slightly higher in European regions like Frankfurt at ~$0.052/hour), plus an identical per-gigabyte data processing fee.</p>

<p>That doesn’t sound punishing until you look at what’s actually traversing it. Every time an ECS task pulls a container image from ECR, queries an S3 bucket, or pushes logs to CloudWatch from a private subnet, AWS treats that traffic as external egress through the NAT unless told otherwise. You end up paying a per-GB processing fee on your own internal data.</p>

<p>To see what is actually hitting the gateway, avoid guessing. Run a quick CloudWatch Logs Insights query over your VPC Flow Logs on the NAT’s Elastic Network Interface (ENI):</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fields</span> <span class="o">@</span><span class="nb">timestamp</span><span class="p">,</span> <span class="n">srcAddr</span><span class="p">,</span> <span class="n">dstAddr</span><span class="p">,</span> <span class="n">bytes</span>
<span class="o">|</span> <span class="n">stats</span> <span class="k">sum</span><span class="p">(</span><span class="n">bytes</span><span class="p">)</span> <span class="k">as</span> <span class="n">totalBytes</span> <span class="k">by</span> <span class="n">dstAddr</span>
<span class="o">|</span> <span class="n">sort</span> <span class="n">totalBytes</span> <span class="k">desc</span>
<span class="o">|</span> <span class="k">limit</span> <span class="mi">10</span>
</code></pre></div></div>

<p>Nine times out of ten, the top talkers aren’t public third-party APIs. They are AWS service endpoints.</p>

<h3 id="the-fixes-ranked-by-effort">The Fixes, Ranked by Effort</h3>

<p>Fixing this isn’t about re-architecting your entire network. It’s simply about taking the traffic off the NAT’s payroll:</p>

<h4 id="1-s3-and-dynamodb-gateway-endpoints-zero-cost">1. S3 and DynamoDB Gateway Endpoints (Zero Cost)</h4>

<p>AWS provides Gateway Endpoints for S3 and DynamoDB completely free of charge. There is zero hourly fee and zero data processing charge. Adding one updates your subnet route tables so traffic to S3 stays on the AWS private network:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"aws_vpc_endpoint"</span> <span class="s2">"s3"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span>            <span class="p">=</span> <span class="nx">aws_vpc</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">id</span>
  <span class="nx">service_name</span>      <span class="p">=</span> <span class="s2">"com.amazonaws.${var.region}.s3"</span>
  <span class="nx">vpc_endpoint_type</span> <span class="p">=</span> <span class="s2">"Gateway"</span>
  <span class="nx">route_table_ids</span>   <span class="p">=</span> <span class="nx">aws_route_table</span><span class="err">.</span><span class="nx">private</span><span class="p">[</span><span class="err">*</span><span class="p">]</span><span class="err">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If your applications read datasets, media, or run heavy data lifecycles against S3, this single block drops your NAT data volume immediately.</p>

<h4 id="2-interface-endpoints-run-the-math-first">2. Interface Endpoints (Run the Math First)</h4>

<p>For ECR, CloudWatch, and Secrets Manager, AWS offers Interface Endpoints (PrivateLink). These are <em>not</em> free - they cost around $0.01 per AZ per hour plus ~$0.01 per GB processed.</p>

<p>Do the break-even math before spinning them up: if a service processes 1 TB of image pulls a month, routing that through PrivateLink ($10) is vastly cheaper than routing it through a NAT Gateway ($45-$52). For low-traffic APIs, the idle hourly cost of the endpoint might exceed the NAT savings.</p>

<h4 id="3-image-hygiene-and-cross-az-hops">3. Image Hygiene and Cross-AZ Hops</h4>

<p>Shrink your container images and take advantage of ECS pull-through caches so tasks aren’t constantly pulling multi-gigabyte layers on deploy.</p>

<p>Additionally, watch your Availability Zone alignment. If a task in <code class="language-plaintext highlighter-rouge">eu-central-1a</code> routes through a NAT Gateway in <code class="language-plaintext highlighter-rouge">eu-central-1b</code>, you get billed for cross-AZ data transfer <em>and</em> NAT processing. AWS’s Regional NAT Gateway mode simplifies management across AZs, but remember that it still bills hourly for each AZ it covers - it is an operational abstraction, not a cost cut.</p>

<h3 id="make-egress-an-explicit-architecture-choice">Make Egress an Explicit Architecture Choice</h3>

<p>The only way to keep this spend under control permanently is to give network egress an owner:</p>

<ol>
  <li><strong>Tag the NAT and endpoints:</strong> Assign the cost explicitly to the core platform budget instead of leaving it in shared unallocated buckets.</li>
  <li><strong>Set a threshold alert:</strong> Add an AWS Budget alert specifically filtered on the <code class="language-plaintext highlighter-rouge">NatGateway-Bytes</code> usage type to catch rogue egress spikes before the invoice arrives.</li>
  <li><strong>Update your ADR template:</strong> Make “Network Egress Path” a required field in your Architecture Decision Records. Whenever an engineer introduces a service pulling high volumes of external or AWS data, they must document whether it routes via Gateway Endpoints, PrivateLink, or public subnets. This can be an overkill, especially depending on the team size and culture, so YMMV.</li>
</ol>]]></content><author><name>Dorrabb Khan</name></author><category term="aws" /><category term="finops" /><category term="networking" /><summary type="html"><![CDATA[NAT Gateway costs aren't a networking bug - they're an ownership failure. How to eliminate the AWS egress tax using free Gateway Endpoints, PrivateLink routing, and explicit ADR rules.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Running a DevOps career on cron jobs (and a part-time Master’s)</title><link href="https://dorrabbkhan.github.io/blog/2026/a-devops-career-on-cron-jobs-and-a-masters/" rel="alternate" type="text/html" title="Running a DevOps career on cron jobs (and a part-time Master’s)" /><published>2026-09-20T00:00:00+00:00</published><updated>2026-09-20T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/a-devops-career-on-cron-jobs-and-a-masters</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/a-devops-career-on-cron-jobs-and-a-masters/"><![CDATA[<p><strong>TL;DR:</strong> You can’t brute-force your way through a full-time senior engineering role and a graduate degree at the same time. If you try to live on adrenaline and late-night hustle, you will burn out. Surviving both comes down to clear boundaries, asynchronous communication, and knowing when to completely step away from the desk.</p>

<p>When I started balancing my full-time DevOps work with a part-time Master’s in AI, my immediate instinct was just to push harder. I figured I could tackle a full day of work and then easily grind through coursework late into the night. That strategy soon felt like hitting a wall. I had to stop winging my schedule and start treating my time and energy with real discipline.</p>

<h2 id="killing-the-instant-reply-habit">Killing the Instant-Reply Habit</h2>

<p>In operations, it’s easy to feel like you have to be responsive every second, and your day can easily fracture into dozens of tiny distractions. If you spend all day reacting to things as they pop up, you finish mentally exhausted, with your actual core tasks half-done. That inevitably forces your workday to bleed into your evenings.</p>

<p>I had to get comfortable checking Slack in deliberate blocks rather than watching it constantly. Most questions aren’t genuine production incidents; they are async conversations that can wait an hour or two. Giving yourself focused, uninterrupted stretches during the workday is the only way you get to log off on time without feeling guilty.</p>

<h2 id="weekends-deep-blocks-vs-total-disconnect">Weekends: Deep Blocks vs. Total Disconnect</h2>

<p>When you juggle work and school, the weekend is a trap. The instinct is to turn Saturday and Sunday into 12-hour cram sessions to catch up on everything you didn’t finish during the week. Do that twice, and Monday morning feels like a punishment!</p>

<p>Instead, I treat the weekend in two distinct modes. Saturday morning is reserved for a single deep-focus academic block—tackling math proofs or coding an assignment that requires three uninterrupted hours. By 13:00, the laptop closes.</p>

<p>The rest of the weekend is non-negotiable downtime. Whether that means going out, physical activity, or just unplugging completely, it forces my brain to drop the cognitive load entirely. If you don’t deliberately protect that space to reset, you start next week already running on empty.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="career" /><category term="devops" /><category term="productivity" /><summary type="html"><![CDATA[Balancing a full-time DevOps role with a part-time Master's in AI without burning out: deliberate boundaries, async communication, and protected downtime.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">High cardinality is the silent killer of observability budgets</title><link href="https://dorrabbkhan.github.io/blog/2026/high-cardinality-is-the-silent-killer-of-observability-budgets/" rel="alternate" type="text/html" title="High cardinality is the silent killer of observability budgets" /><published>2026-08-15T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/high-cardinality-is-the-silent-killer-of-observability-budgets</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/high-cardinality-is-the-silent-killer-of-observability-budgets/"><![CDATA[<p><strong>TL;DR:</strong> Everyone loves adding custom tags to their logs and metrics right up until the high cardinality of your labels multiplies into millions of billable time series. Fixing (and preventing) this requires strict engineering discipline.</p>

<p>The core issue with managed tools like DataDog isn’t always the base pricing—it’s how they calculate custom metrics. A single metric is defined by the unique combination of the metric name, tags, and host. This means cardinality is multiplicative. If you have 50 microservices and you decide to add a <code class="language-plaintext highlighter-rouge">request_id</code> tag to a core metric processing millions of requests, you haven’t just added a label; you’ve instantly created millions of unique, billable time series.</p>

<h2 id="the-forced-cleanup">The Forced Cleanup</h2>

<p>When I architected our migration away from DataDog to a custom Loki, Grafana, and Prometheus stack hosted on AWS, the goal was cost reduction. We ended up slashing our monthly observability costs by roughly 90% across our cloud and on-premise infrastructure. But looking back, the most valuable part of that migration wasn’t swapping the infrastructure itself - it was the forced cleanup of what we were actually indexing.</p>

<p>Self-hosting Prometheus forces you to confront your cardinality directly because if you don’t, your own instances will simply run out of memory. You realize very quickly that a massive portion of ingested series are never actually used in any dashboard or alert.</p>

<h2 id="dropping-the-weight-at-scrape-time">Dropping the Weight at Scrape Time</h2>

<p>To maintain that 90% cost reduction and keep our AWS backend running efficiently, we had to enforce strict lifecycle boundaries on our metrics.</p>

<p>Instead of ingesting every unique identifier, you drop high-churn, high-cardinality labels before they ever reach storage. In Prometheus, you handle this at scrape time using <code class="language-plaintext highlighter-rouge">metric_relabel_configs</code>.</p>

<p>Here is a realistic snippet of how you strip out expensive labels like <code class="language-plaintext highlighter-rouge">pod_name</code> or temporary revision hashes before they bloat your TSDB:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># prometheus.yml snippet</span>
<span class="na">scrape_configs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">kubernetes-pods"</span>
    <span class="na">kubernetes_sd_configs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">pod</span>

    <span class="c1"># Drop high-cardinality labels before ingestion to save memory and storage</span>
    <span class="na">metric_relabel_configs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">labeldrop</span>
        <span class="na">regex</span><span class="pi">:</span> <span class="s2">"</span><span class="s">pod_name|replicaset|controller_revision_hash|request_id"</span>

      <span class="c1"># Alternatively, drop entire metric families that nobody queries</span>
      <span class="pi">-</span> <span class="na">source_labels</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">__name__</span><span class="pi">]</span>
        <span class="na">regex</span><span class="pi">:</span> <span class="s2">"</span><span class="s">go_memstats_.*_bytes"</span>
        <span class="na">action</span><span class="pi">:</span> <span class="s">drop</span>
</code></pre></div></div>

<p>By explicitly dropping these labels, Prometheus aggregates the data, and your millions of unique series collapse back down into a manageable, queryable dataset. Observability should mean answering specific operational questions, not hoarding every possible data point.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="observability" /><category term="prometheus" /><category term="finops" /><summary type="html"><![CDATA[One innocent tag can explode into millions of billable time series. How dropping high-cardinality labels at scrape time can cut observability spend dramatically.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The silent productivity killer: Local dev environments</title><link href="https://dorrabbkhan.github.io/blog/2026/the-silent-productivity-killer-local-dev-environments/" rel="alternate" type="text/html" title="The silent productivity killer: Local dev environments" /><published>2026-07-08T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/the-silent-productivity-killer-local-dev-environments</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/the-silent-productivity-killer-local-dev-environments/"><![CDATA[<p><strong>TL;DR:</strong> If setting up a local dev environment requires a 30-page Confluence wiki and three days of debugging, your architecture is broken. The hidden tax of manual setups destroys developer momentum. Containerize everything, enforce reproducible environments, and treat Developer Experience (DevEx) as a core infrastructure metric.</p>

<p>We spend endless cycles optimizing CI/CD pipelines to shave off two minutes of build time, but we completely ignore the fact that engineers spend hours fighting their own laptops just to boot the stack. This is the hidden tax of bad developer platform engineering.</p>

<h2 id="the-compound-interest-of-fast-feedback">The Compound Interest of Fast Feedback</h2>

<p>Over time working in DevOps, it became blatantly obvious that raw cloud infrastructure efficiency means little if the local feedback loop is crawling. Every time a developer has to debug a local PostgreSQL version mismatch or untangle an undocumented environment variable, you lose momentum.</p>

<p>Cleaning up deployment platforms and standardizing the local stack is directly correlated with preserving developer sanity. A frictionless local setup compounds into massive productivity gains because developers are actually writing code, not playing sysadmin.</p>

<h2 id="containerizing-the-friction-away">Containerizing the Friction Away</h2>

<p>The solution is strict, reproducible environments. In my experience, migrating to fully containerized dev environments fundamentally changes the engineering culture. When we adopted <code class="language-plaintext highlighter-rouge">docker compose</code> dev setups, we managed to streamline onboarding and accelerate our deployment cycles significantly, just because “it works on my machine tho” stopped being a valid excuse.</p>

<p>Instead of a README bloated up with bash commands, a new engineer should just need this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># docker-compose.dev.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">api</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
      <span class="na">target</span><span class="pi">:</span> <span class="s">development</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">3000:3000"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./src:/app/src</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">NODE_ENV=development</span>
      <span class="pi">-</span> <span class="s">DATABASE_URL=postgres://user:pass@db:5432/localdb</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">db</span>

  <span class="na">db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:15-alpine</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">pgdata:/var/lib/postgresql/data</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">pgdata</span><span class="pi">:</span>
</code></pre></div></div>

<p>One <code class="language-plaintext highlighter-rouge">docker compose up</code> and the entire stack is ready.</p>

<h2 id="tools-to-fix-your-devex">Tools to Fix Your DevEx</h2>

<p>If you want to strip the friction out of your local setups, here are the tools actually worth your time:</p>

<ul>
  <li><strong>Docker Compose:</strong> The reliable baseline. Perfect for orchestrating databases, caching layers, and APIs in a unified local network.</li>
  <li><strong>Devcontainers:</strong> If your team lives in VS Code, <code class="language-plaintext highlighter-rouge">devcontainer.json</code> is basically magic. It spins up a container and mounts your editor directly inside it, pre-installing all the exact extensions, linters, and runtimes the project needs.</li>
  <li><strong>Devbox / Nix + direnv:</strong> If Docker overhead is too heavy for your machine, Devbox (powered by Nix) gives you isolated, completely reproducible local shell environments without the virtualization tax. Pair it with <code class="language-plaintext highlighter-rouge">direnv</code> to auto-load environment variables the second you <code class="language-plaintext highlighter-rouge">cd</code> into a directory.</li>
</ul>

<p>Treat your local dev environments like production infrastructure. Automate it, document it, and get out of the developers’ way.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="devex" /><category term="docker" /><summary type="html"><![CDATA[If onboarding takes a 30-page wiki and three days of debugging, your local setup is the real bottleneck. Containerize everything and treat DevEx as a core infrastructure metric.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rust for the Python dev: shifting your mental model</title><link href="https://dorrabbkhan.github.io/blog/2026/rust-for-the-python-dev/" rel="alternate" type="text/html" title="Rust for the Python dev: shifting your mental model" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/rust-for-the-python-dev</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/rust-for-the-python-dev/"><![CDATA[<p><strong>TL;DR:</strong> Moving from Python to Rust requires a complete rewiring of how you think about memory, concurrency, and error handling. The real benefit of Rust in cloud infrastructure is not only raw speed, but the absolute predictability of a strict, compiler-driven architecture.</p>

<p>Moving from Python to Rust feels less like learning a new language and more like unlearning bad habits. You don’t just swap out <code class="language-plaintext highlighter-rouge">def</code> for <code class="language-plaintext highlighter-rouge">fn</code> and carry on. You have to fundamentally change how you reason about memory boundaries, state mutations, and what actually constitutes a “safe” concurrent operation.</p>

<p>When I was working on migrating a legacy codebase from Python 2.7 to modern Python 3.8, the mental model was entirely runtime-dependent. You write the logic, write the tests, push to staging, and inevitably discover a handful of unhandled errors that slipped through.</p>

<p>Contrast that with my experience engineering high-throughput microservices from scratch with Rust. The strict, compiler-driven discipline of Rust forces you to confront every possible failure state before the code even compiles. It is a massive hurdle at first, but it completely changes your operational baseline long-term.</p>

<h2 id="beyond-raw-speed">Beyond Raw Speed</h2>

<p>We all know Rust is incredibly fast. But when you are designing systems, raw compute speed is only one piece of the puzzle. The real game-changers for high-throughput cloud messaging are memory safety and predictable concurrency.</p>

<p>In Python, spinning up a thread pool to process an SQS queue is syntactically easy, but ensuring thread-safe state sharing is a constant battle against the Global Interpreter Lock and subtle race conditions. In Rust, if your concurrency model is flawed, the compiler simply refuses to build it.</p>

<h2 id="the-compile-time-shift">The Compile-Time Shift</h2>

<p>Let’s look at a minimal example of polling an AWS SQS queue. In a traditional Python approach, you might wrap a <code class="language-plaintext highlighter-rouge">boto3</code> call in a broad <code class="language-plaintext highlighter-rouge">try/except</code> block. Rust forces a much safer paradigm:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">aws_sdk_sqs</span><span class="p">::{</span><span class="n">Client</span><span class="p">,</span> <span class="n">Error</span><span class="p">};</span>

<span class="k">async</span> <span class="k">fn</span> <span class="nf">poll_sqs_queue</span><span class="p">(</span><span class="n">client</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Client</span><span class="p">,</span> <span class="n">queue_url</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">Error</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">receive_message_output</span> <span class="o">=</span> <span class="n">client</span>
        <span class="nf">.receive_message</span><span class="p">()</span>
        <span class="nf">.queue_url</span><span class="p">(</span><span class="n">queue_url</span><span class="p">)</span>
        <span class="nf">.max_number_of_messages</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
        <span class="nf">.send</span><span class="p">()</span>
        <span class="k">.await</span><span class="o">?</span><span class="p">;</span> <span class="c1">// The '?' forces explicit error propagation</span>

    <span class="c1">// messages() returns a slice in aws-sdk-sqs 1.x, so we check it directly.</span>
    <span class="k">if</span> <span class="n">receive_message_output</span><span class="nf">.messages</span><span class="p">()</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
        <span class="nd">println!</span><span class="p">(</span><span class="s">"Queue empty, gracefully backing off."</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">msg</span> <span class="k">in</span> <span class="n">receive_message_output</span><span class="nf">.messages</span><span class="p">()</span> <span class="p">{</span>
            <span class="c1">// Rust forces us to handle the Option explicitly. No implicit NoneTypes.</span>
            <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="o">=</span> <span class="n">msg</span><span class="nf">.body</span><span class="p">()</span> <span class="p">{</span>
                <span class="nd">println!</span><span class="p">(</span><span class="s">"Processing message payload: {}"</span><span class="p">,</span> <span class="n">body</span><span class="p">);</span>
                <span class="c1">// Pipeline logic goes here</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Notice the heavy reliance on <code class="language-plaintext highlighter-rouge">Result</code> and <code class="language-plaintext highlighter-rouge">Option</code> types. There are no null pointers or runtime exceptions lurking in the pipeline. You are forced to explicitly acknowledge that the queue might be empty, that a message body might not exist, or that the network call might fail.</p>

<p>This shift from runtime hope to compile-time guarantees is a steep hill to climb. But once you rewire your mental model to satisfy the borrow checker, the anxiety of deploying asynchronous cloud infrastructure virtually disappears.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="rust" /><category term="python" /><category term="aws" /><summary type="html"><![CDATA[Moving from Python to Rust means unlearning runtime habits - the compiler forces explicit error handling and memory safety, buying you predictable, high-throughput infrastructure code.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">FinOps is an architectural constraint, not an accounting problem</title><link href="https://dorrabbkhan.github.io/blog/2026/finops-is-an-architectural-constraint-not-an-accounting-problem/" rel="alternate" type="text/html" title="FinOps is an architectural constraint, not an accounting problem" /><published>2026-05-02T00:00:00+00:00</published><updated>2026-05-02T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/finops-is-an-architectural-constraint-not-an-accounting-problem</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/finops-is-an-architectural-constraint-not-an-accounting-problem/"><![CDATA[<p><strong>TL;DR:</strong> We need to treat cost the same way we treat latency or availability: as a strict architectural constraint. Relying on finance to flag an overgrown AWS bill is an anti-pattern. Real FinOps means baking automated teardowns, lifecycle policies, and rightsizing directly into your IaC.</p>

<p>Developers treat the AWS bill like a problem for the finance team, right up until a rogue Lambda function burns through the quarterly budget in a single weekend. The usual response is a panicked Slack thread, a quick patch, and a vague promise to “keep an eye on costs.”</p>

<p>That approach is fundamentally broken. You wouldn’t push code that degrades system latency by three seconds and tell the product team you’ll simply keep an eye on it. Cost shouldn’t be treated any differently. Shifting the conversation from “how to save money” to “how to build cost-aware systems” fundamentally changes the way you design architecture.</p>

<h2 id="baking-cost-into-the-architecture">Baking Cost into the Architecture</h2>

<p>In my experience with AWS and Terraform, looking at infrastructure through this exact lens is how I managed to slash our core AWS infrastructure spend by 73%. When scaling the AWS backend architecture for our SaaS at adnymics, I realized that meaningful cost reduction was about enforcing strict lifecycle boundaries at the code level.</p>

<p>We executed comprehensive FinOps strategies by treating EC2 and RDS rightsizing and S3 lifecycle policies as core engineering patterns. If an asset doesn’t need to live in standard storage indefinitely, its transition to colder storage should be codified on day 1.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Treating cost as a constraint: automated data archiving</span>
<span class="nx">resource</span> <span class="s2">"aws_s3_bucket_lifecycle_configuration"</span> <span class="s2">"cost_aware_storage"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="err">.</span><span class="nx">app_data</span><span class="err">.</span><span class="nx">id</span>

  <span class="nx">rule</span> <span class="p">{</span>
    <span class="nx">id</span>     <span class="p">=</span> <span class="s2">"archive-stale-data"</span>
    <span class="nx">status</span> <span class="p">=</span> <span class="s2">"Enabled"</span>

    <span class="nx">transition</span> <span class="p">{</span>
      <span class="nx">days</span>          <span class="p">=</span> <span class="mi">30</span>
      <span class="nx">storage_class</span> <span class="p">=</span> <span class="s2">"GLACIER"</span>
    <span class="p">}</span>

    <span class="nx">expiration</span> <span class="p">{</span>
      <span class="nx">days</span> <span class="p">=</span> <span class="mi">365</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Beyond storage, compute waste is almost always a symptom of operational friction. We implemented automated test-environment teardowns to guarantee that ephemeral resources actually stayed ephemeral. When you bake these constraints into your environments directly, cost management becomes an invisible byproduct of good engineering.</p>

<p>Treat your budget like an SLA. If you architect for it up front, the finance team will never ping you about a spiked cloud bill.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="finops" /><category term="aws" /><category term="terraform" /><summary type="html"><![CDATA[Treat cloud cost like latency or availability - a strict architectural constraint baked into IaC with lifecycle policies and automated teardowns, not an afterthought for the finance team.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Applying system architecture to human problems</title><link href="https://dorrabbkhan.github.io/blog/2026/applying-systems-architecture-to-human-problems/" rel="alternate" type="text/html" title="Applying system architecture to human problems" /><published>2026-04-11T00:00:00+00:00</published><updated>2026-04-11T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/applying-systems-architecture-to-human-problems</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/applying-systems-architecture-to-human-problems/"><![CDATA[<p><strong>TL;DR:</strong> Designing highly available cloud systems and managing human communities rely on the exact same principles. Scaling an application to handle millions of requests is fundamentally similar to scaling a mentorship program: you need robust pipelines, clear documentation, and a way to load-balance your own availability.</p>

<p>Designing cloud-native backends teaches you a lot about fault tolerance and resource allocation. But ironically, the hardest scalability lessons I’ve learned didn’t come from configuring AWS - they came from trying to scale my own bandwidth as a mentor and community leader.</p>

<p>When I was designing containerized dev environments, the goal was strictly operational: reduce the local onboarding setup time for new engineers. Recently, I realized this exact platform engineering principle applies perfectly to navigating human bottlenecks.</p>

<h2 id="platform-engineering-for-human-friction">Platform Engineering for Human Friction</h2>

<p>In 2025, I founded Ilm Abroad Pakistan, an educational project to help students navigate the complex university and visa application processes for Germany. At first, I was doing what every junior engineer does with a new server: manually provisioning everything. I answered the same visa questions and reviewed the same application formatting over and over.</p>

<p>It didn’t scale. The manual onboarding was causing dropped packets (missed emails) and latency (delayed responses).</p>

<p>To fix it, I had to treat the mentorship program like developer platform engineering. I built out structured documentation and standardized “runbooks” for the students. Just like a good CI/CD pipeline removes friction from shipping code, creating self-serve guides removed the friction from the university application process.</p>

<h2 id="load-balancing-human-availability">Load Balancing Human Availability</h2>

<p>Leading an 80+ member Google Developer Student Club during my Bachelor’s taught me that “human availability” is a finite, easily exhausted resource.</p>

<p>If you route all incoming traffic to a single node without a load balancer, that node will eventually crash. The same thing happens when every single community question, technical blocker, or event planning detail is routed directly to you. To maintain high availability in a community, you have to architect away single points of failure.</p>

<p>If I were to define this community management approach as infrastructure-as-code, it would look something like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Mentorship Request Routing Pipeline</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">human-load-balancer</span>
<span class="na">on</span><span class="pi">:</span>
  <span class="na">incoming_request</span><span class="pi">:</span>
    <span class="na">types</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">visa_help</span><span class="pi">,</span> <span class="nv">tech_blocker</span><span class="pi">,</span> <span class="nv">general_advice</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">route_traffic</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">community-cluster</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check Documentation Cache</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if request in standard_faqs:</span>
             <span class="s">return docs.ilmAbroad.url</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Delegate to Peer Network</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">cache_miss == </span><span class="no">true</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">assign_to(available_senior_students)</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Escalate to Founder</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">complexity &gt; threshold</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">alert(dorrabb)</span>
          <span class="s">schedule_async_meeting()</span>
</code></pre></div></div>

<p>By caching the common answers (documentation) and delegating the initial routing (peer network), you reserve your active processing power for the highly complex edge cases that actually require your intervention. When you start viewing human burnout as a failure to implement proper queueing, the solutions become a lot more obvious.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="leadership" /><category term="mentorship" /><category term="devops" /><summary type="html"><![CDATA[Scaling a mentorship program follows the same rules as scaling cloud infrastructure: cache common answers, load-balance your availability, and architect away single points of failure.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">If you can’t explain it to the Board, it won’t ship</title><link href="https://dorrabbkhan.github.io/blog/2026/if-you-cant-explain-it-to-the-board-it-wont-ship/" rel="alternate" type="text/html" title="If you can’t explain it to the Board, it won’t ship" /><published>2026-03-18T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/if-you-cant-explain-it-to-the-board-it-wont-ship</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/if-you-cant-explain-it-to-the-board-it-wont-ship/"><![CDATA[<p><strong>TL;DR:</strong> The hardest part of an engineering role isn’t writing the code; it’s translating technical trade-offs to people who only care about the bottom line. You can design the most elegant system in the world, but it won’t ship if you can’t get stakeholder buy-in. It is crucial to have a framework for translating complex cloud infrastructure changes that bridge the gap between engineering and the business.</p>

<p>We spend years mastering our tech stack, but almost zero time learning how to talk about it. When you transition into a senior role, the bottleneck stops being your ability to write a Terraform module. The real bottleneck becomes your ability to explain <em>why</em> that module is necessary to a stakeholder who just wants the feature shipped, or a Board staring at a massive cloud bill.</p>

<h2 id="the-translation-framework">The Translation Framework</h2>

<p>When you talk to non-technical stakeholders, you have to completely drop the engineering jargon and focus exclusively on business impact: cost, risk, and speed.</p>

<p>For example, when I architected the migration of our observability stack away from DataDog to a custom Loki, Grafana, and Prometheus setup on AWS, I didn’t pitch the stakeholders on the technical elegance of PromQL. I pitched them on the fact that this move would slash our monthly observability costs by up to 90%.</p>

<p>To win technical arguments, frame them as business solutions. If you want to spend two sprints paying down technical debt, explain how it directly translates to faster feature delivery or a reduction in SLA breaches next quarter.</p>

<p>Over time, I also noticed that metaphors do a lot of the heavy lifting in these conversations. Stakeholders don’t need the precise mechanics; they need a mental model they can reason with. A well-chosen metaphor hands them that model in seconds, and once they have it, all the relevant technical details you present actually stick.</p>

<h2 id="a-blueprint-for-business-driven-adrs">A blueprint for business-driven ADRs</h2>

<p>A good Architecture Decision Record (ADR) shouldn’t just be a log of what the engineering team decided to do. It needs to be a document that translates complex technical reality into clear business context.</p>

<p>Here is the straightforward ADR format I would recommend to ensure both developers and the business are aligned:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># ADR [004]: Migrating Observability Stack to Loki/Prometheus</span>

<span class="gs">**1. The Business Problem**</span>
Our current SaaS monitoring costs are scaling linearly with our traffic, currently eating up X% of our monthly infrastructure budget and hurting product margins.

<span class="gs">**2. The Technical Decision**</span>
We will migrate our primary observability pipeline from DataDog to a self-hosted Loki, Grafana, and Prometheus stack on AWS.

<span class="gs">**3. The Bottom-Line Impact**</span>
<span class="p">
-</span> <span class="gs">**Pros:**</span> Projected 90% reduction in monthly observability costs. Total control over data retention policies.
<span class="p">-</span> <span class="gs">**Cons (Trade-offs):**</span> Requires approximately 4 weeks of dedicated engineering time to build. Introduces minor ongoing maintenance overhead for the DevOps team.

<span class="gs">**4. The Rollout Strategy**</span>
Run both systems in parallel for 14 days to validate metrics, then tear down the legacy system.
</code></pre></div></div>

<p>By explicitly listing the business problem first and clearly defining the trade-offs (including engineering time), you eliminate the friction between engineering and management. You give the board exactly what they need to make an informed decision, and you give your team the air cover they need to actually do the work.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="leadership" /><category term="career" /><category term="communication" /><summary type="html"><![CDATA[The hardest part of senior engineering isn't the code - it's translating technical trade-offs into cost, risk, and speed for stakeholders. A framework and ADR blueprint for winning buy-in.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">You probably don’t need Kubernetes (yet)</title><link href="https://dorrabbkhan.github.io/blog/2026/you-probably-dont-need-kubernetes-yet/" rel="alternate" type="text/html" title="You probably don’t need Kubernetes (yet)" /><published>2026-02-27T00:00:00+00:00</published><updated>2026-02-27T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/you-probably-dont-need-kubernetes-yet</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/you-probably-dont-need-kubernetes-yet/"><![CDATA[<p><strong>TL;DR:</strong> We’re conditioned to architect for Google-scale on day one, which usually just leads to massive compute waste. You can serve millions of requests and maintain strict SLAs using a “boring” stack like managed containers, only adopting Kubernetes when you hit genuine breaking points.</p>

<p>Every time a new project spins up, someone reaches for a Helm chart. We’ve convinced ourselves that if we aren’t running a multi-node Kubernetes cluster from day one, we’re doing it wrong. The reality is that prematurely optimizing for scale just results in a bloated cloud bill and an unnecessarily complex deployment pipeline.</p>

<h2 id="the-power-of-boring-architecture">The Power of “Boring” Architecture</h2>

<p>There is massive value in pushing simple microservice limits before introducing heavy orchestration. Boring architecture is predictable and doesn’t require a dedicated team to troubleshoot at 2 AM.</p>

<p>In my experience with ECS, you get the vast majority of container orchestration benefits without managing a control plane. Infrastructure spend is healthy, and deployment speed is vastly superior using straightforward CI/CD pipelines and Docker/Ansible for containerized dev environments.</p>

<p>Here is a simplified look at the kind of “boring” configuration that handles serious production traffic without a single <code class="language-plaintext highlighter-rouge">kubectl</code> command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> [Amazon Route 53]
              |
              v
 [Application Load Balancer]
              |
              v
     [AWS ECS (Fargate)]
   (Stateless API Containers)
       /             \
      v               v
 [Amazon RDS]      [Amazon S3]
 (Database)      (Static Assets)
</code></pre></div></div>

<p>With a setup like this, upgrading a node pool simply doesn’t exist. You push code, your pipeline triggers a rolling update, and traffic shifts gracefully.</p>

<h2 id="finding-the-real-tipping-point">Finding the Real Tipping Point</h2>

<p>So when do you actually need Kubernetes?</p>

<p>You need it when the “boring” stack genuinely breaks. If your service discovery between dozens of microservices has become an unmanageable web, it might be time.</p>

<p>Until you hit those very specific operational walls, stick to the simple stuff. Lean on managed services to do the heavy lifting. Give yourself a break on the operational overhead, and spend that engineering time actually building your product.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="kubernetes" /><category term="aws" /><category term="devops" /><summary type="html"><![CDATA[We're conditioned to architect for Google-scale on day one - but a boring stack of managed containers serves millions of requests. Adopt Kubernetes only when you hit real breaking points.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building my site with Jekyll</title><link href="https://dorrabbkhan.github.io/blog/2026/building-my-site-with-jekyll/" rel="alternate" type="text/html" title="Building my site with Jekyll" /><published>2026-02-03T00:00:00+00:00</published><updated>2026-02-03T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/building-my-site-with-jekyll</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/building-my-site-with-jekyll/"><![CDATA[<p><strong>TL;DR:</strong> I skipped the complex cloud-native stack and built my blog with Jekyll and GitHub Pages. Zero hosting costs, zero database maintenance, and a frictionless Markdown workflow. The urge to overengineer was incredibly strong, but instead, I went with Jekyll. Here’s why.</p>

<p>Jekyll is a static site generator. There’s no database to patch, no server to maintain, and zero hosting costs via GitHub Pages. It’s entirely decoupled. I also wanted a frictionless developer experience for my writing, which is achieved completely via this setup.</p>

<p>My blog workflow relies entirely on standard Markdown front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Keeping</span><span class="nv"> </span><span class="s">it</span><span class="nv"> </span><span class="s">Simple:</span><span class="nv"> </span><span class="s">Jekyll</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">DevOps</span><span class="nv"> </span><span class="s">Blog"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-02-03</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>I write a post, run <code class="language-plaintext highlighter-rouge">git push</code>, and GitHub Pages handles the rest.</p>

<p>Give yourself a break on your personal projects. Sometimes a folder full of Markdown files is all you need.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="jekyll" /><category term="blogging" /><summary type="html"><![CDATA[Skipping the complex cloud-native stack and building a blog with Jekyll and GitHub Pages: zero hosting costs, zero databases, and a frictionless Markdown workflow.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Setting the bar low: One post a month in 2026</title><link href="https://dorrabbkhan.github.io/blog/2026/one-post-a-month-for-2026/" rel="alternate" type="text/html" title="Setting the bar low: One post a month in 2026" /><published>2026-01-09T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://dorrabbkhan.github.io/blog/2026/one-post-a-month-for-2026</id><content type="html" xml:base="https://dorrabbkhan.github.io/blog/2026/one-post-a-month-for-2026/"><![CDATA[<p><strong>TL;DR:</strong> I have a bad habit of hoarding half-written technical notes that never get published because they don’t feel “complete” enough. For 2026, the goal is simple: publish one post every month. Not necessarily 3,000-word manifestos every time. Just real notes from my experiences - whatever I’m building, breaking, or learning.</p>

<p>Twelve posts by the end of December. Let’s see if keeping it simple actually keeps my blog active.</p>]]></content><author><name>Dorrabb Khan</name></author><category term="writing" /><category term="blogging" /><summary type="html"><![CDATA[Ditching the habit of hoarding half-written notes: a resolution to publish one real post every month in 2026.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dorrabbkhan.github.io/assets/images/og-default.png" /><media:content medium="image" url="https://dorrabbkhan.github.io/assets/images/og-default.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>