<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Haripriya Veluchamy</title>
    <description>The latest articles on DEV Community by Haripriya Veluchamy (@techwithhari).</description>
    <link>https://dev.to/techwithhari</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1914764%2Fbc8a04cf-4e71-485f-8880-5b49f05c9560.png</url>
      <title>DEV Community: Haripriya Veluchamy</title>
      <link>https://dev.to/techwithhari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techwithhari"/>
    <language>en</language>
    <item>
      <title>One Lookup Table Turned a Risky Server Migration Into a One-Line Change</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 18 Aug 2026 16:28:54 +0000</pubDate>
      <link>https://dev.to/techwithhari/one-lookup-table-turned-a-risky-server-migration-into-a-one-line-change-2mf8</link>
      <guid>https://dev.to/techwithhari/one-lookup-table-turned-a-risky-server-migration-into-a-one-line-change-2mf8</guid>
      <description>&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I was running a system where multiple backend servers each served a set of users, and a routing layer decided which server a given user's requests should go to. Nothing unusual  this is a common shape for anything that scales past one machine.&lt;/p&gt;

&lt;p&gt;The question that turned out to matter a lot more than I expected: when something in the routing layer refers to "the server," what is it actually storing? A real address? Or a name that gets looked up?&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it mattered
&lt;/h2&gt;

&lt;p&gt;At some point I needed to move a group of users off one server and onto another  the kind of thing that happens for all sorts of reasons: retiring old hardware, rebalancing load, recovering from an incident.&lt;/p&gt;

&lt;p&gt;Before touching anything, I checked how the routing data was actually structured. It turned out every record referenced servers by a stable label  not a raw address. The label was just an ID; a separate lookup table mapped each label to wherever that server actually lived right now.&lt;/p&gt;

&lt;p&gt;That one fact changed the entire migration from "rewrite a bunch of records for every affected user" into "update one row in the lookup table." The routing records themselves never needed to change. Every user still pointed at the same label  the label's &lt;em&gt;meaning&lt;/em&gt; just changed.&lt;/p&gt;

&lt;p&gt;I verified it worked by checking the routing behavior for an affected user right after the update, before assuming anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, generalized
&lt;/h2&gt;

&lt;p&gt;This is the indirection pattern, and it's worth naming explicitly because it's easy to skip when you're building the first version of something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct reference:&lt;/strong&gt; Store the real, resolvable thing (an IP address, a file path, a specific resource ID) everywhere it's used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indirect reference:&lt;/strong&gt; Store a stable name/label everywhere it's used, and keep exactly one place that resolves that label to the real thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Direct references feel simpler at first  there's no lookup step, nothing extra to maintain. But that simplicity is a trap: the real address ends up copied into every place that uses it, and moving the real thing means finding and updating every one of those places.&lt;/p&gt;

&lt;p&gt;Indirect references cost you one extra lookup table. In exchange, moving the real thing becomes a single update, in a single place, and everything downstream is unaffected because none of it ever knew the real address to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this shows up beyond servers
&lt;/h2&gt;

&lt;p&gt;The same trade-off appears constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS names vs. hardcoded IPs&lt;/strong&gt;  this is the same pattern at internet scale. Change the DNS record, nothing downstream needs to know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags referencing a config key vs. a hardcoded value&lt;/strong&gt;  the flag's &lt;em&gt;name&lt;/em&gt; stays stable while what it resolves to changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database foreign keys vs. duplicating a denormalized value everywhere&lt;/strong&gt;  the ID stays stable; look up the current details when you need them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service discovery vs. hardcoded service addresses&lt;/strong&gt;  exactly the migration scenario above, generalized to any service-to-service call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every case, the question is the same: if the "real thing" changes, how many places need to be touched? If the answer is "more than one," you're using a direct reference somewhere it should have been indirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;Before building the first version of anything that will eventually need to move, scale, or be replaced  a server, a config value, a downstream dependency  decide up front whether callers will reference it directly or through a label that gets resolved. It's a small design decision early on, and it's the single biggest factor in whether a future migration is a five-minute change or a project.&lt;/p&gt;

&lt;p&gt;If you're not sure whether you have this in place already, ask the question I asked before touching anything: what is actually stored in the place that decides where a request goes  a real address, or a name? If it's a real address, that's worth fixing before you need to move it under pressure.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>softwareengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Multi region deployment with Terraform modules</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:59:01 +0000</pubDate>
      <link>https://dev.to/techwithhari/multi-region-deployment-with-terraform-modules-2jok</link>
      <guid>https://dev.to/techwithhari/multi-region-deployment-with-terraform-modules-2jok</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I was managing infrastructure across multiple regions  a handful of virtual machines, one per region, each running the same service. The Terraform setup had grown the way these things usually do: when I needed a VM in a new region, I copied the folder for an existing region, renamed a few things, and adjusted the values that were different (location, network ranges, naming).&lt;/p&gt;

&lt;p&gt;It worked. Until it didn't.&lt;/p&gt;

&lt;p&gt;Every region had its own full copy of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The provider block&lt;/li&gt;
&lt;li&gt;The resource definitions (VM, network interface, disk, public IP, etc.)&lt;/li&gt;
&lt;li&gt;The output block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three regions in, I had three nearly-identical folders, each a few hundred lines, differing in maybe ten lines of actual content. Any time I wanted to change something structural  add a tag, adjust a disk size, fix a naming convention  I had to make that change in every single folder, and hope I didn't miss one or introduce a subtle inconsistency between them.&lt;/p&gt;

&lt;p&gt;This is the classic copy-paste infrastructure trap: it feels fast in the moment (just copy the folder!) but every region you add makes the &lt;em&gt;next&lt;/em&gt; change more expensive, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: separate "what's shared" from "what's different"
&lt;/h2&gt;

&lt;p&gt;The fix was to stop treating each region as its own Terraform project and instead treat the &lt;em&gt;infrastructure definition&lt;/em&gt; as shared code, with only the differences expressed as data.&lt;/p&gt;

&lt;p&gt;Concretely, that meant three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A shared module&lt;/strong&gt;  one Terraform module containing the actual resource definitions (VM, networking, disk, etc.), written with input variables for everything that varies between regions: location, name suffix, network ranges, and so on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. One shared entry point&lt;/strong&gt;  a single root configuration that calls the module, rather than one root configuration per region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A small config file per region&lt;/strong&gt;  instead of a full copy of the module, each region gets a short file (five or six lines) specifying just its own values: which cloud location to deploy into, what to name things, what network ranges to use.&lt;/p&gt;

&lt;p&gt;Adding a new region now means adding one small config file. No new Terraform resource code, no new pipeline definition, nothing to copy and rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond "less typing"
&lt;/h2&gt;

&lt;p&gt;The obvious win is less duplication. The less obvious win is &lt;em&gt;correctness&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A structural fix only needs to happen once.&lt;/strong&gt; Change the module, every region picks it up. Before, a structural change meant editing N folders and trusting yourself to get all N edits identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift between regions becomes visible.&lt;/strong&gt; With copy-pasted folders, two regions can quietly diverge over time as one gets a fix the other doesn't. With a shared module, divergence can only happen in the small config file  which is short enough to diff at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review gets easier.&lt;/strong&gt; A pull request adding a new region is now a five-line diff to a new config file, not a few hundred lines of near-duplicate resource code that's hard to review carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I kept separate on purpose
&lt;/h2&gt;

&lt;p&gt;One thing worth calling out: sharing the &lt;em&gt;code&lt;/em&gt; doesn't mean sharing everything. Each region still keeps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own Terraform state file, so a mistake applying to one region can't touch another region's resources.&lt;/li&gt;
&lt;li&gt;Its own deployment approval gate in the pipeline, so promoting a change to one region doesn't silently promote it everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is "share the &lt;em&gt;how&lt;/em&gt;, isolate the &lt;em&gt;where it lands&lt;/em&gt;." Sharing a module is safe. Sharing a blast radius is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;If you find yourself about to copy a folder to stand up a new instance of something  a new region, a new environment, a new tenant  that's usually the signal to stop and ask: what's actually different here? Usually the answer is "not much"  a handful of values. Everything else can be a shared module, parameterized by those values.&lt;/p&gt;

&lt;p&gt;Copy-paste feels like the fast path. It's fast for the &lt;em&gt;first&lt;/em&gt; copy. It gets slower and riskier every time after that.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>cloud</category>
      <category>devops</category>
      <category>aws</category>
    </item>
    <item>
      <title>The App Needed a Real Desktop, Not Just a Real Windows Machine</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:37:06 +0000</pubDate>
      <link>https://dev.to/techwithhari/the-app-needed-a-real-desktop-not-just-a-real-windows-machine-284b</link>
      <guid>https://dev.to/techwithhari/the-app-needed-a-real-desktop-not-just-a-real-windows-machine-284b</guid>
      <description>&lt;p&gt;The App Needed a Real Desktop, Not Just a Real Windows Machine&lt;/p&gt;

&lt;p&gt;Before any of the debugging stories, gotchas, or architecture arguments in this series existed, there was one earlier decision that had to fail first  and it's the one that actually explains why everything after it looks the way it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan that looked right on paper
&lt;/h2&gt;

&lt;p&gt;The system in question wraps a Windows-only, GUI-first desktop application  the kind of software that ships as a &lt;code&gt;.exe&lt;/code&gt;, expects to be installed via a wizard, and exposes automation only through a first-party SDK that talks to the running application process. No web API, no headless mode, nothing designed for servers at all.&lt;/p&gt;

&lt;p&gt;The instinct, reasonably, was: don't fight that. Run it inside a Windows compatibility layer, in a Linux container. Keeps everything in the same containerized, cloud-native tooling as the rest of the stack. Cheaper to host. Fits the mental model everything else in the system already used.&lt;/p&gt;

&lt;p&gt;It did not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "did not work" actually looked like
&lt;/h2&gt;

&lt;p&gt;Not a clean, informative failure  a slow, grinding one. The automation layer would intermittently fail to connect to the running application with low-level timeout errors, with no consistent trigger. Sometimes it worked for hours. Sometimes it failed within minutes of a restart. The compatibility layer itself would occasionally become unstable in ways that were hard to attribute to any single cause. Every fix felt like it addressed a symptom, and a slightly different symptom would show up a few days later.&lt;/p&gt;

&lt;p&gt;Eventually the right call was: stop trying to make this specific approach reliable, and question whether the approach itself was ever going to be reliable  rather than continuing to patch a foundation that might be structurally wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pivot, and the deeper thing it revealed
&lt;/h2&gt;

&lt;p&gt;The fix was to abandon the compatibility-layer approach entirely and run the application on an actual Windows machine, natively. This alone mostly solved it  but "mostly" is doing real work in that sentence, because it surfaced a second, more specific requirement that the first failure had been obscuring the whole time.&lt;/p&gt;

&lt;p&gt;Running on real Windows wasn't sufficient by itself. The application's automation layer would still fail to connect if the underlying process was running in a &lt;strong&gt;headless or service-style context&lt;/strong&gt;  even on genuine Windows, even with no compatibility layer involved at all. It only worked reliably when the process was running inside a real, &lt;strong&gt;interactive, logged-in desktop session&lt;/strong&gt;  the same kind of session that exists when an actual person is sitting at the keyboard, not a background service context that happens to also be "on Windows."&lt;/p&gt;

&lt;p&gt;This turned out to be the actual root requirement the whole time. The compatibility-layer approach hadn't just been unstable in some vague sense  it was fundamentally incapable of providing this, no matter how much it was tuned, because a compatibility layer running headless in a container was never going to look like an interactive desktop session to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn't just a trading-software quirk
&lt;/h2&gt;

&lt;p&gt;I think this generalizes further than it looks. A meaningful category of Windows software  licensed engineering tools, certain legacy financial or back-office systems, some CAD and design applications  was written assuming a human is logged in: window handles exist, GUI event loops are running, certain OS-level session facilities are available that simply don't exist in a headless context. None of that is a bug in the software. It's just an assumption baked in from an era when "run this on a server" wasn't a use case anyone designing it considered.&lt;/p&gt;

&lt;p&gt;If you're trying to automate something in this category in the cloud, "get it running on Windows" is necessary but not sufficient. The actual question to ask early  ideally before building anything around it  is: &lt;strong&gt;does this specific piece of software's automation surface require a real interactive session, or does it genuinely work headless?&lt;/strong&gt; That's usually one focused test, and it's a much cheaper question to answer on day one than to discover, the way this system did, after weeks of chasing intermittent failures in the wrong layer.&lt;/p&gt;

&lt;p&gt;Once that requirement was understood clearly, the actual infrastructure need became concrete and solvable: real auto-logon configuration, launching the process specifically as an interactive session rather than a generic service, and  importantly  verifying &lt;em&gt;which kind&lt;/em&gt; of session a running process is actually in as a first-class health check, not just checking whether the process exists at all. "The process is running" and "the process is running somewhere it can actually work" turned out to be two different, both necessary, checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ties this back to everything else in this series
&lt;/h2&gt;

&lt;p&gt;Looking back across the debugging stories, the audit findings, and the architecture arguments this series has covered, I think they're all really one lesson wearing different clothes: &lt;strong&gt;the constraint that looks like an inconvenient afterthought is often the actual load-bearing wall.&lt;/strong&gt; A packaging policy decision that seems like a small CI annoyance. A "designed to fail" test that quietly carries an unrelated assumption about safety. A vault that's been silently unreachable long enough that nobody remembers assuming it worked. A scaling pattern that's right for most systems and simply wrong for this one. And here, at the root of all of it: an assumption that "a real Windows machine" and "a real interactive desktop session" were the same thing, when they were never quite the same thing at all.&lt;/p&gt;

&lt;p&gt;None of these were exotic problems. They were all findable early, cheaply, by asking one honest, specific question before building  rather than discovering the honest answer later, expensively, after something was already built on top of the wrong assumption.&lt;/p&gt;

</description>
      <category>virtualmachine</category>
      <category>cloud</category>
      <category>devops</category>
      <category>azure</category>
    </item>
    <item>
      <title>Autoscaling Doesn't Fit Every Workload Here's How to Tell, and What to Build Instead</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:06:37 +0000</pubDate>
      <link>https://dev.to/techwithhari/autoscaling-doesnt-fit-every-workload-heres-how-to-tell-and-what-to-build-instead-45ff</link>
      <guid>https://dev.to/techwithhari/autoscaling-doesnt-fit-every-workload-heres-how-to-tell-and-what-to-build-instead-45ff</guid>
      <description>&lt;p&gt;"Just put it behind an autoscaling group" is one of those pieces of advice that's right so often it stops getting questioned. For a stateless API or a web server, it's genuinely close to free scaling  add a replica, traffic balances across it, remove a replica, nobody notices. It's such a reliable default that it's easy to reach for it everywhere.&lt;/p&gt;

&lt;p&gt;It doesn't fit every workload, though, and the difference matters enough that I think it's worth naming precisely  not as "autoscaling is bad," but as: here's the specific property a workload needs to have before reactive autoscaling makes sense, and here's what to do instead when it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The property that actually matters: interchangeability
&lt;/h2&gt;

&lt;p&gt;A stateless replica is interchangeable. Any instance can serve any request, right now, with zero setup cost specific to that request. That's the entire reason autoscaling groups work as well as they do  the thing you're adding is immediately, fully useful the moment it's up, and removing one costs nothing because whatever it was doing, another replica can pick up instantly.&lt;/p&gt;

&lt;p&gt;A lot of backends quietly violate this assumption without anyone noticing, because the violation only shows up under real scale pressure, not in normal development. Two common ways this happens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session affinity.&lt;/strong&gt; Some external systems only allow a single active session per account, or per some other resource key. Once a session is established on a specific instance, that instance  not just "some instance in the pool"  is now the only place that session lives. A second replica can't share it, take it over seamlessly, or load-balance it away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow, non-trivial startup.&lt;/strong&gt; A stateless container can typically start serving traffic in well under a second. Some backends  anything wrapping a heavier external process, a licensed desktop application being automated, a protocol requiring a multi-step handshake before it's usable  take real, measurable time (seconds to minutes) before a fresh instance is actually useful. If your scaling trigger is "traffic just spiked, react now," and your instances take two minutes to become ready, you've built a system that's structurally too slow for the exact problem it's meant to solve.&lt;/p&gt;

&lt;p&gt;If either of these is true for your backend, a generic reactive autoscaling group isn't a slightly-worse fit  it's actively the wrong shape of solution, and no amount of tuning the trigger thresholds fixes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling out: the fix is timing, not tooling
&lt;/h2&gt;

&lt;p&gt;The good news: scaling out for this class of system is solvable, just not with second-scale reactivity. Instead of "traffic spiked, add a replica now," the right pattern is a slower, trend-based trigger: watch a capacity metric (percentage of current instances' sessions in use, say), and cross a threshold  70-75% is a common choice  &lt;em&gt;before&lt;/em&gt; you're actually out of room, giving the new instance its full startup time to become ready ahead of when it's actually needed.&lt;/p&gt;

&lt;p&gt;This is the same idea as reactive autoscaling, just running on a slower clock that respects your workload's real startup cost, instead of pretending it doesn't have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling in: the part almost everyone skips
&lt;/h2&gt;

&lt;p&gt;Scaling out gets most of the attention because it's the "we're growing" story. Scaling &lt;em&gt;in&lt;/em&gt;  safely  is the harder half, and it's the part a lot of systems never actually build, quietly assuming it'll be fine.&lt;/p&gt;

&lt;p&gt;It won't be fine, by default, for exactly the same reason scaling out needed rethinking: an instance in this class of system might have live, in-progress sessions on it. A naive scale-in policy  "utilization dropped, terminate an instance"  has no way to know that, and will happily kill active work.&lt;/p&gt;

&lt;p&gt;This isn't a novel problem, and it's worth knowing the industry already has a real, named answer for it: AWS's Auto Scaling supports lifecycle hooks specifically for this  a termination hook can hold an instance in a pending state, giving your own code time to drain in-progress work or migrate it elsewhere, rather than the platform just pulling the plug. There's an even more direct newer option built for exactly this situation: instance lifecycle policies that keep an instance retained rather than force-terminating it if a graceful shutdown doesn't complete cleanly. The pattern to copy, regardless of which cloud you're on: &lt;strong&gt;stop routing new work to an instance first, wait for its existing sessions to end naturally, only then actually remove it.&lt;/strong&gt; That's not exotic engineering  it's the same "drain, then remove" idea load balancers have used for connection draining for years, just applied one layer up, at the compute level instead of the request level.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't a niche problem  real, large-scale systems already do this
&lt;/h2&gt;

&lt;p&gt;It's worth checking whether an actual production system at scale validates this shape, rather than trusting my own reasoning alone. Video conferencing platforms are a good real-world example  a live meeting session is about as textbook session-affine as workloads get. Zoom's own published architecture describes exactly the pattern this post is arguing for: participants get routed to the &lt;em&gt;least-loaded&lt;/em&gt; available server for their region, via a dedicated control-plane component that tracks real server load  not round-robin, not a generic autoscaling group blindly adding and removing capacity. That's a capacity-aware assignment layer sitting in front of a pool of session-bound servers, which is precisely the shape you land on once you take "sessions can't just move between replicas" seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;Before reaching for an autoscaling group, ask one honest question: &lt;strong&gt;can any instance in this pool serve any unit of work right now, with no setup cost specific to that instance?&lt;/strong&gt; If yes, standard autoscaling is a great, close-to-free default  use it. If no  because of session affinity, slow startup, or both  you need two separate, deliberately different mechanisms: a slower, trend-based trigger for scaling out, and real drain logic for scaling in. Trying to force a session-affine, slow-starting workload into a fast, reactive, interchangeable-replica model doesn't just work worse  it doesn't really work at all, and the failure mode is subtle enough that it's easy to only discover it once you're already depending on it.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>virtualmachine</category>
      <category>automation</category>
    </item>
    <item>
      <title>Vault cant found What a Live Audit Found That Code Review Never Would</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:28:13 +0000</pubDate>
      <link>https://dev.to/techwithhari/vault-cant-found-what-a-live-audit-found-that-code-review-never-would-548e</link>
      <guid>https://dev.to/techwithhari/vault-cant-found-what-a-live-audit-found-that-code-review-never-would-548e</guid>
      <description>&lt;p&gt;Some bugs hide in logic. This one hid in the gap between "the code looks correct" and "the code is actually working"  and the only way I found it was by refusing to trust either claim without checking the live system directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Mid-migration between two credential-storage systems: an older secrets vault (call it the &lt;em&gt;legacy vault&lt;/em&gt;) used by an original provider integration, and a newer, purpose-built vault for a self-hosted replacement being rolled out gradually. During the transition, both exist side by side. The code that decides which vault to check for a given user's credentials looked, on review, completely correct  it classified each connection record as "legacy" or "new" based on a stored field, and looked in the matching vault.&lt;/p&gt;

&lt;p&gt;Nothing about the code review raised a flag. Which is exactly the problem with code review as your only line of defense: it tells you the logic is &lt;em&gt;internally consistent&lt;/em&gt;, not that the systems it's talking to still exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the audit, properly this time
&lt;/h2&gt;

&lt;p&gt;Instead of trusting the classification logic in the abstract, I wrote a small audit script with one rule: &lt;strong&gt;don't reconstruct what a secret's name &lt;em&gt;should&lt;/em&gt; be and assume it's there  actually query the live vault and see.&lt;/strong&gt; This sounds obvious. It's also very easy to skip, because "reconstruct the expected name, spot-check a couple" feels like it should be equivalent to "check them all live," and it really isn't.&lt;/p&gt;

&lt;p&gt;The script pulled every connection record from the database, classified each one the same way the app's own code did, and then made a real, live call to whichever vault it &lt;em&gt;should&lt;/em&gt; be in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;p&gt;Every single legacy-vault lookup failed. Not "secret not found"  a connection failure. The vault itself wasn't resolving anymore.&lt;/p&gt;

&lt;p&gt;That's a materially different problem than a missing secret. A missing secret means "someone forgot to write this." A vault that doesn't resolve at all means the entire piece of infrastructure the code has been confidently pointing at no longer exists  and every single credential-write attempt to it, for every legacy user, had been silently failing, this whole time, for however long it had been broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did nobody notice?
&lt;/h2&gt;

&lt;p&gt;The honest, slightly embarrassing answer: the code that wrote to this vault caught the failure, logged a warning, and moved on. Not a crash. Not an alert. A line in a log nobody was watching, for a code path nobody expected to fail, because "the vault exists" was baked in as an assumption so early in the system's life that it had stopped being something anyone thought to re-check.&lt;/p&gt;

&lt;p&gt;This is the actual shape of the bug, more than the specific vault: &lt;strong&gt;a &lt;code&gt;try/catch&lt;/code&gt; that swallows a failure gracefully is indistinguishable, from the outside, from success  until someone goes looking.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually surprised me
&lt;/h2&gt;

&lt;p&gt;Here's where it got more interesting than "we found a broken thing, went and fixed it." Digging into &lt;em&gt;why&lt;/em&gt; the legacy vault mattered at all, it turned out the underlying legacy provider managed those users' actual live sessions entirely on its own side  the app's cached copy of their credentials was never actually read from again in practice. The vault had been dead, silently, for a long stretch, and functionally, nothing legacy-side had actually broken because of it.&lt;/p&gt;

&lt;p&gt;That's not a "phew, no harm done, nothing to see here" ending though. It's a different, more uncomfortable one: &lt;strong&gt;we got lucky that this specific dead dependency happened to be redundant.&lt;/strong&gt; The audit didn't know that in advance, and neither did I. The only way to find out whether a silently-dead piece of infrastructure actually matters is to go looking  you can't reason your way to "probably fine" from the code alone.&lt;/p&gt;

&lt;p&gt;The same audit, run against the &lt;em&gt;newer&lt;/em&gt; vault  the one actually still in active, load-bearing use  found a real, smaller, actionable problem: a couple of expected entries genuinely missing. Real incomplete setup, not infrastructure rot. Fixed directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code review confirms logic. It cannot confirm that the systems the logic depends on still exist.&lt;/strong&gt; Those are different guarantees, and it's easy to unconsciously treat the first as if it implies the second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A caught-and-logged failure is a blind spot by construction&lt;/strong&gt;, not just bad luck  the entire point of catching it gracefully is that nothing breaks loudly. That's good for uptime and bad for ever noticing, unless something is actually watching those logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Reconstruct the expected value and spot-check it" is not the same audit as "query the live system for every case."&lt;/strong&gt; The first checks your mental model. The second checks reality. They can disagree, and when they do, reality wins.&lt;/li&gt;
&lt;li&gt;Every so often, a system this old deserves a live audit, not just a code review  specifically to surface the assumptions that were true once, got baked in early, and nobody has had a reason to re-question since.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>monitoring</category>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why Your CI Agent Can't Install pip</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:06:39 +0000</pubDate>
      <link>https://dev.to/techwithhari/why-your-ci-agent-cant-install-pip-11if</link>
      <guid>https://dev.to/techwithhari/why-your-ci-agent-cant-install-pip-11if</guid>
      <description>&lt;p&gt;If you've ever set up a self-hosted CI/CD agent on a fresh Ubuntu image and hit &lt;code&gt;pip: command not found&lt;/code&gt; or &lt;code&gt;No module named pip&lt;/code&gt;, your first instinct is probably to check the network. Mine was too. That instinct is wrong, and the real answer took me four attempts to actually land on.&lt;/p&gt;

&lt;p&gt;Here's the full story, including the three dead ends, because I think the dead ends are the useful part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A self-hosted CI agent (in my case, an Azure DevOps agent, but this applies just as much to a self-hosted GitHub Actions runner or a plain Jenkins box) running on a minimal Ubuntu image. No sudo access on the box  that's deliberate, since the agent shouldn't need root just to run a pipeline. I needed to install a CLI tool that only ships via &lt;code&gt;pip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reasonable first attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;python3 -m pip install --user --quiet some-cli-tool&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tool'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;/usr/bin/python3: No module named pip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine, I thought  &lt;code&gt;pip&lt;/code&gt; module is missing, &lt;code&gt;ensurepip&lt;/code&gt; should bootstrap it. Every tutorial says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: &lt;code&gt;ensurepip&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;python3 -m ensurepip --user --upgrade&lt;/span&gt;
    &lt;span class="s"&gt;python3 -m pip install --user --quiet some-cli-tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;/usr/bin/python3: No module named ensurepip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not just &lt;code&gt;pip&lt;/code&gt; missing  &lt;code&gt;ensurepip&lt;/code&gt; itself, the &lt;em&gt;thing that's supposed to install pip&lt;/em&gt;, doesn't exist either. That's the first real clue something structural is going on, not just a missing package.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual reason (worth understanding, not just working around)
&lt;/h2&gt;

&lt;p&gt;Debian and Ubuntu deliberately strip &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;ensurepip&lt;/code&gt; out of the base &lt;code&gt;python3&lt;/code&gt; package. This isn't a bug or an oversight  it's a packaging policy decision. Both live in separate packages (&lt;code&gt;python3-pip&lt;/code&gt;, &lt;code&gt;python3-venv&lt;/code&gt;) that you're expected to install via &lt;code&gt;apt&lt;/code&gt;. On a full desktop or dev-configured server, you've probably had these installed for so long you forgot they're not actually part of core Python.&lt;/p&gt;

&lt;p&gt;On a minimal, no-sudo CI image, you don't have &lt;code&gt;apt&lt;/code&gt; access at all  so you can't just &lt;code&gt;apt install python3-pip&lt;/code&gt; your way out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: &lt;code&gt;venv&lt;/code&gt; (also fails, same root cause)
&lt;/h2&gt;

&lt;p&gt;My next thought: skip &lt;code&gt;pip&lt;/code&gt;/&lt;code&gt;ensurepip&lt;/code&gt; entirely, use a virtual environment instead, since &lt;code&gt;venv&lt;/code&gt; normally bootstraps its own &lt;code&gt;pip&lt;/code&gt; on creation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;python3 -m venv /tmp/tool-venv&lt;/span&gt;
    &lt;span class="s"&gt;/tmp/tool-venv/bin/pip install --quiet some-cli-tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: fails identically. &lt;code&gt;venv&lt;/code&gt;'s own pip-bootstrapping step depends on  you guessed it  the same missing &lt;code&gt;ensurepip&lt;/code&gt; module. Same wall, different door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 4: the actual fix  &lt;code&gt;get-pip.py&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The real answer is a standalone bootstrapping script, maintained by the Python Packaging Authority specifically for situations like this  environments where &lt;code&gt;ensurepip&lt;/code&gt; isn't available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl -sS https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py&lt;/span&gt;
    &lt;span class="s"&gt;python3 /tmp/get-pip.py --user --quiet&lt;/span&gt;
    &lt;span class="s"&gt;python3 -m pip install --user --quiet some-cli-tool&lt;/span&gt;
    &lt;span class="s"&gt;echo "##vso[task.prependpath]$HOME/.local/bin"&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(get-pip,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sudo)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get-pip.py&lt;/code&gt; doesn't depend on &lt;code&gt;ensurepip&lt;/code&gt; at all  it's a self-contained bootstrapper that installs &lt;code&gt;pip&lt;/code&gt; directly. This worked immediately, no sudo, no apt, no venv complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing to double-check before you assume this is your problem
&lt;/h2&gt;

&lt;p&gt;Before chasing this fix, rule out the boring explanation first: confirm your agent actually has network access to PyPI at all. A quick diagnostic step saved me from solving the wrong problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl -sS -o /dev/null -w "pypi.org: %{http_code}\n" https://pypi.org --max-time 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that comes back with anything other than a fast &lt;code&gt;200&lt;/code&gt;, you're dealing with a firewall/egress issue, not this one  and no amount of &lt;code&gt;get-pip.py&lt;/code&gt; will fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;python3 -m pip&lt;/code&gt; and &lt;code&gt;python3 -m ensurepip&lt;/code&gt; are &lt;em&gt;both&lt;/em&gt; missing on a Debian/Ubuntu box, that's not a broken image  it's the distro's actual, intentional packaging policy. &lt;code&gt;apt install python3-pip&lt;/code&gt; is the "normal" fix, and it's simply not available to you on a locked-down, no-sudo CI agent. &lt;code&gt;get-pip.py&lt;/code&gt; is the one workaround that doesn't need &lt;code&gt;apt&lt;/code&gt;, doesn't need &lt;code&gt;ensurepip&lt;/code&gt;, and doesn't need root.&lt;/p&gt;

&lt;p&gt;Small thing, but it cost me three wrong turns before I found it  hopefully this saves you those three.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>python</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Why AI Can't Actually Hit Your Word Limit</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:20:08 +0000</pubDate>
      <link>https://dev.to/techwithhari/why-ai-cant-actually-hit-your-word-limit-35b1</link>
      <guid>https://dev.to/techwithhari/why-ai-cant-actually-hit-your-word-limit-35b1</guid>
      <description>&lt;p&gt;I Was drafting my blog post, told Claude "keep it to 250 words." Got back something like 340. Asked again. Same story.&lt;/p&gt;

&lt;p&gt;Figured there's a real reason behind this, and since I'm neck-deep in AI/ML fundamentals right now, decided to actually understand it instead of just being annoyed by it.&lt;/p&gt;

&lt;h3&gt;
  
  
  It generates one token at a time, no plan
&lt;/h3&gt;

&lt;p&gt;LLMs don't write like we do think of the whole thing, then fill it in. They generate autoregressively: predict the next token, add it to the context, predict the next one, repeat. There's no "here's my 250-word essay, let me write it out" step happening anywhere. Every single token is picked based on what came before, nothing more.&lt;/p&gt;

&lt;p&gt;So when you ask for 250 words, the model isn't holding a target in memory and counting down. It's pattern-matching: "text like this, following an instruction like this, tends to stop around here." That's a guess shaped by training data, not a computation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Words ≠ tokens
&lt;/h3&gt;

&lt;p&gt;Also it's not even counting &lt;em&gt;words&lt;/em&gt;. Internally, everything is tokens: sub-word chunks. "Debugging" might be one token or three depending on the tokenizer. "250 words" as an instruction gets converted into an expectation of &lt;em&gt;roughly&lt;/em&gt; how many tokens that maps to, and that mapping is fuzzy. There's no clean 1:1 between what you asked for and what the architecture tracks.&lt;/p&gt;

&lt;h3&gt;
  
  
  No counter anywhere in the architecture
&lt;/h3&gt;

&lt;p&gt;This is the part that surprised me most: there's no length-tracking variable sitting in the model's forward pass. No &lt;code&gt;if word_count == 250: stop&lt;/code&gt;. The only thing steering generation is the probability distribution over the next token, sampled step by step, until it predicts an end-of-sequence token or hits a max-length cutoff set by the system.&lt;/p&gt;

&lt;p&gt;Compare that to something like a &lt;code&gt;for&lt;/code&gt; loop with a counter deterministic, exact. LLM generation is closer to sampling from a learned distribution, over and over. Approximate by design, not by bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  The workaround
&lt;/h3&gt;

&lt;p&gt;Since I can't get an exact count out of a single generation, what actually works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a natural stopping point instead of a hard number&lt;/li&gt;
&lt;li&gt;Generate first, then trim or expand after, treating the count as a second pass, not the first&lt;/li&gt;
&lt;li&gt;For real precision, set limits in characters or use iterative regeneration with a check step, rather than trusting the first output&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this clicked for me
&lt;/h3&gt;

&lt;p&gt;This ties directly into what I've been learning attention, softmax over vocabulary, sampling strategies. Word-count control would need something structurally different: a running counter fed back into the decoding loop, or a constrained decoding step that force-stops at a token budget. Neither is how these mainstream models work by default.&lt;/p&gt;

&lt;p&gt;Small annoyance, decent rabbit hole. Worth knowing before you set a hard word limit and assume the model's counting along with you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Learning AI/ML from the ground up cloud engineer trying to understand the models she uses daily.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>development</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Serving ML Artifacts from Amazon S3 Files How I used After the Launch</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:37:34 +0000</pubDate>
      <link>https://dev.to/techwithhari/serving-ml-artifacts-from-amazon-s3-files-how-i-used-after-the-launch-764</link>
      <guid>https://dev.to/techwithhari/serving-ml-artifacts-from-amazon-s3-files-how-i-used-after-the-launch-764</guid>
      <description>&lt;h2&gt;
  
  
  The Honest Story
&lt;/h2&gt;

&lt;p&gt;Two months ago, everyone was posting about Amazon S3 Files. New feature, big announcement, screenshots everywhere. I scrolled past most of them another AWS launch, another round of "here's what it does" posts.&lt;/p&gt;

&lt;p&gt;I never actually understood what it was until it became my problem.&lt;/p&gt;

&lt;p&gt;I was in the middle of my ML learning journey, building a semantic search project. Large FAISS indexes, BM25 artifacts all needed to be available at serving time. I was uploading them directly into my deployment. It worked, until it didn't. Hit the size limit. Container cold starts became painful. Every restart meant downloading hundreds of megabytes before the first request could be served.&lt;/p&gt;

&lt;p&gt;I had one option left S3.&lt;/p&gt;

&lt;p&gt;And somewhere in the back of my head I remembered: "wait, there's that new S3 file system thing everyone was talking about." S3 is already my favorite AWS service. So why not try it?&lt;/p&gt;

&lt;p&gt;I tried it. I built something real with it. And now I'm writing the post I wish existed two months ago not "here's what S3 Files is" but "here's what actually happens when you use it."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A semantic search engine over AWS documentation 5048 pages, indexed with FAISS + BM25 hybrid retrieval, served via FastAPI. The entire ML artifact stack (indexes, metadata) is served directly from an S3 Files NFS mount on EC2. No cold start downloads. No boto3 in the serving code. Just &lt;code&gt;open()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings: &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Semantic search: FAISS (IndexFlatIP)&lt;/li&gt;
&lt;li&gt;Keyword search: BM25Okapi&lt;/li&gt;
&lt;li&gt;Serving: FastAPI + Jinja2&lt;/li&gt;
&lt;li&gt;Artifact storage: Amazon S3 + S3 Files (NFS)&lt;/li&gt;
&lt;li&gt;Compute: EC2 t3.medium (Ubuntu 22.04)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Harivelu0/s3-files-ml-serving" rel="noopener noreferrer"&gt;Harivelu0/s3-files-ml-serving&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem S3 Files Solves
&lt;/h2&gt;

&lt;p&gt;Before S3 Files, serving large ML artifacts from containers looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Every container startup
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hf_hub_download&lt;/span&gt;

&lt;span class="n"&gt;faiss_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faiss.index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 7MB
&lt;/span&gt;&lt;span class="n"&gt;bm25_path&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25_index.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 17MB
&lt;/span&gt;&lt;span class="n"&gt;meta_path&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;corpus_meta.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 2.6MB
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every container restart meant downloading ~27MB before the first request. For larger models this becomes hundreds of MB or GBs. You'd need to manage download logic, handle failures, worry about &lt;code&gt;/tmp&lt;/code&gt; size limits, and every container on the same host duplicates the same data.&lt;/p&gt;

&lt;p&gt;With S3 Files, your serving code becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;

&lt;span class="c1"&gt;# Just open() no download, no boto3
&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mnt/artifacts/faiss.index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mnt/artifacts/bm25_index.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The S3 bucket is mounted as an NFS volume on the EC2 host. Your container reads from it like a local file. That's it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Docs (5048 pages)
        ↓
crawl_aws_docs.py (Sitemap + BeautifulSoup)
        ↓
build_index.py (FAISS + BM25 + corpus_meta)
        ↓
S3 Bucket (versioning enabled)
        ↓
S3 File System (NFS layer on top of bucket)
        ↓
Mount Target (NFS endpoint inside VPC)
        ↓
EC2 Ubuntu 22.04
  └── /mnt/artifacts (S3 Files mounted here)
       └── Docker container
            └── FastAPI reads indexes directly
                 └── http://ec2-ip:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setting Up S3 Files What Actually Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. S3 bucket needs versioning enabled
&lt;/h3&gt;

&lt;p&gt;This one catches everyone. S3 Files will refuse to create a file system without it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; your-bucket &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. IAM role trust principal is &lt;code&gt;elasticfilesystem.amazonaws.com&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Not &lt;code&gt;s3files.amazonaws.com&lt;/code&gt; even though the service is called S3 Files, it's built on EFS under the hood.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"elasticfilesystem.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_ACCOUNT_ID"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The IAM role needs EventBridge permissions
&lt;/h3&gt;

&lt;p&gt;S3 Files uses EventBridge internally to monitor bucket changes. Without these, your file system gets stuck in &lt;code&gt;creating&lt;/code&gt; forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"events:PutRule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"events:DeleteRule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"events:PutTargets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"events:RemoveTargets"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. boto3 API uses camelCase
&lt;/h3&gt;

&lt;p&gt;The S3 Files boto3 client uses camelCase unlike most other AWS services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong
&lt;/span&gt;&lt;span class="n"&gt;s3files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_file_system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role_arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Correct
&lt;/span&gt;&lt;span class="n"&gt;s3files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_file_system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role_arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Mount needs &lt;code&gt;amazon-efs-utils&lt;/code&gt;, not plain NFS
&lt;/h3&gt;

&lt;p&gt;Plain &lt;code&gt;mount -t nfs4&lt;/code&gt; will fail. S3 Files requires the &lt;code&gt;amazon-efs-utils&lt;/code&gt; package (v3.0+) which handles TLS + IAM auth automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build and install amazon-efs-utils&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; cmake golang-go rustc cargo
git clone https://github.com/aws/efs-utils
&lt;span class="nb"&gt;cd &lt;/span&gt;efs-utils &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./build-deb.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ./build/amazon-efs-utils&lt;span class="k"&gt;*&lt;/span&gt;.deb

&lt;span class="c"&gt;# Then mount&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; s3files fs-0xxxxxxxxx:/ /mnt/artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. EC2 instance needs &lt;code&gt;AmazonS3FilesClientFullAccess&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Without this policy on the EC2 role, the mount returns &lt;code&gt;access denied by server&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam attach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role-name&lt;/span&gt; your-ec2-role &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/AmazonS3FilesClientFullAccess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Serving Code
&lt;/h2&gt;

&lt;p&gt;FastAPI startup loads everything from the mount. No download logic. No error handling for network failures during download. The mount is always there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ARTIFACTS_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARTIFACTS_DIR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mnt/artifacts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Reads directly from S3 Files mount
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARTIFACTS_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faiss.index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARTIFACTS_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25_index.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bm25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARTIFACTS_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;corpus_meta.pkl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container startup: model loads in ~2s. Artifacts available instantly from mount.&lt;/p&gt;




&lt;h2&gt;
  
  
  Weekly Index Updates The Real S3 Files Advantage
&lt;/h2&gt;

&lt;p&gt;This is where S3 Files goes beyond just solving cold starts.&lt;/p&gt;

&lt;p&gt;AWS updates their docs regularly. With traditional artifact serving you'd need to rebuild indexes, push a new container image, and redeploy causing downtime.&lt;/p&gt;

&lt;p&gt;With S3 Files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# update_pipeline.sh runs weekly via EventBridge&lt;/span&gt;
python scripts/crawl_aws_docs.py &lt;span class="nt"&gt;--update&lt;/span&gt;   &lt;span class="c"&gt;# only changed pages&lt;/span&gt;
python precompute/build_index.py            &lt;span class="c"&gt;# rebuild indexes to /tmp&lt;/span&gt;

&lt;span class="c"&gt;# Atomic swap no downtime&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/faiss.index.new     /mnt/artifacts/artifacts/faiss.index
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/bm25_index.pkl.new  /mnt/artifacts/artifacts/bm25_index.pkl
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/corpus_meta.pkl.new /mnt/artifacts/artifacts/corpus_meta.pkl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The serving container picks up the new indexes on the next query. Zero restart. Zero redeploy. The mount sees the updated S3 objects immediately.&lt;/p&gt;

&lt;p&gt;This is the killer feature not just cold start elimination, but &lt;strong&gt;live index updates without any deployment&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The search UI running at &lt;code&gt;http://98.93.65.241:8000&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: &lt;code&gt;"aws sns permission issue"&lt;/code&gt; → 10 results in &lt;strong&gt;334ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Artifacts loaded at startup: &lt;strong&gt;instant&lt;/strong&gt; (no download)&lt;/li&gt;
&lt;li&gt;Index update: rebuild + upload to S3 → serving picks up &lt;strong&gt;without restart&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;S3 Files is genuinely useful for ML workloads where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Artifacts are large (&amp;gt;100MB)&lt;/li&gt;
&lt;li&gt;Multiple containers need the same data&lt;/li&gt;
&lt;li&gt;Indexes update regularly without downtime&lt;/li&gt;
&lt;li&gt;You want to avoid baking artifacts into Docker images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a replacement for EFS if you need pure file system performance. And it's not for every use case if your artifacts never change and containers rarely restart, the complexity isn't worth it.&lt;/p&gt;

&lt;p&gt;But if you've ever stared at a container downloading 500MB on every cold start, S3 Files is exactly what you were waiting for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Harivelu0/s3-files-ml-serving" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html" rel="noopener noreferrer"&gt;Amazon S3 Files docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/efs-utils" rel="noopener noreferrer"&gt;amazon-efs-utils GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Built a Self Resizing EC2 for My ML Data</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 07 Jun 2026 11:45:14 +0000</pubDate>
      <link>https://dev.to/techwithhari/how-i-built-a-self-resizing-ec2-for-my-ml-data-4i38</link>
      <guid>https://dev.to/techwithhari/how-i-built-a-self-resizing-ec2-for-my-ml-data-4i38</guid>
      <description>&lt;h2&gt;
  
  
  The Pain Point
&lt;/h2&gt;

&lt;p&gt;I'm on an ML learning journey. That means a lot of data. A lot of processing. And a lot of AWS free credits I really can't afford to waste.&lt;/p&gt;

&lt;p&gt;Here's what my typical day looked like:&lt;/p&gt;

&lt;p&gt;I'd spin up a &lt;code&gt;t3.large&lt;/code&gt; to run a data pipeline. Load some datasets, process them, store them. The pipeline would run for a couple of hours sometimes I didn't even know exactly how long it would take. Then I'd go to sleep.&lt;/p&gt;

&lt;p&gt;Next morning I'd check CloudWatch and realise the VM had been sitting idle since 3AM. Running. Doing nothing. Burning credits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the problem. You need a big machine for data loading. You don't need it after.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Thought Let AWS ML Decide
&lt;/h2&gt;

&lt;p&gt;My first instinct was to use &lt;strong&gt;AWS Compute Optimizer&lt;/strong&gt;. It's a managed ML service that analyses your EC2 usage patterns and recommends the right instance type. Smart, right?&lt;/p&gt;

&lt;p&gt;I enabled it. Waited. And waited.&lt;/p&gt;

&lt;p&gt;Turns out Compute Optimizer needs at least &lt;strong&gt;30 consecutive hours&lt;/strong&gt; of usage data before it generates recommendations. For a VM I spin up occasionally for pipeline runs that's not practical.&lt;/p&gt;

&lt;p&gt;So I moved on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Second Thought Bedrock Agent
&lt;/h2&gt;

&lt;p&gt;Next idea: use &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; as an agent to reason about when and how to resize. Let an LLM decide.&lt;/p&gt;

&lt;p&gt;But the more I thought about it, the more it felt like overkill. The decision isn't complex:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Did the pipeline finish? Yes → resize down. No → don't."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a reasoning problem. That's an automation problem. Using Bedrock here would be AI washing  adding complexity without adding value.&lt;/p&gt;

&lt;p&gt;So I kept it simple.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution EventDriven VM Resize
&lt;/h2&gt;

&lt;p&gt;What I built: a lightweight agent that listens for your pipeline to complete, then automatically resizes the VM down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;emit_event.py&lt;/code&gt;  runs on your VM, fires when pipeline exits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EventBridge&lt;/strong&gt; receives the event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt;  handles resize logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SNS&lt;/strong&gt;  sends email alert (success or failure)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline finishes
      ↓
emit_event.py → EventBridge
      ↓
Lambda triggered
      ↓
SUCCESS → resize down + email
FAILURE → keep size (debug) + email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ML. No LLM. Just the right tool for the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your VM                        AWS Cloud
──────────────────             ─────────────────────
run_pipeline.sh                EventBridge (custom bus)
  step1.py               →          ↓
  step2.py                     Lambda
  step3.py                       ├── stop EC2
  emit_event.py ──────────→      ├── resize instance type
                                 ├── start EC2
                                 └── SNS email alert
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All AWS resources are deployed with a single CloudFormation command. No manual console clicking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup  3 Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1  Deploy AWS infrastructure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Harivelu0/vm-resize-agent
&lt;span class="nb"&gt;cd &lt;/span&gt;vm-resize-agent

aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/template.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; vm-resize-agent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_NAMED_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;AlertEmail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your@email.com &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;TargetInstanceType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;t3.medium &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your email → confirm the AWS subscription link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2  Copy agent to your VM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp &lt;span class="nt"&gt;-i&lt;/span&gt; your-key.pem &lt;span class="nt"&gt;-r&lt;/span&gt; agent/ pipeline/ &lt;span class="se"&gt;\&lt;/span&gt;
  ec2-user@your-vm-ip:~/vm-resize-agent/

&lt;span class="c"&gt;# on VM&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;boto3
aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3  Add your pipeline steps
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;pipeline/steps.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 /home/user/myproject/fetch_data.py
python3 /home/user/myproject/transform.py
python3 /home/user/myproject/load_db.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash pipeline/run_pipeline.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. When pipeline finishes → VM resizes → email arrives.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Key Design Decision steps.conf
&lt;/h2&gt;

&lt;p&gt;One thing I was particular about: this tool should work for &lt;strong&gt;anyone's pipeline&lt;/strong&gt;, not just mine.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;run_pipeline.sh&lt;/code&gt; never changes. Users only edit &lt;code&gt;steps.conf&lt;/code&gt;  one command per line, one step per line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# steps.conf&lt;/span&gt;
python3 /home/user/fetch_gdelt.py
python3 /home/user/build_forecasts.py
python3 /home/user/calibrate.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper reads each line, runs it in order, tracks success/failure, and emits the event at the end. Generic by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Email Alert
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On success:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline: data-loader
Status:   SUCCESS
Steps:    3/3
Duration: 45m 12s
Instance: i-0abc123
Resized:  Yes -&amp;gt; t3.medium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On failure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline:  data-loader
Status:    FAILURE
Steps:     2/3
Failed at: step_3
Instance:  i-0abc123
Resized:   No (kept original size for debugging)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failure case is important the VM intentionally stays large so you can SSH in and debug without losing state.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39zjfoebd2b22tbhsvl3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39zjfoebd2b22tbhsvl3.png" alt=" " width="798" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29898npypv1153shb2pb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29898npypv1153shb2pb.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mh29kfqozjupdmkcovv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mh29kfqozjupdmkcovv.png" alt=" " width="318" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrbl8bfsvv2mq9ytbi5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrbl8bfsvv2mq9ytbi5u.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr72su3nwq3zld1f3zy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr72su3nwq3zld1f3zy4.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Difficulties I Faced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. IP changes after resize&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Lambda stops and restarts the EC2, it gets a new public IP. Learned this the hard way when SSH stopped working. Fix: assign an &lt;strong&gt;Elastic IP&lt;/strong&gt; before running demos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 allocate-address &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws ec2 associate-address &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allocation-id&lt;/span&gt; eipalloc-xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Docker goes down after resize&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EC2 resize = reboot. Any running Docker containers stop. Add this to &lt;code&gt;/etc/rc.local&lt;/code&gt; on your VM so services restart automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service docker start
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/ec2-user/myproject &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Live Demo
&lt;/h2&gt;

&lt;p&gt;For the demo I used a real weather dataset loaded into Postgres running in Docker on the EC2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EC2: t3.large (before)
       ↓
Pipeline runs: download → parse → load into Postgres
       ↓
emit_event.py fires automatically when done
       ↓
Lambda: stops EC2 → resizes → starts EC2
       ↓
EC2: t3.medium (after)
       ↓
Email arrives in Gmail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cost Reality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EventBridge&lt;/td&gt;
&lt;td&gt;~$0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;td&gt;~$0 (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNS email&lt;/td&gt;
&lt;td&gt;~$0 (first 1000 free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total stack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The savings depend on your instance. A &lt;code&gt;t3.large&lt;/code&gt; idle for 20hrs/day wastes ~$25/month. For larger instances like &lt;code&gt;c5.4xlarge&lt;/code&gt; you're looking at $200+ saved per month.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use This
&lt;/h2&gt;

&lt;p&gt;This is for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You run data pipelines on EC2 manually or on a schedule&lt;/li&gt;
&lt;li&gt;Your pipeline takes unpredictable time to complete&lt;/li&gt;
&lt;li&gt;You don't need the heavy instance after loading is done&lt;/li&gt;
&lt;li&gt;You're not ready for EMR or Glue yet (pre-pipeline stage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already using EMR Serverless or Glue (they auto-terminate)&lt;/li&gt;
&lt;li&gt;Your pipeline runs less than 30 minutes (manual resize is fine)&lt;/li&gt;
&lt;li&gt;You need horizontal scaling (use Auto Scaling Groups instead)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why a VM and Not Glue or EMR?
&lt;/h2&gt;

&lt;p&gt;Honest answer I didn't know enough about my data yet to make that decision.&lt;/p&gt;

&lt;p&gt;Glue and EMR are great services. But they come with questions you need to answer upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's your data format?&lt;/li&gt;
&lt;li&gt;What transformations do you need?&lt;/li&gt;
&lt;li&gt;What's the volume?&lt;/li&gt;
&lt;li&gt;Do you need Spark?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you're learning, you don't have those answers yet. You just need to load data, see what you're working with, and prove the pipeline works.&lt;/p&gt;

&lt;p&gt;A VM lets you do that with zero infrastructure decisions. Just Python scripts. When the pipeline is proven and you understand your data then you migrate to the right managed service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is that phase. Before you know which service you need.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is actually how real teams work too. Nobody starts with EMR on day one of a new data project. You explore first. You prove it works. Then you scale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 1: Explore         → VM + Python scripts
Phase 2: Prove it works  → VM + vm-resize-agent
Phase 3: Scale           → Glue / EMR / Spark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;Everything is open source. Clone, edit &lt;code&gt;steps.conf&lt;/code&gt;, deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Harivelu0/vm-resize-agent" rel="noopener noreferrer"&gt;https://github.com/Harivelu0/vm-resize-agent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things I want to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto resize &lt;strong&gt;back up&lt;/strong&gt; before next scheduled run (cron-based)&lt;/li&gt;
&lt;li&gt;Slack alerts alongside email&lt;/li&gt;
&lt;li&gt;Support for Azure VMs (same pattern, different SDK)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ideas or run into issues open a GitHub issue. Happy to help.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public as part of my ML learning journey. Follow along for more practical AWS patterns.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How I Built Production AI Agent Monitoring with Langfuse</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 13 May 2026 16:32:02 +0000</pubDate>
      <link>https://dev.to/techwithhari/how-i-built-production-ai-agent-monitoring-with-langfuse-52he</link>
      <guid>https://dev.to/techwithhari/how-i-built-production-ai-agent-monitoring-with-langfuse-52he</guid>
      <description>&lt;p&gt;Multi-agent AI systems fail silently.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;200 OK&lt;/code&gt; response doesn’t mean the AI made good decisions.&lt;/p&gt;

&lt;p&gt;That was the biggest thing I realized while building a multi-agent system.&lt;/p&gt;

&lt;p&gt;My architecture looked like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Query → Multi Agent Call → Final Response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything looked normal from an infrastructure perspective.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIs were healthy&lt;/li&gt;
&lt;li&gt;Latency looked fine&lt;/li&gt;
&lt;li&gt;Users were getting responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But I still couldn’t answer important questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the Agent route the query to the right specialist?&lt;/li&gt;
&lt;li&gt;Did the agent hallucinate information?&lt;/li&gt;
&lt;li&gt;Did it ignore specialist outputs?&lt;/li&gt;
&lt;li&gt;Did it attribute responses incorrectly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional monitoring couldn’t help because the system technically wasn’t failing.&lt;/p&gt;

&lt;p&gt;The failures were happening at the &lt;strong&gt;decision layer&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Trace Visibility
&lt;/h2&gt;

&lt;p&gt;I used Langfuse to trace every agent execution.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool calls&lt;/li&gt;
&lt;li&gt;Input/output payloads&lt;/li&gt;
&lt;li&gt;Token usage&lt;/li&gt;
&lt;li&gt;Latency per step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an agent touched something, I wanted visibility into it.&lt;/p&gt;

&lt;p&gt;No black boxes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deterministic Checks
&lt;/h2&gt;

&lt;p&gt;Some validations didn’t need another LLM.&lt;/p&gt;

&lt;p&gt;I added rule-based checks for things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the agent call tools from the correct domain?&lt;/li&gt;
&lt;li&gt;Did the agent call tools it wasn’t supposed to?&lt;/li&gt;
&lt;li&gt;Was the expected workflow followed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks are binary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass → &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fail → &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast and cheap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Faithfulness Checks
&lt;/h2&gt;

&lt;p&gt;This was mainly for hallucination detection.&lt;/p&gt;

&lt;p&gt;I compare the final response with outputs from specialist agents.&lt;/p&gt;

&lt;p&gt;If the Final layer introduces claims that weren’t exist in source outputs, it gets flagged.&lt;/p&gt;

&lt;p&gt;This helped catch cases where the system sounded confident but wasn’t grounded.&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM Judges
&lt;/h2&gt;

&lt;p&gt;For things deterministic checks can’t measure, I use Azure OpenAI as judges.&lt;/p&gt;

&lt;p&gt;They evaluate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routing correctness&lt;/li&gt;
&lt;li&gt;Response quality&lt;/li&gt;
&lt;li&gt;Attribution accuracy&lt;/li&gt;
&lt;li&gt;Conflict handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This runs for every multi-agent response.&lt;/p&gt;

&lt;p&gt;Expensive? Yes.&lt;br&gt;
Useful? Definitely.&lt;/p&gt;




&lt;h2&gt;
  
  
  100% Traffic Monitoring
&lt;/h2&gt;

&lt;p&gt;I didn’t want sampling.&lt;/p&gt;

&lt;p&gt;Every production request goes through the evaluation pipeline.&lt;/p&gt;

&lt;p&gt;Because edge cases are usually the exact things sampling misses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost + Latency Tracking
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems get expensive very fast.&lt;/p&gt;

&lt;p&gt;I track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tokens per agent&lt;/li&gt;
&lt;li&gt;Latency per step&lt;/li&gt;
&lt;li&gt;Expensive execution paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made optimization much easier.&lt;/p&gt;




&lt;h1&gt;
  
  
  What This Actually Caught
&lt;/h1&gt;

&lt;p&gt;This surfaced issues normal monitoring completely missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong attribution
&lt;/h3&gt;

&lt;p&gt;Correct insights were assigned to the wrong specialist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignored outputs
&lt;/h3&gt;

&lt;p&gt;Sometimes Agent completely ignored specialist responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing mistakes
&lt;/h3&gt;

&lt;p&gt;The call occasionally sent queries to the wrong agent.&lt;/p&gt;




&lt;p&gt;None of these showed up in normal monitoring dashboards.&lt;/p&gt;

&lt;p&gt;Everything looked healthy.&lt;/p&gt;




&lt;h1&gt;
  
  
  Stack
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Observability:&lt;/strong&gt; Langfuse&lt;br&gt;
&lt;strong&gt;LLM Evaluation:&lt;/strong&gt; Azure OpenAI&lt;br&gt;
&lt;strong&gt;Deterministic Checks:&lt;/strong&gt; TypeScript&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Takeaway
&lt;/h1&gt;

&lt;p&gt;For multi-agent systems, uptime monitoring is not enough.&lt;/p&gt;

&lt;p&gt;You also need decision monitoring.&lt;/p&gt;

&lt;p&gt;Because a successful response can still be completely wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Built a Self-Updating SEO Brain Inspired by Andrej Karpathy's LLM Wiki</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 10 May 2026 13:39:47 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-built-a-self-updating-seo-brain-inspired-by-andrej-karpathys-llm-wiki-39p1</link>
      <guid>https://dev.to/techwithhari/i-built-a-self-updating-seo-brain-inspired-by-andrej-karpathys-llm-wiki-39p1</guid>
      <description>&lt;h2&gt;
  
  
  The Tweet That Changed How I Think About AI + Knowledge
&lt;/h2&gt;

&lt;p&gt;In early April 2026, Andrej Karpathy (OpenAI co-founder, former Tesla AI Director) posted something deceptively simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Something I'm finding very useful recently: using LLMs to build personal knowledge bases for various topics of research interest."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He followed it up with a GitHub gist titled &lt;strong&gt;LLM Wiki&lt;/strong&gt; an "idea file" describing a pattern for building knowledge bases that actually &lt;em&gt;compound&lt;/em&gt; over time instead of rediscovering the same information on every query.&lt;/p&gt;

&lt;p&gt;I decided to build it for a real production problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: RAG Has No Memory
&lt;/h2&gt;

&lt;p&gt;In My organization, we were running an SEO monitoring pipeline for our landing site. It used &lt;strong&gt;Cognee&lt;/strong&gt; (a knowledge graph framework) backed by &lt;strong&gt;Neo4j + ChromaDB&lt;/strong&gt; on a $35/month VM.&lt;/p&gt;

&lt;p&gt;Every day it would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull Google Search Console data&lt;/li&gt;
&lt;li&gt;Scrape our pages for SEO issues&lt;/li&gt;
&lt;li&gt;Query the knowledge graph&lt;/li&gt;
&lt;li&gt;Post a Slack report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked. But it had a fundamental flaw &lt;strong&gt;the same flaw Karpathy describes in every RAG system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time it ran, it was rediscovering knowledge from scratch. It had no memory of what it found yesterday. It couldn't connect "we deployed this fix on March 13" with "clicks went up 81% on March 18." It couldn't say "this keyword has been declining for 3 weeks here's why." It just answered the current query and forgot everything.&lt;/p&gt;

&lt;p&gt;On top of that: the VM broke 3+ times. Neo4j config issues. ChromaDB API path changes. Sidecar containers failing silently. We spent more time fixing the pipeline than reading its output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Karpathy's Core Insight
&lt;/h2&gt;

&lt;p&gt;His idea is simple but changes everything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead of retrieving from raw documents at query time compile knowledge once, keep it current, and query the compiled result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The architecture has 3 layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw/        → immutable source data (never edited)
wiki/       → LLM-maintained markdown knowledge base
AGENTS.md   → schema/rules file telling the LLM what to do
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM reads new raw data → integrates it into the wiki → cross-references it with existing knowledge → flags contradictions → the wiki gets richer every day.&lt;/p&gt;

&lt;p&gt;His exact framing: &lt;strong&gt;"Obsidian is the IDE. The LLM is the programmer. The wiki is the codebase."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No vector database. No embeddings. No $35/month VM. Just markdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built: LLM Wiki for SEO
&lt;/h2&gt;

&lt;p&gt;I took Karpathy's pattern and applied it specifically to SEO monitoring for vibetrader.com.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 3 Layers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;raw/&lt;/code&gt;&lt;/strong&gt; : daily immutable snapshots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;raw/gsc/&lt;/span&gt;&lt;span class="mi"&gt;2026-04-17&lt;/span&gt;&lt;span class="err"&gt;.json&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Google&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Search&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(clicks,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CTR,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;positions,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;queries)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;raw/audit/&lt;/span&gt;&lt;span class="mi"&gt;2026-04-17&lt;/span&gt;&lt;span class="err"&gt;.json&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(H&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;meta,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;canonical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;checks)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;raw/commits/&lt;/span&gt;&lt;span class="mi"&gt;2026-04-17&lt;/span&gt;&lt;span class="err"&gt;.json&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;landing-site&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;changed)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;wiki/&lt;/code&gt;&lt;/strong&gt; : LLM-maintained knowledge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;wiki/overview.md              ← "story so far" updated daily
wiki/log.md                   ← append-only daily log
wiki/topics/keywords.md       ← keyword clusters + position tracking
wiki/topics/issues.md         ← open SEO issues with severity
wiki/topics/recommendations.md ← history of recs + acted/pending status
wiki/topics/code-changes.md   ← code change impact tracker
wiki/topics/performance.md    ← CAUSAL CHAINS: issue → fix → metric improvement
wiki/topics/competitors.md    ← weekly competitor analysis
wiki/topics/lint-report.md    ← weekly wiki health check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; : the schema file. Tells the LLM exactly how to update each page, what cross-links to write, what format to follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3:00 PM IST  daily
─────────────────
gsc_pull.py    → pulls GSC data via API → raw/gsc/today.json
audit.py       → fetches vibetrader.com, checks H1/meta/schema → raw/audit/today.json
               → posts Slack audit digest (SEO/GEO/AEO scores)
ingest.py      → reads ALL raw/ + ALL wiki/
               → sends full context to Azure OpenAI
               → LLM updates all wiki pages with cross-links
               → appends to log.md
               → posts enriched Slack report
git commit     → wiki/ + raw/ committed back to main

4:00 PM IST  daily (1 hour after ingest)
─────────────────────────────────────────
fix_agent.py   → reads wiki/issues + recommendations
               → LLM classifies which issues are auto-fixable
               → skill functions run deterministically (not LLM-generated code)
               → raises PR to landing-site dev branch
               → never auto-merges

11:30 AM IST  Saturday
───────────────────────
lint.py        → audits wiki for staleness, contradictions, orphan pages
               → health score /100
               → saves lint-report.md + posts Slack

2:30 PM IST  Sunday
────────────────────
competitor_analysis.py → scrapes competitor sites
                       → LLM analysis → updates competitors.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Knowledge Graph in Plain Markdown
&lt;/h2&gt;

&lt;p&gt;The key innovation over standard LLM Wiki I added a &lt;code&gt;performance.md&lt;/code&gt; page that tracks &lt;strong&gt;causal chains&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### ✅ 2026-03-18 — API response fix&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Cause:**&lt;/span&gt; [[topics/issues]] API returning incomplete pagination data
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Fix:**&lt;/span&gt; API response items + pagination as separate fields → [[topics/code-changes]]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Before:**&lt;/span&gt; 127 clicks/day
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**After:**&lt;/span&gt; 229.7 clicks/day
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Impact:**&lt;/span&gt; +81% clicks
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Keywords moved:**&lt;/span&gt; [[topics/keywords]] — all branded queries improved
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Confidence:**&lt;/span&gt; HIGH — spike confirmed same week as deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every entry answers: &lt;strong&gt;what caused this? what moved? how confident?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what Cognee was trying to do with Neo4j store causal relationships between entities. We're doing it in plain markdown, with &lt;code&gt;[[wiki-links]]&lt;/code&gt; that Obsidian renders as a visual knowledge graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  Obsidian as the Visualization Layer
&lt;/h2&gt;

&lt;p&gt;Open Obsidian → point it at your &lt;code&gt;wiki/&lt;/code&gt; folder → graph view instantly shows all cross-connections between pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;performance&lt;/code&gt; ↔ &lt;code&gt;keywords&lt;/code&gt; ↔ &lt;code&gt;issues&lt;/code&gt; ↔ &lt;code&gt;recommendations&lt;/code&gt; ↔ &lt;code&gt;code-changes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the wiki accumulates data daily, the graph gets richer. After 30 days, clicking any node shows a real story: "this keyword dropped because of this issue, which was fixed by this code change, which resulted in this metric improvement."&lt;/p&gt;

&lt;p&gt;That's the knowledge graph. No Neo4j required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Interactive Query Mode
&lt;/h2&gt;

&lt;p&gt;Beyond the daily Slack push, I added a local query script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# one-shot&lt;/span&gt;
python scripts/query.py &lt;span class="s2"&gt;"what keywords are improving this week?"&lt;/span&gt;

&lt;span class="c"&gt;# REPL with follow-up context&lt;/span&gt;
python scripts/query.py
you&amp;gt; which code change had the most SEO impact?
wiki&amp;gt; Based on code-changes.md:
      March 18 — API response fix
      Before: 127 clicks/day | After: 229.7 clicks/day | Impact: +81%
      This is the highest confirmed impact change &lt;span class="k"&gt;in &lt;/span&gt;the wiki.

you&amp;gt; why did that happen?   ← follow-up, remembers context
wiki&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM answers from the &lt;strong&gt;accumulated wiki context&lt;/strong&gt; not by re-reading 30 days of raw JSON. This is exactly what Karpathy's "query" operation describes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Weekly Lint Pass
&lt;/h2&gt;

&lt;p&gt;One gap in most LLM Wiki implementations no health check. I added &lt;code&gt;lint.py&lt;/code&gt; that runs every Saturday:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Staleness detection&lt;/strong&gt; : claims in the wiki that contradict newer log entries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradiction detection&lt;/strong&gt; : two pages saying opposite things&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan pages&lt;/strong&gt; : pages with no cross-links pointing to them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing concept pages&lt;/strong&gt;: &lt;code&gt;[[wiki-links]]&lt;/code&gt; referenced but never created&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale recommendations&lt;/strong&gt;: recs pending &amp;gt; 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Posts a health score /100 to Slack and saves &lt;code&gt;lint-report.md&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix Agent (Skill-Based, Not LLM-Generated Code)
&lt;/h2&gt;

&lt;p&gt;The fix agent reads the wiki, identifies fixable issues, and raises PRs to the landing-site repo. The key design decision: &lt;strong&gt;the LLM only classifies issues it never writes code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead, deterministic "skill" functions handle each fix type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;add_jsonld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;          &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="n"&gt;adding&lt;/span&gt;
&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;add_internal_link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;
&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fix_meta_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;       &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;explicit&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;
&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fix_robots_txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;      &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;sitemap&lt;/span&gt; &lt;span class="n"&gt;fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM outputs &lt;code&gt;{"skill": "add_jsonld", "page": "app/layout.tsx"}&lt;/code&gt;. The skill runs. No hallucinated file paths. No duplicate schemas. No business logic touched.&lt;/p&gt;

&lt;p&gt;PRs always target &lt;code&gt;dev&lt;/code&gt; branch. Never auto-merge. Human review required.&lt;/p&gt;




&lt;h2&gt;
  
  
  vs Cognee: Honest Comparison
&lt;/h2&gt;

&lt;p&gt;After running both systems in parallel for a week:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cognee&lt;/th&gt;
&lt;th&gt;LLM Wiki&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Neo4j + ChromaDB + VM&lt;/td&gt;
&lt;td&gt;Plain markdown files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;~$35&lt;/td&gt;
&lt;td&gt;~$0 (just Azure OpenAI tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times it broke&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files to maintain&lt;/td&gt;
&lt;td&gt;6 JS pipeline files&lt;/td&gt;
&lt;td&gt;5 Python scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History awareness&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;Grounded references exact dates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEO accuracy&lt;/td&gt;
&lt;td&gt;3/10 (missed JSON-LD already added)&lt;/td&gt;
&lt;td&gt;7/10 (reflected actual state)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowledge compounds&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LLM Wiki won on every dimension except one: &lt;strong&gt;Cognee had richer graph relationships out of the box.&lt;/strong&gt; But we replicated that with &lt;code&gt;performance.md&lt;/code&gt; and &lt;code&gt;[[wiki-links]]&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Karpathy Got Right
&lt;/h2&gt;

&lt;p&gt;The core insight is correct and the more I use it the more obvious it becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG = studying for an exam by re-reading all your textbooks every time you get a question.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM Wiki = you already made notes, highlighted the important parts, drew arrows between connected ideas. Now you just read your notes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The wiki grows daily. The LLM gets smarter about your domain the more data accumulates. By day 30, &lt;code&gt;query.py&lt;/code&gt; answers questions with 30 days of grounded context. By day 90, &lt;code&gt;performance.md&lt;/code&gt; has a dozen confirmed causal chains real institutional knowledge about what moves your metrics and why.&lt;/p&gt;

&lt;p&gt;That's what no RAG system gives you. And you don't need a vector database to get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The pattern works for any domain where you're accumulating knowledge over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO monitoring&lt;/strong&gt; (what I built)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Competitor tracking&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research&lt;/strong&gt; — papers, articles, building a thesis over weeks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering team wiki&lt;/strong&gt; — fed by Slack threads, PRs, incident reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal second brain&lt;/strong&gt; — journal entries, articles, book notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only thing you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;raw/&lt;/code&gt; folder where data lands&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;wiki/&lt;/code&gt; folder for LLM-maintained markdown&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;AGENTS.md&lt;/code&gt; (or &lt;code&gt;CLAUDE.md&lt;/code&gt;) telling the LLM the rules&lt;/li&gt;
&lt;li&gt;A cron job that runs ingest daily&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with one raw data source and one wiki page. Let it run for a week. The compounding effect becomes obvious fast.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>python</category>
      <category>devops</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I built a 20-20-20 eye reminder because my eyes were dying at my desk</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Thu, 16 Apr 2026 08:45:00 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-built-a-20-20-20-eye-reminder-because-my-eyes-were-dying-at-my-desk-be8</link>
      <guid>https://dev.to/techwithhari/i-built-a-20-20-20-eye-reminder-because-my-eyes-were-dying-at-my-desk-be8</guid>
      <description>&lt;p&gt;I have severe eye dryness. My doctor told me to follow the &lt;strong&gt;20-20-20 rule&lt;/strong&gt; every 20 minutes, look at something 20 feet away for 20 seconds. Simple. Except when you're deep in a bug or a feature, 20 minutes evaporates and you've been staring at your monitor for 3 hours straight.&lt;/p&gt;

&lt;p&gt;I tried phone reminders. I dismissed them without thinking. I tried sticky notes. I ignored them. I needed something that &lt;em&gt;actually blocked me&lt;/em&gt; from working until I did the break.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; a dismissible reminder is just noise. The popup had to be impossible to close until the 20-second countdown finished.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Runs silently in the &lt;strong&gt;system tray&lt;/strong&gt; in the background&lt;/li&gt;
&lt;li&gt;Every 20 minutes fires a &lt;strong&gt;loud beeping alarm&lt;/strong&gt; (winsound, no external files needed)&lt;/li&gt;
&lt;li&gt;Dark &lt;strong&gt;blocking popup&lt;/strong&gt; appears X button is disabled, you cannot close it&lt;/li&gt;
&lt;li&gt;20-second &lt;strong&gt;countdown runs automatically&lt;/strong&gt; with a progress bar&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;"Continue" button is locked&lt;/strong&gt; until the countdown finishes, then turns green&lt;/li&gt;
&lt;li&gt;Two buttons: &lt;strong&gt;Continue&lt;/strong&gt; (back to work) or &lt;strong&gt;Stop&lt;/strong&gt; (end the session)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The core trick: disabling the close button
&lt;/h2&gt;

&lt;p&gt;The single most important line in the whole app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WM_DELETE_WINDOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By overriding &lt;code&gt;WM_DELETE_WINDOW&lt;/code&gt; with a no-op lambda, clicking the X does absolutely nothing. The window stays open. You have to wait out the countdown.&lt;/p&gt;


&lt;h2&gt;
  
  
  The countdown + locked button
&lt;/h2&gt;

&lt;p&gt;The "Continue" button starts &lt;code&gt;state="disabled"&lt;/code&gt; and only becomes clickable when the timer hits zero:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;count_var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;continue_btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#003322&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#44ff88&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hand2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;after_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The button turns green when it unlocks a satisfying visual reward for actually doing the break.&lt;/p&gt;


&lt;h2&gt;
  
  
  The alarm
&lt;/h2&gt;

&lt;p&gt;No external audio files needed. &lt;code&gt;winsound&lt;/code&gt; is part of the Windows standard library:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;winsound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Beep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;winsound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Beep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two alternating frequencies, six times. Annoying enough that you cannot ignore it. Runs in a daemon thread so it does not block the UI.&lt;/p&gt;


&lt;h2&gt;
  
  
  The full popup
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show_popup&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Toplevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Eye Break!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#0a0a0a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-topmost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WM_DELETE_WINDOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- the magic line
&lt;/span&gt;
    &lt;span class="c1"&gt;# ... countdown, progress bar, locked button ...
&lt;/span&gt;
    &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;play_alarm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mainloop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;continue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The popup is always-on-top (&lt;code&gt;-topmost True&lt;/code&gt;) so it cannot be buried under other windows.&lt;/p&gt;


&lt;h2&gt;
  
  
  Setup: auto-start on Windows boot
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; Install dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pystray pillow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; Save &lt;code&gt;eye_reminder.py&lt;/code&gt; to a permanent location:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\you\EyeReminder\eye_reminder.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; Create a startup shortcut. Press &lt;code&gt;Win+R&lt;/code&gt;, type &lt;code&gt;shell:startup&lt;/code&gt;, and create a shortcut there pointing to:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;pythonw&lt;/span&gt; &lt;span class="kd"&gt;C&lt;/span&gt;:\Users\you\EyeReminder\eye_reminder.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Using &lt;code&gt;pythonw&lt;/code&gt; instead of &lt;code&gt;python&lt;/code&gt; means no terminal window appears on startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt; Reboot. The app silently starts in your system tray. Right-click the tray icon to quit anytime.&lt;/p&gt;


&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;Before running with 20-minute intervals, test with 5 seconds:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TEST_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;INTERVAL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TEST_MODE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;BREAK_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TEST_MODE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Flip &lt;code&gt;TEST_MODE = False&lt;/code&gt; when you are happy with it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tkinter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Built-in — popup UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;winsound&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Built-in — alarm beeps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pystray&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;System tray icon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Pillow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required by pystray for the icon image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Core functionality (popup + alarm) works with zero installs. &lt;code&gt;pystray&lt;/code&gt; and &lt;code&gt;Pillow&lt;/code&gt; are optional the app works without them, just without the tray icon.&lt;/p&gt;


&lt;h2&gt;
  
  
  Full source
&lt;/h2&gt;
&lt;h2&gt;
  
  
  &lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Harivelu0" rel="noopener noreferrer"&gt;
        Harivelu0
      &lt;/a&gt; / &lt;a href="https://github.com/Harivelu0/eye-remainder" rel="noopener noreferrer"&gt;
        eye-remainder
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;eye-reminder&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A lightweight Python app that enforces the &lt;strong&gt;20-20-20 rule&lt;/strong&gt; for eye health on Windows.&lt;/p&gt;

&lt;p&gt;Every 20 minutes, a blocking popup appears with a loud alarm. You cannot dismiss it until a 20-second countdown finishes. No cheating.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;The 20-20-20 rule says: every 20 minutes, look at something 20 feet away for 20 seconds. Every reminder app I tried was too easy to dismiss. This one isn't.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;[20 min timer] --&amp;gt; ALARM fires --&amp;gt; Blocking popup appears
                                        |
                                   20-sec countdown
                                        |
                               "Continue" button unlocks
                                        |
                              Click Continue --&amp;gt; back to work
                              Click Stop    --&amp;gt; session ends
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Runs silently in the &lt;strong&gt;system tray&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loud beeping alarm&lt;/strong&gt; on every trigger (no audio files needed)&lt;/li&gt;
&lt;li&gt;Popup is &lt;strong&gt;always-on-top&lt;/strong&gt; and the X button is disabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Continue" button locked&lt;/strong&gt; until full 20-second countdown completes&lt;/li&gt;
&lt;li&gt;System tray icon with right-click &lt;strong&gt;Quit&lt;/strong&gt; option&lt;/li&gt;
&lt;li&gt;Built-in &lt;strong&gt;test mode&lt;/strong&gt; (5-sec interval) for quick verification&lt;/li&gt;
&lt;/ul&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;

&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;Windows&lt;/li&gt;

&lt;li&gt;Python 3.8+&lt;/li&gt;

&lt;li&gt;…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Harivelu0/eye-remainder" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;

&lt;/h2&gt;


&lt;p&gt;My eyes are noticeably less dry after two weeks of using this. Sometimes the best tool is the one you build in an afternoon because nothing else works the way you need it to.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>health</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
