<?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>Two Autoscaling Policies, Zero Coordination: Why More Signals Made Scaling Worse</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 20 Sep 2026 07:48:47 +0000</pubDate>
      <link>https://dev.to/techwithhari/two-autoscaling-policies-zero-coordination-why-more-signals-made-scaling-worse-1g02</link>
      <guid>https://dev.to/techwithhari/two-autoscaling-policies-zero-coordination-why-more-signals-made-scaling-worse-1g02</guid>
      <description>&lt;p&gt;I had a fleet of stateful worker instances behind an Auto Scaling Group  On-Demand&lt;br&gt;
floor for guaranteed baseline capacity, Spot instances above that for burst. Standard&lt;br&gt;
setup. Then we added a second scaling signal to fix a real gap in the first one, and&lt;br&gt;
the fleet started flipping size every 30–90 seconds. This is the story of why that&lt;br&gt;
happened, and why it wasn't a tuning problem.&lt;/p&gt;

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

&lt;p&gt;Each instance in the fleet holds a fixed pool of "slots"  a bounded number of&lt;br&gt;
concurrent stateful sessions it can serve. Autoscaling started with the obvious&lt;br&gt;
signal: CPU. A &lt;code&gt;TargetTrackingScaling&lt;/code&gt; policy on average CPU across the ASG, scaling&lt;br&gt;
out when the fleet got busy.&lt;/p&gt;

&lt;p&gt;It worked, until it didn't. CPU is a &lt;em&gt;fleet-wide average&lt;/em&gt;. An individual instance&lt;br&gt;
could be completely full  zero free slots, rejecting new sessions  while the&lt;br&gt;
fleet's average CPU still looked comfortable, because three other instances were&lt;br&gt;
idle. CPU told you the fleet was fine. The thing that actually mattered  was any&lt;br&gt;
single instance out of room  was invisible to it.&lt;/p&gt;

&lt;p&gt;So we added a second signal: a CloudWatch Alarm watching per-instance free-slot&lt;br&gt;
count, tied to a Step Scaling policy, firing when any instance hit zero free slots&lt;br&gt;
for a sustained window. This is a completely reasonable instinct  CPU is a lagging,&lt;br&gt;
indirect proxy for capacity; free-slot count is the real thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;Now there were two independent scaling authorities, reacting to two different&lt;br&gt;
metrics, with no way to reconcile a disagreement. CPU's policy would look at the&lt;br&gt;
fleet-wide average, see it drop after a burst, and scale in. The pool-alarm's policy&lt;br&gt;
would look at the resulting per-instance squeeze, see zero free slots, and scale&lt;br&gt;
right back out. Neither policy knew the other existed. Neither was wrong on its own&lt;br&gt;
terms. Together, they fought.&lt;/p&gt;

&lt;p&gt;Real observed behavior, pulled from &lt;code&gt;describe-scaling-activities&lt;/code&gt;: the fleet size&lt;br&gt;
flipping between 5 and 6 instances every 30 to 90 seconds, repeatedly, for minutes&lt;br&gt;
at a stretch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First attempt: tune the timing.&lt;/strong&gt; Lengthened the pool alarm's sustained-breach&lt;br&gt;
window from 3 minutes to 10, on the theory the two policies just needed more&lt;br&gt;
separation. Deployed. The real activity log afterward showed it was &lt;em&gt;still&lt;/em&gt;&lt;br&gt;
oscillating  just on a 5–6 minute cadence instead of 30–90 seconds. The timing&lt;br&gt;
change reduced frequency. It didn't touch the actual disagreement. Two authorities&lt;br&gt;
still existed, still had different opinions, still had no way to agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix: one authority, not two
&lt;/h2&gt;

&lt;p&gt;The right move wasn't tuning either policy. It was removing one of them.&lt;/p&gt;

&lt;p&gt;The metric that actually reflects real capacity is occupancy  slots in use divided&lt;br&gt;
by slots available, aggregated properly across the fleet, not two separate numbers&lt;br&gt;
each policy interprets differently. &lt;strong&gt;CloudWatch Metric Math&lt;/strong&gt; makes this&lt;br&gt;
computable: &lt;code&gt;occupied_per_instance = pool_max_size - avg(free_slots)&lt;/code&gt;, targeting a&lt;br&gt;
real utilization band (in our case, 75%).&lt;/p&gt;

&lt;p&gt;Two things worth knowing if you're building this yourself, because both cost real&lt;br&gt;
debugging time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SEARCH()&lt;/code&gt; is not supported inside CloudWatch Metric Alarms.&lt;/strong&gt; Only inside&lt;br&gt;
Dashboards and direct &lt;code&gt;GetMetricData&lt;/code&gt; calls. First attempt at a true fleet-wide&lt;br&gt;
aggregate (summing free slots across every instance, dynamically, without hardcoding&lt;br&gt;
instance IDs) used &lt;code&gt;SEARCH&lt;/code&gt; inside a plain Alarm's metric query. &lt;code&gt;PutMetricAlarm&lt;/code&gt;&lt;br&gt;
rejected it outright  not a syntax error, a hard platform limitation. Confirmed&lt;br&gt;
against AWS's own documentation, not assumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target Tracking policies support Metric Math as a separate, genuinely different&lt;br&gt;
feature from Alarms&lt;/strong&gt;  live since December 2022, and &lt;em&gt;not&lt;/em&gt; subject to the same&lt;br&gt;
&lt;code&gt;SEARCH&lt;/code&gt; restriction in the same way, because Target Tracking evaluates the metric&lt;br&gt;
expression directly rather than routing through the Alarms evaluation engine. This&lt;br&gt;
is the detail that made the fix possible: replace both old policies  the CPU&lt;br&gt;
target-tracking policy and the pool-based step-scaling alarm  with a single&lt;br&gt;
&lt;code&gt;TargetTrackingScaling&lt;/code&gt; policy computing occupancy via Metric Math.&lt;/p&gt;

&lt;p&gt;One authority. One metric that actually reflects real capacity. Nothing left to&lt;br&gt;
disagree with.&lt;/p&gt;

&lt;p&gt;Verified before applying  &lt;code&gt;terraform plan&lt;/code&gt; showed exactly the intended diff, one&lt;br&gt;
resource added, three removed, nothing else touched. Applied cleanly on the first&lt;br&gt;
real attempt. Confirmed afterward via &lt;code&gt;describe-policies&lt;/code&gt;: exactly one scaling&lt;br&gt;
policy exists. No oscillation since.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalizable part
&lt;/h2&gt;

&lt;p&gt;This wasn't really an AWS-specific bug. It's a distributed-systems pattern that&lt;br&gt;
shows up anywhere two control loops react to different observations of the same&lt;br&gt;
underlying system state, without a shared source of truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each loop is locally correct  it's doing exactly what its own metric tells it to.&lt;/li&gt;
&lt;li&gt;Neither loop is aware the other exists, let alone that it just undid the other's
decision.&lt;/li&gt;
&lt;li&gt;The oscillation isn't a bug in either policy. It's an emergent property of running
two of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix generalizes too: don't tune the disagreement, find the metric that actually&lt;br&gt;
represents the thing you care about, and have exactly one thing making the decision&lt;br&gt;
based on it. If you're tempted to add a second scaling signal because the first one&lt;br&gt;
has a real, valid blind spot  that blind spot is real, but the fix is usually a&lt;br&gt;
better single metric, not a second policy running in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS building blocks used&lt;/strong&gt;: EC2 Auto Scaling Group (On-Demand + Spot mix), Target&lt;br&gt;
Tracking Scaling Policies, CloudWatch Metric Math, CloudWatch Alarms, Step Scaling&lt;br&gt;
Policies, SNS (for the alert path that first surfaced the oscillation as noisy&lt;br&gt;
paging).&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>software</category>
    </item>
    <item>
      <title>I Spent a Month on Architecture The Migration Took Two Days</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 15 Sep 2026 07:22:06 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-spent-a-month-on-architecture-the-migration-took-two-days-2kij</link>
      <guid>https://dev.to/techwithhari/i-spent-a-month-on-architecture-the-migration-took-two-days-2kij</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg63hzcebwhwtzgtfh93c.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg63hzcebwhwtzgtfh93c.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
I recently moved a production backend service from one cloud provider to another. Solo. Zero downtime. Real users, real traffic, no maintenance window.&lt;/p&gt;

&lt;p&gt;The real reason I moved it: auto-scaling and spot instances  cheaper, flexible compute that scales up and down with demand  never worked cleanly on the old provider. I kept fighting it instead of benefiting from it. On top of that, I kept running into permission issues that had nothing to do with what I was actually trying to do. Both were signals the same thing: time to move to a cleaner setup, on infrastructure we already had access to, where I could design things properly instead of working around old debt.&lt;/p&gt;

&lt;p&gt;The actual migration took two days. Getting the architecture right took a month.&lt;/p&gt;

&lt;p&gt;I used to think migration was the hard part. It's not. Design is.&lt;/p&gt;

&lt;p&gt;Here's what that month actually looked like  including the parts I got wrong before I got them right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong turn I almost shipped
&lt;/h2&gt;

&lt;p&gt;The old system had a setup step: each machine needed to establish its own identity before it could do real work. The old approach was simple  configure one machine correctly, capture its state, copy that state onto every new machine. It had worked fine there for a long time.&lt;/p&gt;

&lt;p&gt;I assumed the same approach would work on the new provider. It didn't. Figuring out why took three tries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try 1&lt;/strong&gt;: captured the state, copied it to new machines. Every one failed the same way.&lt;/p&gt;

&lt;p&gt;I traced it back to how the original setup was done  through a remote session, by clicking an icon, instead of launching the software with a flag that tells it to use an isolated local config. Without that flag, the setup silently saved itself somewhere else entirely. The "source" machine was never actually configured right in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try 2&lt;/strong&gt;: fixed that, redid the setup, recaptured, recopied. Same failure, on a different fresh machine, even though I'd verified the source was correct this time.&lt;/p&gt;

&lt;p&gt;That ruled out my first theory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try 3&lt;/strong&gt;: tried a literal clone of the working machine, same hardware profile, no changes. Same failure. Ruled out hardware identity too.&lt;/p&gt;

&lt;p&gt;I also tried automating the manual setup step with a scripting tool, so no human would ever need to do it. It could find the right window  real progress  but couldn't actually type into it. A session can exist without a real display attached, and that turns out to matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual fix&lt;/strong&gt;: stop copying state entirely. Install fresh, log in through a plain config file, let each machine do its own setup from scratch. No copying, no cloning. It worked immediately, on the very next machine, with zero manual steps.&lt;/p&gt;

&lt;p&gt;The lesson: don't copy identity between machines. Let each one earn its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I didn't expect
&lt;/h2&gt;

&lt;p&gt;Before rolling this out for real, I ran an actual load test  real concurrent usage, not a guess.&lt;/p&gt;

&lt;p&gt;The new machine had the same CPU as the old one, just more memory. I assumed load would scale roughly the way it had before. It didn't. At light load, CPU sat around 22%. At moderate load, it jumped to 92%  nowhere near a straight line.&lt;/p&gt;

&lt;p&gt;I lowered the safe capacity limit by about half, based on that real number, instead of keeping the number that had worked on the old setup. More memory didn't matter here  the real limit was CPU, and the only way to find that was to actually test it.&lt;/p&gt;

&lt;p&gt;The same test also caught a real bug: one failed connection was silently blocking the next unrelated one, because two different failure types looked identical to my code. Only showed up under real concurrent load.&lt;/p&gt;

&lt;h2&gt;
  
  
  A trade-off I made on purpose
&lt;/h2&gt;

&lt;p&gt;Once more than one machine was handling traffic behind a shared entry point, a returning user could land on a different machine than before. Fixing that properly meant building a much more complex routing layer.&lt;/p&gt;

&lt;p&gt;I chose not to build it. The system already had a way to recover gracefully when that happened  it just set itself up fresh wherever it landed. Building the complex fix would have solved a problem the system already tolerated fine. I wrote the trade-off down clearly, including exactly when I'd need to revisit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the entry point on purpose
&lt;/h2&gt;

&lt;p&gt;Two reasonable options existed for routing traffic in: a simple passthrough, or a heavier one that handles encryption itself. The service already handled its own encryption. Picking the heavier option would've meant solving a problem I didn't have. I went with the simple passthrough.&lt;/p&gt;

&lt;p&gt;I also rolled it out in two small, safe steps instead of one big one: first, add the new entry point &lt;em&gt;alongside&lt;/em&gt; the old direct access, so I could prove it worked with zero risk. Only once that was confirmed did I remove the old path. Two small reversible steps beat one big risky one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling  the actual reason I moved
&lt;/h2&gt;

&lt;p&gt;This was the part I was most careful about, since it's what drove the whole move.&lt;/p&gt;

&lt;p&gt;The setup could scale  a small always-on baseline, with cheaper burst capacity added on top when needed. That part was straightforward.&lt;/p&gt;

&lt;p&gt;What I deliberately didn't do yet: build a fully automatic policy that scales on its own based on load. I didn't have real usage data yet, and an automatic policy built on a guess is worse than no policy  it either reacts too late or too early, and you won't know which until it's live.&lt;/p&gt;

&lt;p&gt;Instead, I scaled manually for the specific period it mattered, watching real alerts. That gave me real numbers under real load. Only after that did building a real automatic policy become a genuine next step instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing access properly this time
&lt;/h2&gt;

&lt;p&gt;Since permission friction was part of what pushed this move in the first place, I made sure not to carry the same problem forward. I reviewed every broad permission the new setup had, kept the ones that genuinely couldn't be scoped any narrower, and added explicit restrictions on the sensitive actions that could be. That review caught one real gap before it caused a problem  a resource was missing a tag that a new safety rule depended on, which would have accidentally blocked a legitimate action.&lt;/p&gt;

&lt;p&gt;The principle: design access narrowly from day one. It's much harder to tighten permissions later than to grant them as they're actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the pipeline, deliberately
&lt;/h2&gt;

&lt;p&gt;I set up the deployment pipeline to be fully automatic  any change that passes its checks deploys on its own, no manual approval step in between.&lt;/p&gt;

&lt;p&gt;This was a deliberate choice, not laziness. If every deploy needs someone to manually approve it, that someone becomes a bottleneck  and in practice, that someone is me, getting pulled in for routine changes that don't actually need a human gate. Automating it means any developer on the team can ship their own change without needing to loop me in every time. Fewer interruptions, faster shipping, same real safety controls just enforced differently  through proper checks and scoped permissions, not a manual click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerts for everything that can fail
&lt;/h2&gt;

&lt;p&gt;Simple rule I followed throughout: if something can fail silently, it needs an alert. Not one flat "something's wrong" notification  different problems need different urgency. A minor slowdown shouldn't page the same way a real outage does.&lt;/p&gt;

&lt;p&gt;Getting this right early meant I could rely on watching alerts instead of guessing, during the manual-scaling window above and afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retiring the old system carefully
&lt;/h2&gt;

&lt;p&gt;Once the new setup was proven, I didn't just delete the old one. Before removing anything, I checked what was still actually depending on it  and found one thing still quietly running against it that I would have broken. Instead of accepting that disruption, I fixed the actual gap first, confirmed nothing else was still depending on the old system, and only then removed it for good.&lt;/p&gt;

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

&lt;p&gt;None of these decisions were obvious going in. Each one meant testing an assumption instead of trusting it  that state would copy over cleanly, that load would scale the way it used to, that an old permission setup was fine to carry forward.&lt;/p&gt;

&lt;p&gt;The month wasn't spent writing documents. It was spent finding out which assumptions were wrong before they became real problems. That's what made the two-day execution possible  not extra planning for its own sake, but planning that had already been tested against reality.&lt;/p&gt;




&lt;p&gt;Curious if others have hit the same wall with auto-scaling or spot instances on a provider that just doesn't handle it cleanly  feels like a more common trigger for switching providers than people usually admit.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>aws</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Built a Production Grade ML Candidate Ranking System</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 09 Sep 2026 17:19:32 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-built-a-production-grade-ml-candidate-ranking-system-4od3</link>
      <guid>https://dev.to/techwithhari/i-built-a-production-grade-ml-candidate-ranking-system-4od3</guid>
      <description>&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Three months ago, I participated in the Redrob India Data &amp;amp; AI Challenge. Track 1: given 100,000 candidate profiles and a job description for a Senior ML/AI Engineer role, build an AI system that ranks candidates the way a great recruiter would  not by matching keywords, but by actually understanding who fits.&lt;/p&gt;

&lt;p&gt;The output needed to be a ranked CSV of the top 100 candidates, with reasoning for each pick.&lt;/p&gt;

&lt;p&gt;I'm a cloud and backend engineer, about 2 years into my career. I've never formally studied machine learning. I learned what I needed by building things. This felt like exactly that kind of problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Keyword Matching Fails
&lt;/h2&gt;

&lt;p&gt;Before jumping into what I built, here's the thing that bothered me about standard systems.&lt;/p&gt;

&lt;p&gt;A candidate who writes "built dense retrieval pipeline serving 50M+ queries" and another who writes "implemented embedding-based search system" are saying the same thing. BM25  the industry standard keyword matcher  sees two completely different documents because the words don't overlap.&lt;/p&gt;

&lt;p&gt;Meanwhile, a verbose mediocre engineer who keyword-stuffs their resume scores higher than a terse brilliant engineer who just ships.&lt;/p&gt;

&lt;p&gt;And humans doing this at scale have their own version of the same problem: juggling 20+ signals inconsistently across hundreds of profiles. Notice period, location, GitHub activity, company prestige, career trajectory, skill depth  no one can hold all of that in their head fairly.&lt;/p&gt;

&lt;p&gt;I wanted to build something that combines signals the way a thoughtful recruiter actually would.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Don't Depend on Any Single Signal
&lt;/h2&gt;

&lt;p&gt;The core insight I started with: &lt;strong&gt;no single signal is trustworthy alone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;BM25 misses semantic matches. Embedding similarity misses keyword-heavy roles. Skill matching without trajectory context rewards buzzword collectors. Trajectory scoring without behavioral signals misses the person who's perfect on paper but wants 6 months notice.&lt;/p&gt;

&lt;p&gt;So I built a 4-signal system where every signal checks the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Formula
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = (0.20 × BM25 + 0.30 × FAISS + 0.20 × Skill + 0.30 × Trajectory)
      × yoe_factor
      × ml_product_years_factor
      × gate_multiplier
      × behavioral_gate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me explain each piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Precompute Everything (once, ~1.7 hours)
&lt;/h2&gt;

&lt;p&gt;I split the pipeline into two stages. Stage 1 is the heavy lifting  run once, save everything to disk. Stage 2 is pure math  runs in under 2 minutes on CPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 1: BM25 (weight: 0.20)
&lt;/h3&gt;

&lt;p&gt;Classic keyword search using &lt;code&gt;BM25Okapi&lt;/code&gt;. I built a query from the JD  specific terms like "vector search", "FAISS", "learning to rank", "production ML", "retrieval"  and scored all 100K candidates against it.&lt;/p&gt;

&lt;p&gt;BM25 is fast and good at catching explicit keyword matches. It's also easily gamed, which is why it only gets 20% weight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 2: FAISS Cosine Similarity (weight: 0.30)
&lt;/h3&gt;

&lt;p&gt;I encoded all 100K career text descriptions using &lt;code&gt;BAAI/bge-small-en-v1.5&lt;/code&gt; into 384-dimensional vectors, then searched with the JD vector using FAISS IndexFlatIP.&lt;/p&gt;

&lt;p&gt;Why BGE-small over the more common &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;? On MTEB retrieval benchmarks, BGE-small scores 61.7 vs MiniLM's 56.9. That's an 8.4% improvement  and with the same 384 dimensions and similar inference speed. For a retrieval task specifically, it matters.&lt;/p&gt;

&lt;p&gt;BGE also uses asymmetric encoding: the JD (query) gets a prefix:&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;jd_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Represent this sentence for searching relevant passages: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jd_embed_text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candidate texts are encoded without prefix. This is the correct BGE usage for retrieval  and it makes a real difference in practice. Meta's Lead AI Engineer jumped from rank 13 to rank 9 after switching from MiniLM to BGE-small.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 3: Skill Quality (weight: 0.20)
&lt;/h3&gt;

&lt;p&gt;For every skill in the dataset, I computed &lt;code&gt;cosine_similarity(skill_embedding, jd_vector)&lt;/code&gt; to get a relevance score. Then for each candidate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skill_quality = Σ (skill_relevance × proficiency_weight × min(1.0, duration_months / 24))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "Expert in Vector Search for 36 months" beats "Beginner in Vector Search for 48 months". And only JD-relevant skills count  Figma proficiency doesn't help a ranking engineer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal 4: Trajectory (weight: 0.30)
&lt;/h3&gt;

&lt;p&gt;This is the most novel signal and the one I'm most proud of.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trajectory = 0.35 × production_score
           + 0.25 × pre_llm_score
           + 0.15 × still_coding
           + 0.10 × title_progression
           +        prestige_bonus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Production score&lt;/strong&gt;: I scanned career descriptions for production markers  "shipped", "deployed", "serving N users", "A/B test", "latency", "99th percentile". Engineers who actually shipped things use this vocabulary. Those who only researched don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-LLM ML depth&lt;/strong&gt;: Total months spent on scikit-learn, PyTorch, XGBoost, TensorFlow, FAISS, Spark MLlib. JD explicitly says "if you learned ML after the LLM wave, this role isn't for you." I implemented a soft ramp to 48 months instead of a hard cutoff  so someone with 3 skills × 18 months each scores properly instead of getting zeroed out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still coding&lt;/strong&gt;: GitHub activity score from the platform signals + coding keywords in current role. Penalizes engineers who've moved entirely into management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Title progression&lt;/strong&gt;: Senior/Lead/Principal/Staff titles get a 1.30× multiplier on the title progression component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prestige bonus&lt;/strong&gt;: This is data-driven from the candidates' own profile fields. Instead of a hardcoded list of "good companies", I read &lt;code&gt;industry&lt;/code&gt; and &lt;code&gt;company_size&lt;/code&gt; directly from the JSONL. Software + 10000+ employees → 0.15 bonus. Fintech/SaaS/AI + mid-size → 0.10 bonus. Only the FAANG tier (Google, Meta, Apple, etc.) gets a hardcoded 0.20 override. This means Razorpay, Zomato, Paytm automatically get classified correctly based on their industry + size  no manual list maintenance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Gates: Hard Filtering Before Ranking
&lt;/h2&gt;

&lt;p&gt;Some candidates shouldn't be in the top 100 regardless of score. I implemented hard gate multipliers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Multiplier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Honeypot (impossible profile data)&lt;/td&gt;
&lt;td&gt;× 0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-title (Operations Manager, HR, Accountant...)&lt;/td&gt;
&lt;td&gt;× 0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consulting-only career (TCS, Infosys, Wipro entire career)&lt;/td&gt;
&lt;td&gt;× 0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CV/Speech primary domain&lt;/td&gt;
&lt;td&gt;× 0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure research (no production evidence)&lt;/td&gt;
&lt;td&gt;× 0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework-only (only LangChain/LlamaIndex, no real ML)&lt;/td&gt;
&lt;td&gt;× 0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And a behavioral gate (range 0.50–1.20) that accounts for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notice period (≤30 days → +0.15, &amp;gt;120 days → -0.10)&lt;/li&gt;
&lt;li&gt;Location (Pune/Noida → +0.20 per JD)&lt;/li&gt;
&lt;li&gt;Willingness to relocate (+0.10)&lt;/li&gt;
&lt;li&gt;Overseas without relocation intent (-0.30)&lt;/li&gt;
&lt;li&gt;GitHub activity signals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Years of Experience Soft Multipliers
&lt;/h3&gt;

&lt;p&gt;The JD says 5–9 years. I implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 3.5yr → hard exclude (hard gate)&lt;/li&gt;
&lt;li&gt;3.5–5yr → &lt;code&gt;max(0.40, yoe / 5.0)&lt;/code&gt; soft penalty&lt;/li&gt;
&lt;li&gt;5–9yr → 1.0× (ideal)&lt;/li&gt;
&lt;li&gt;Above 9yr → taper: &lt;code&gt;max(0.85, 1.0 - (yoe - 9.0) × 0.02)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And separately: &lt;code&gt;ml_product_years&lt;/code&gt;  years spent in applied ML roles at product companies (not consulting). JD explicitly requires "4-5 years in applied ML at product companies." This was a direct scoring multiplier: &lt;code&gt;min(1.0, 0.50 + (ml_yrs / 4.0) × 0.50)&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reasoning Layer
&lt;/h2&gt;

&lt;p&gt;Every candidate gets a written explanation in the CSV. No LLM involved  all facts come directly from the raw profile.&lt;/p&gt;

&lt;p&gt;Three tiers based on rank:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranks 1–10&lt;/strong&gt;: Full evidence  company, ML years at product companies, GitHub activity score, and a recent work description snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5.9yr exp, currently Senior AI Engineer at Apple; top skills: scikit-learn,
TensorFlow, Python; 5.8yr at ML product companies github_activity=97;
Trivandrum, Kerala; immediately available (30d notice). Recent: Built and
shipped a production recommendation system at a marketplace product, going
from offline experimentation to live A/B test in 5 months.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ranks 11–50&lt;/strong&gt;: Summary with top skills, location, availability, and the most recent work description where not duplicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranks 51–100&lt;/strong&gt;: Gap analysis  honest about why they ranked lower, with specific concerns (job-hop rate, limited ML product tenure, semantic mismatch).&lt;/p&gt;

&lt;p&gt;I also de-duplicated reasoning  synthetic data had identical career descriptions, so I tracked seen descriptions and skipped the "Recent:" snippet for duplicates, preventing identical reasoning entries in the CSV.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problems I Actually Hit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;ml_product_years&lt;/code&gt; was computed but never wired in
&lt;/h3&gt;

&lt;p&gt;I had a whole function that computed years in applied ML at product companies. It was stored in &lt;code&gt;features.pkl&lt;/code&gt;. It was never referenced in &lt;code&gt;rank.py&lt;/code&gt;. The JD literally says this is a requirement. I caught it during a cross-read of the scoring formula. Fixed by adding it as a multiplier.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Prestige was hardcoded
&lt;/h3&gt;

&lt;p&gt;My first version had a list of "good companies" hardcoded. That's not data-driven  it's bias. Switched to reading &lt;code&gt;industry&lt;/code&gt; + &lt;code&gt;company_size&lt;/code&gt; from the JSONL. Razorpay (Fintech + 5000+ employees) now auto-classifies correctly. Infosys (IT Services) auto-penalizes. No manual list maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pre-LLM 24-month hard cutoff
&lt;/h3&gt;

&lt;p&gt;I had &lt;code&gt;if pre_llm_months &amp;lt; 24: score = 0&lt;/code&gt;. A candidate with 3 skills × 18 months each = 54 months total but no single skill over 24 months got zeroed out. Wrong. Switched to &lt;code&gt;min(1.0, deep_months / 48.0)&lt;/code&gt;  a soft ramp to 48 months.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. HuggingFace rejecting binary files via git
&lt;/h3&gt;

&lt;p&gt;HF now uses Xet storage for binary files and rejects them via regular git push. I had 154MB FAISS index and 175MB BM25 index. Solution: uploaded them via &lt;code&gt;huggingface_hub&lt;/code&gt; Python API to a separate HF Dataset repo (&lt;code&gt;Haripvelu/redrob-artifacts&lt;/code&gt;), then used &lt;code&gt;hf_hub_download()&lt;/code&gt; in the demo to fetch them on first run.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;TODAY&lt;/code&gt; hardcoded
&lt;/h3&gt;

&lt;p&gt;I hardcoded &lt;code&gt;TODAY = date(2026, 6, 13)&lt;/code&gt; during development and forgot to change it. It was used for computing "days since last active" in the behavioral gate. Fixed to &lt;code&gt;TODAY = date.today()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Reasoning cut mid-sentence
&lt;/h3&gt;

&lt;p&gt;I was truncating reasoning at 200 characters with a word-boundary cut. "Built and shipped a production recommendation system at a marketplace product, going from..." got cut after "going" sometimes. Switched to sentence-boundary detection using &lt;code&gt;rfind('.')&lt;/code&gt; within 400 chars.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Top 10&lt;/strong&gt;: Apple, Salesforce, Mad Street Den, Zomato, Amazon, Razorpay, Ola, Microsoft, Meta, Netflix. All product companies. All India-based. All 5–9yr YoE range. All ML/NLP/search roles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After switching to BGE-small&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meta Lead AI Engineer: rank 13 → rank 9 ✅&lt;/li&gt;
&lt;li&gt;Zomato Senior ML Engineer: rank 8 → rank 4 ✅&lt;/li&gt;
&lt;li&gt;Google Search Engineer: rank 17 → rank 40 ✅ (their actual skills were SAP, YOLO, Kubeflow  not a semantic match for NLP/search despite the "Google" halo)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;62/62 custom eval checks passing (format validation, artifact integrity, signal sanity, adversarial probes, ranking quality)&lt;/li&gt;
&lt;li&gt;48/48 unit tests passing&lt;/li&gt;
&lt;li&gt;0 honeypots in top 100&lt;/li&gt;
&lt;li&gt;0 consulting-only in top 100&lt;/li&gt;
&lt;li&gt;validate_submission.py (official checker) passes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Runtime&lt;/strong&gt;: ~1.7 hours precompute (one-time), under 2 minutes to rank 100K candidates on CPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Learning to Rank&lt;/strong&gt;: Replace the hand-tuned weights with LambdaMART trained on historical recruiter feedback. The 0.20/0.30/0.20/0.30 weights were arrived at through experimentation and intuition  real feedback data would make them rigorous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM Re-ranker on top 50&lt;/strong&gt;: Add a small local model (Phi-3 Mini) to re-rank the top 50 after the initial retrieval. Still CPU-friendly, but adds semantic reasoning the embedding similarity can't capture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic JD embedding&lt;/strong&gt;: The JD embed text is currently hardcoded in &lt;code&gt;precompute.py&lt;/code&gt;. The pipeline should accept any JD and re-embed at precompute time. That makes it a general-purpose ranking system, not a one-role system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Months Later
&lt;/h2&gt;

&lt;p&gt;I submitted everything. GitHub, HuggingFace Space with live demo, ranked CSV, PDF deck answering all the required questions. I waited.&lt;/p&gt;

&lt;p&gt;Still waiting.&lt;/p&gt;

&lt;p&gt;I don't know if results were announced privately, if the challenge was extended, or if it quietly ended. I'm not bitter  I got something valuable regardless: a real end-to-end ML pipeline I built from scratch under pressure, a working understanding of hybrid retrieval systems, and a few bugs caught that I'd have shipped to production.&lt;/p&gt;

&lt;p&gt;If you're building something similar  a hiring system, a recommendation engine, a search ranker  the core lesson is: &lt;strong&gt;don't depend on any single signal&lt;/strong&gt;. Ensemble them. Gate the obvious failures hard. Be honest in your reasoning. And build the eval suite before you think you need it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Harivelu0/redrob-ranker" rel="noopener noreferrer"&gt;github.com/Harivelu0/redrob-ranker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://huggingface.co/spaces/Haripvelu/redrob-ranker" rel="noopener noreferrer"&gt;huggingface.co/spaces/Haripvelu/redrob-ranker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Space runs the full BM25 + FAISS pipeline (not a fake demo)  the large indexes are fetched from HF Dataset on first click. Takes ~45 seconds to download, then ranks instantly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Haripriya (hp). I'm a software engineer focused on cloud, backend, and ML infrastructure. I write about building real things.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>data</category>
    </item>
    <item>
      <title>Why I Stopped Clicking "Create Alert" in the Cloud Console</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Sun, 06 Sep 2026 15:12:06 +0000</pubDate>
      <link>https://dev.to/techwithhari/why-i-stopped-clicking-create-alert-in-the-cloud-console-55pm</link>
      <guid>https://dev.to/techwithhari/why-i-stopped-clicking-create-alert-in-the-cloud-console-55pm</guid>
      <description>&lt;h2&gt;
  
  
  What I used to do
&lt;/h2&gt;

&lt;p&gt;Every time I set up a new server, I'd go into the cloud console and manually click through the UI to set up alerts  "tell me if CPU goes above 80%", "tell me if this server stops responding", that kind of thing.&lt;/p&gt;

&lt;p&gt;It worked. For one server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it broke
&lt;/h2&gt;

&lt;p&gt;When I had 5 servers instead of 1, I noticed something: only some of them had alerts. A couple of new servers had none at all.&lt;/p&gt;

&lt;p&gt;Why? Because setting up alerts by clicking through the console is a manual step. And manual steps get forgotten. I'd create a new server, get it running, move on to the next thing  and just... not go back and click through the alert setup again.&lt;/p&gt;

&lt;p&gt;Nobody did anything wrong. It's just what happens when "add monitoring" depends on a human remembering to do it every single time.&lt;/p&gt;

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

&lt;p&gt;Instead of clicking through the console, I wrote a small script that creates the alerts for me  using code, not clicks.&lt;/p&gt;

&lt;p&gt;Now, whenever I create a new server, I run the same script, and it automatically sets up the same alerts every time: heartbeat check (is it alive?), CPU check, error-rate check. Every server gets the exact same coverage, because it's the same script every time  not me remembering to click 10 buttons in the right order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is better
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (manual clicking):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to forget for a new server&lt;/li&gt;
&lt;li&gt;Easy to accidentally set it up slightly differently each time&lt;/li&gt;
&lt;li&gt;No record of what alerts exist or why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After (script):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every new server automatically gets the same alerts&lt;/li&gt;
&lt;li&gt;The script itself is proof of what alerts exist  I can read the code&lt;/li&gt;
&lt;li&gt;If I want to change something (like the CPU threshold), I change it once in the script, and it's correct for every server going forward&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you're creating something by clicking through a UI more than once, that's usually a sign you should write a script instead. It's not about being fancy  it's that scripts don't forget, and clicking does.&lt;/p&gt;

&lt;p&gt;This applies to alerts, but it applies to almost anything repetitive in the cloud: creating servers, setting permissions, configuring backups. If you do it more than once by hand, write it down as code once, and just run that instead.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>monitoring</category>
      <category>virtualmachine</category>
    </item>
    <item>
      <title>How i did the Multi VM monitoring</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:09:08 +0000</pubDate>
      <link>https://dev.to/techwithhari/how-i-did-the-multi-vm-monitoring-2ef1</link>
      <guid>https://dev.to/techwithhari/how-i-did-the-multi-vm-monitoring-2ef1</guid>
      <description>&lt;h1&gt;
  
  
  I Had 5 Servers Sending Metrics  But Couldn't Tell Which One Was Which
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;I had one server. It sent metrics (CPU, errors, etc.) to my monitoring dashboard. Fine.&lt;/p&gt;

&lt;p&gt;Then I added more servers  same service, running on 4-5 machines now.&lt;/p&gt;

&lt;p&gt;All the metrics from all the machines showed up in the &lt;em&gt;same dashboard&lt;/em&gt;, but with no way to tell which machine sent which metric. If CPU spiked, I couldn't tell if it was server 1 or server 4. If errors went up, same problem.&lt;/p&gt;

&lt;p&gt;This is useless when something breaks. "Something is wrong" isn't helpful  I need "server 3 is wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happened
&lt;/h2&gt;

&lt;p&gt;Each server was supposed to tag its own metrics with its own name  like a label saying "this metric came from server-3."&lt;/p&gt;

&lt;p&gt;I had set up a config value meant to do exactly that. But when I checked, that name was never actually reaching the code that sends metrics. The setting existed in a file, but it wasn't making it into the running program the way I assumed it would.&lt;/p&gt;

&lt;p&gt;So every server, silently, was sending metrics with no name attached  or the same default name. No error. No warning. It just quietly didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed it
&lt;/h2&gt;

&lt;p&gt;Instead of hoping a config file setting would "just work" and reach the right place automatically, I made it explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a server starts up, its name gets set directly, as a hard requirement.&lt;/li&gt;
&lt;li&gt;That name gets passed by hand, step by step, into the exact piece of code that sends metrics.&lt;/li&gt;
&lt;li&gt;Nothing is assumed to "just be available"  it's handed over directly, on purpose, every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked it worked by looking at the actual metrics in the dashboard afterward and confirming each server's name showed up correctly.&lt;/p&gt;

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

&lt;p&gt;If your app depends on a setting "just working" through some automatic config-loading magic  double check it's actually arriving where you think it is. Don't assume. Go look.&lt;/p&gt;

&lt;p&gt;And before you scale from 1 server to many: make sure you can tell them apart in your monitoring &lt;em&gt;before&lt;/em&gt; you actually need to, during a real problem, at 2am, with no way to know which machine to even look at.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>devops</category>
      <category>cloud</category>
      <category>virtualmachine</category>
    </item>
    <item>
      <title>I did Golden Images</title>
      <dc:creator>Haripriya Veluchamy</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:23:15 +0000</pubDate>
      <link>https://dev.to/techwithhari/i-did-golden-images-1np8</link>
      <guid>https://dev.to/techwithhari/i-did-golden-images-1np8</guid>
      <description>&lt;h1&gt;
  
  
  Golden Images  How I Stopped Manually Logging Into Every New Server
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every time I spun up a new server for a service, it worked  but it wasn't actually &lt;em&gt;ready&lt;/em&gt;. There was always one manual step left: log in, run through some interactive setup, get the application into a working state. Only after that could the server actually do its job.&lt;/p&gt;

&lt;p&gt;For one server, that's a minor annoyance. For a fleet that's supposed to scale up and down on demand, it's a dealbreaker. You can't call something "automated provisioning" if a human still has to remote in and click through a setup wizard before it's usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: capture the setup once, replay it everywhere
&lt;/h2&gt;

&lt;p&gt;The pattern here is usually called a &lt;strong&gt;golden image&lt;/strong&gt;  and the idea is simple: instead of repeating a manual setup step on every new machine, do it once, capture the &lt;em&gt;result&lt;/em&gt; of that setup, and have every future machine apply that captured state automatically during provisioning.&lt;/p&gt;

&lt;p&gt;Concretely, I built a small tool that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connects to a machine that's already been through the manual setup and is in a known-good state.&lt;/li&gt;
&lt;li&gt;Packages up just the state that setup actually produced  not the whole machine, just the specific files/config that resulted from the manual steps.&lt;/li&gt;
&lt;li&gt;Uploads that package to storage, versioned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the provisioning script for every &lt;em&gt;new&lt;/em&gt; machine downloads that package and applies it automatically as part of boot  no human, no remote session, no wizard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake worth mentioning
&lt;/h2&gt;

&lt;p&gt;My first version of this captured too much. Instead of packaging just the setup-derived state, it grabbed an entire application data folder  which included the application's own installed binaries, not just the configuration that setup had produced.&lt;/p&gt;

&lt;p&gt;That meant every new machine, when it applied the "golden" package, got its fresh application install silently overwritten with whatever binary version happened to be running on the machine I captured from. New servers ended up running an older version of the software than the one they'd just installed  a regression that was confusing to trace, because nothing had "changed" from the provisioning script's point of view.&lt;/p&gt;

&lt;p&gt;The fix was narrowing the capture to exactly the two or three subfolders that actually held setup-derived state, and leaving the application's own installation untouched. Smaller package, and it stopped fighting the install step instead of complementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth doing before you scale
&lt;/h2&gt;

&lt;p&gt;The manual-setup step is easy to live with when you have one or two servers  it's a one-time cost. It stops being easy to live with the moment you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autoscaling&lt;/strong&gt;  a new instance needs to be usable within seconds of being created, with zero human involvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster recovery&lt;/strong&gt;  if a machine dies, replacing it shouldn't require someone to remember and redo a manual setup checklist from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;  every machine that skips the manual step (or where someone does it slightly differently) is a machine that behaves subtly differently from the rest of the fleet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A golden image turns "someone has to set this machine up" into "every machine sets itself up, identically, using the same captured state." It's the same principle as infrastructure-as-code, applied to the &lt;em&gt;data&lt;/em&gt; a machine needs rather than just its configuration.&lt;/p&gt;

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

&lt;p&gt;If any part of your provisioning process still requires a human to log in and do something interactive  even something that "only takes two minutes"  that's the step blocking real automation. Capture what that manual step actually produces, package just that, and make applying it part of the automated boot process.&lt;/p&gt;

&lt;p&gt;And when you build the capture step: be precise about what you're capturing. It's tempting to grab "the whole folder" for simplicity, but bundling setup-derived state together with things that shouldn't be frozen (like application binaries) turns your automation tool into a tool that silently reintroduces old versions. Capture the smallest thing that actually represents "the setup happened," and nothing more.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>automation</category>
      <category>infrastructure</category>
    </item>
    <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>
  </channel>
</rss>
