<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Beyond Localhost - Medium]]></title>
        <description><![CDATA[Engineering that ships. War stories first: incidents, migrations, benchmarks and the on-call reality behind the slides. System design, infra, Kubernetes, backend. No hype, no product pitches. Want to write for us? Open the Write for Us tab. - Medium]]></description>
        <link>https://medium.com/beyond-localhost?source=rss----7ab1ae1562b8---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Beyond Localhost - Medium</title>
            <link>https://medium.com/beyond-localhost?source=rss----7ab1ae1562b8---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 10 Nov 2026 15:47:22 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/beyond-localhost" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[My upgrade checker said clean. Twelve services disagreed.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/beyond-localhost/my-upgrade-checker-said-clean-twelve-services-disagreed-d33bb0674d01?source=rss----7ab1ae1562b8---4"><img src="https://cdn-images-1.medium.com/max/1400/1*tnpQ4kK9z_S24mDinv48Rg.png" width="1400"></a></p><p class="medium-feed-snippet">I wrote a tool to catch Kubernetes deprecations before an EKS upgrade. On the 1.36 upgrade it reported nothing, and so did EKS Upgrade&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/beyond-localhost/my-upgrade-checker-said-clean-twelve-services-disagreed-d33bb0674d01?source=rss----7ab1ae1562b8---4">Continue reading on Beyond Localhost »</a></p></div>]]></description>
            <link>https://medium.com/beyond-localhost/my-upgrade-checker-said-clean-twelve-services-disagreed-d33bb0674d01?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/d33bb0674d01</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[deployment]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Heinan Cabouly]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 17:01:02 GMT</pubDate>
            <atom:updated>2026-11-09T17:01:01.874Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The Model Shouldn’t Own My Desktop. The Execution Boundary Should.]]></title>
            <link>https://medium.com/beyond-localhost/the-model-shouldnt-own-my-desktop-the-execution-boundary-should-49fa38b3f813?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/49fa38b3f813</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 02:31:01 GMT</pubDate>
            <atom:updated>2026-11-09T02:31:01.663Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Two side-by-side macOS desk setups: a warm, personal workspace on the left and a darker computer workspace on the right, representing separate spaces for a person and an AI agent." src="https://cdn-images-1.medium.com/max/1024/1*gzUhtuTDxA7p8QAFM8htnA.png" /><figcaption>Conceptual illustration of a private macOS workspace and a separate agent workspace. AI-generated; not a product screenshot.</figcaption></figure><p>One of the stranger bugs I ran into while building Mac MCP looked, at first, like a successful browser action. An AI agent entered text in a web editor. The words appeared on the screen, and the tool returned successfully. But the page hadn&#39;t accepted the edit in its own internal state. When it rerendered, the text disappeared.</p><p>That bothered me more than a straightforward error would have. If a tool fails loudly, at least you know where to look. Here the visible result and the real result had parted company, and the agent couldn&#39;t tell.</p><p>I build Mac MCP, a local service that lets AI clients work with macOS apps, browsers, files and commands. At first it&#39;s easy to focus on the model: which one plans better, how many tools it can call, how often a task finishes without help. I care about those things. But the bugs that changed how I think about the product mostly happened after the model had already made its decision.</p><p>That&#39;s what I mean by the execution boundary. The model can ask to type into a page, open a file or run a command. A separate local service has to decide whether the request is allowed, whether the target is still the right one, and what it can honestly say happened afterward. Those are three different questions.</p><p>Say you ask an agent to write a reply and leave it as a draft. It&#39;s a small task: find the field, type the message, stop. One extra click can send it. To the model, that might be a minor mistake in an otherwise reasonable sequence of actions. To the person whose account it is, the consequence is quite different.</p><p>I don&#39;t want the model&#39;s confidence to override that instruction. Permission has to be checked against the actual operation, not against the model&#39;s explanation of why it thinks the operation is harmless. If the user said &quot;don&#39;t send,&quot; a convincing paragraph of reasoning shouldn&#39;t make Send an acceptable button to press.</p><p>The editor bug taught me the same lesson from the other direction. A tool reporting that it typed something doesn&#39;t prove the website saved it. Rich-text editors keep their own state, which doesn&#39;t always agree with the text you can see. In Mac MCP, I&#39;ve worked on distinguishing a visible insertion from a confirmed editor change. When that confirmation isn&#39;t there, the tool can report editor_model_unconfirmed rather than pretending the edit stuck. Even that isn&#39;t proof of a server-side save. It&#39;s a more precise account of what the tool actually checked.</p><p>Working with more than one agent exposed another weak assumption. With a single agent, it&#39;s easy to think the browser tab and focused field you inspected a moment ago still belong to your task. With two agents, one can change the page while the other is deciding what to do. A perfectly sensible click, aimed at yesterday&#39;s version of the page—or even the version from two seconds ago—can land somewhere entirely different.</p><p>I used to think of that mostly as an orchestration problem. It&#39;s also an execution problem. The tool needs to know which tab and element the action belongs to and when its information has gone stale. More reasoning tokens won&#39;t make an old screenshot current. Sometimes the right response is simply to refuse the action and ask the agent to observe again.</p><p>Then there&#39;s the less dramatic but very personal issue of focus. I want an agent to work while I&#39;m using my Mac. I don&#39;t want it dragging Safari to the front halfway through an email or taking over the keyboard while I&#39;m typing. I&#39;ve been testing ways to give agent work its own macOS Desktop instead of making every task compete for the same foreground workspace.</p><p>A separate Desktop helps with the interruption problem, but it doesn&#39;t magically solve permission or verification. The agent can still target the wrong thing. The browser can still reject a change. And some actions genuinely require foreground access. When they do, I&#39;d rather see an honest stop than a clever fallback that steals the screen.</p><p>Shell commands make these distinctions even more important. A delegated AI provider might describe its environment as sandboxed, but that description doesn&#39;t automatically cover what a separate local macOS service can execute. I have to look at where a restriction is actually enforced and which path, if any, can go around it. A sentence in a system prompt is useful guidance. It isn&#39;t an operating-system boundary.</p><p>The practical question I keep coming back to is: what happens if the model makes the wrong choice? Does a check outside the model prevent the action, or are we relying on the model to remember a rule it was told? I don&#39;t think it&#39;s fair to call those two arrangements equally safe.</p><p>I&#39;m also becoming more careful with the word &quot;done.&quot; If an agent submits a form, there is a difference between a click being dispatched, the browser accepting the action, and the server recording the result. A verification check can fail because the expected result isn&#39;t there. It can also fail to run at all because the service is unreachable. The second case is unknown, not proof that the action failed. Retrying the check is sensible; blindly repeating a purchase or a post isn&#39;t.</p><p>All of this makes some demonstrations less impressive. Extra checks add time. Permission prompts are interruptions. A stale-state error means the agent might need to try a different approach. But I&#39;m building something for a computer that belongs to a person, not a benchmark environment where the only thing that matters is a green completion badge.</p><p>I haven&#39;t solved every one of these problems in Mac MCP. Some protections are implemented; others are still being tightened, tested or redesigned. Writing down the boundary has made it easier to notice where I was treating a tool response as stronger evidence than it really was.</p><p>I still enjoy trying a new model. I just care more now about the questions that come after its plan: what was it allowed to touch, did it leave my own work alone, and can it show me what actually changed?</p><p>If the truthful answer is &quot;I tried, but I can&#39;t verify that it worked,&quot; I want the tool to say that. It&#39;s a much better starting point than an agent confidently telling me a job is finished when it isn&#39;t.</p><p>Author&#39;s note: I used AI assistance to edit this article. The examples and technical decisions discussed here come from my work on Mac MCP.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=49fa38b3f813" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/the-model-shouldnt-own-my-desktop-the-execution-boundary-should-49fa38b3f813">The Model Shouldn’t Own My Desktop. The Execution Boundary Should.</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our Autoscaler Took Down Production. Here’s What I Did in the First 30 Minutes.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/beyond-localhost/our-autoscaler-took-down-production-heres-what-i-did-in-the-first-30-minutes-2e4034307159?source=rss----7ab1ae1562b8---4"><img src="https://cdn-images-1.medium.com/max/1438/1*7-HirxNFE0uNjg3AF8nDVw.png" width="1438"></a></p><p class="medium-feed-snippet">The autoscaler did exactly what we told it to do, and that&#x2019;s what took us down.</p><p class="medium-feed-link"><a href="https://medium.com/beyond-localhost/our-autoscaler-took-down-production-heres-what-i-did-in-the-first-30-minutes-2e4034307159?source=rss----7ab1ae1562b8---4">Continue reading on Beyond Localhost »</a></p></div>]]></description>
            <link>https://medium.com/beyond-localhost/our-autoscaler-took-down-production-heres-what-i-did-in-the-first-30-minutes-2e4034307159?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/2e4034307159</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[site-reliability-engineer]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[DevopsLesson]]></dc:creator>
            <pubDate>Sun, 08 Nov 2026 02:31:01 GMT</pubDate>
            <atom:updated>2026-11-09T14:53:47.100Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Designing an Auction Service: Where I Chose Correctness, and Where I Didn’t]]></title>
            <link>https://medium.com/beyond-localhost/designing-an-auction-service-where-i-chose-correctness-and-where-i-didnt-a8a59347b656?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/a8a59347b656</guid>
            <category><![CDATA[concurrency]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <dc:creator><![CDATA[Ashutosh]]></dc:creator>
            <pubDate>Sat, 07 Nov 2026 18:01:02 GMT</pubDate>
            <atom:updated>2026-11-07T18:01:02.357Z</atom:updated>
            <content:encoded><![CDATA[<p>Auction systems look simple — highest bid wins, then the auction closes — until you add concurrency, money, and scale. Then every easy assumption falls apart. Here, the goal is to handle a decent load with <em>reasonable</em> correctness, and to be honest about where “reasonable” means “not perfect.”</p><h3>The problem</h3><p>A user registers. From there they can <strong>create auctions</strong> or <strong>participate as a bidder</strong> on someone else’s. Bids come in, and the highest valid bid at close wins. Simple to state, hard to do well.</p><p>The scale I designed for:</p><ul><li>~1 lakh (100k) running auctions per day</li><li>~10 million users</li><li>Hot auctions peaking around <strong>1k requests/sec on a single auction id</strong></li><li>A long tail of cold auctions nobody’s watching</li><li><strong>Consistency &gt; availability.</strong> If I have to choose, I’d rather reject a bid than accept a wrong one.</li></ul><h3>Out of scope</h3><p><strong>No cancellations.</strong> Once a bid is accepted, it stands. Cancellation of bid is not allowed</p><p><strong>Payment.</strong> I’m not designing the money movement — charging cards, settling with sellers. But I’ll present some ideas on it, because deciding <em>what to keep out of the hot path</em> is itself a core part of the design. “Payment is out of scope” is not the same as “money doesn’t exist.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PqcbH-2cPNGBdBt1r2F7Dw.png" /><figcaption>Auction Platform</figcaption></figure><h3>The shape of it</h3><p>Rather than one monolith, I split responsibilities into services with genuinely different load profiles and consistency needs:</p><ol><li><strong>CRUD service</strong> — auctions and users</li><li><strong>Search service</strong> — discovery</li><li><strong>Bidding path</strong> — Reverse Proxy → Bidding Service → Post Processor / Notification</li><li><strong>Audit path</strong> — Bids Audit Queue → OLAP consumer</li><li><strong>A CDC backstop</strong> from the Bids DB for added correctness</li></ol><p>Let me walk each one in the order a request actually flows.</p><h3>CRUD service</h3><p>The least interesting part, which is a good thing. It receives and persists auction and user data into a plain relational DB (MySQL is fine). Keeping basic properties of user and auction here (refer diagram)</p><p>No cleverness here. This data is low-write, read-heavy, and perfectly happy on a boring SQL database. Resist the urge to over-engineer the easy parts.</p><h3>Search service</h3><p>Discovery is powered by Elasticsearch, kept in sync with the auction table via CDC. ES will prioritise live and future auctions and actively clean out old ones. Nobody searches for an auction that ended three weeks ago. Keeping the index scoped to “things you can actually bid on” keeps it small, fast, and cheap.</p><p>CDC gatekeeps as a perfect trigger for this, example if status is changed to Done, give it lesser score in search, changed to archived remove from ES.</p><h3>The bidding path — this is the whole game</h3><p>Everything above was a warm-up. Bidding is where correctness and scale collide.</p><p>The first real decision: how do you survive a very hot auction without turning your whole fleet stateful? Stateful service are a headache to manage, stateless service with 1L requests? Good luck handling Db queries at that scale.</p><p>So, let’s build the middle ground.</p><p>Let’s build an approach, where nobody owns the particular auction but knows where to requests should land. So, first questions first, are all auctions same? No. Some receive low traffic, others high (let’s call them <strong>hot_auction</strong>) and other very high (calling <strong>very_hot_auction</strong>). Categorising each one of them and processing accordingly will help us with achieving the scale.<br>And the real bottleneck was never the request handling anyway; it’s the DB writes on very_hot_auctions. Spreading reads around doesn’t fix the thing that’s actually on fire.</p><h3>Reverse Proxy: detect heat early, without going stateful</h3><p>So instead of pushing routing logic into the API gateway, I put a bit of client-side logic on the reverse proxy and deployed it there. The pragmatic reason: it was far easier to maintain and ship changes to a proxy I own than to modify the entire gateway layer. Smaller blast radius, faster iteration.</p><p>What does that logic do? It answers one question cheaply: <em>is this auction hot right now?</em> And crucially, it doesn’t need an exact count to do that. I don’t care whether an auction got 973 or 1,041 requests this second — I care that it’s “heavy enough.” So I just keep an eye on the <em>recent</em> count of requests per auction id, and if it crosses a threshold, I treat it as a hot auction. Approximate, cheap, and — importantly — it keeps the design from going stateful just to make a routing decision.</p><p>The payoff of detecting it <em>here</em>, at the one place that sees all the traffic: no service downstream has to build its own detection logic. The proxy stamps a hot_auction header on the request, and everyone after it just consumes and trusts the header. Dumber downstream services are easier to reason about and test.</p><h4>Very High Load</h4><p>For scenario of very_hot_auction (again deduced from the configured request count), the proxy will add a from a bounded set (e.g. 0–9) of prefixes like ‘2’ to ‘auction_1’ and then process consistent hashing logic on combined key (i.e. 2_auction_1 instead of ‘auction_1’ ) making very_hot_auction to be distributed as ‘hot_auction’ across multiple consistent hashed servers.</p><p>Benefit: we only need to solve for normal and hot_auctions, any scale beyond that gets distributed.</p><h3>Bidding Service: the local-max trick</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YWWJKJhpO0doMIF-EO-5QA.png" /><figcaption>Bidding Service Flow</figcaption></figure><p>Under normal load, a bid is simple: check it beats the current winner, check it’s a valid multiple of the tick price, check the auction hasn’t ended, write winner bid to the Bids DB, respond success/ failure to the client.</p><p>Please note, DB write query is conditional with ‘new_bid_price &gt; current_winning_bid_price’. So among concurrent write attempt, higher bid will win even at DB.</p><p>Under <em>hot_auction</em> load, writing every bid straight to the DB would destroy it. So we buffer it in server memory for minimal period instead:</p><ul><li>Each bidding server keeps the local maximum bid it has seen for that auction, in memory.</li><li>It holds for a short window and only attempts to write its local max to the DB.</li></ul><p>The holding period is configurable, and it commits on <strong>whichever comes first:</strong> a request count on that server (say 100–200 bids buffered) <em>or</em> a time ceiling (100ms wait). Two knobs, two guardrails — the count protects the DB from a sudden burst, and the timer protects the <em>customer experience</em> so nobody waits too long for an answer. Together they cut DB load dramatically while keeping response time bounded.</p><p>Essentially we reduce the DB writes drastically to 100–1000x and protect it. Yes, it can happen server crashes while data is in memory. Well, in defense, first of all that happens in rare cases, we keep server numbers optimal. And even when that happens, for clients it will be a normal BAU retryable failure. Clients can retry.</p><h4>Let’s Add More Load</h4><p>Let’s say you add more load. As discussed above, proxy will distribute the load as ‘hot_auction’ among bidding service. Each service only will care about load locally. You know why? Cause max of (local max) is all we need.</p><p>Now picture a very_hot_auction spread across a few servers. Each holds its own local max. They all try to write to DB. The conditional write means only the true maximum price write succeeds — the others fail, re-read the latest winner, and move on. The invariant that makes this safe is simple:</p><blockquote><em>max(local maxes) is always the real maximum, no matter which server a request landed on.</em></blockquote><p>Correctness rides on that one conditional write at the DB — the single serialization point. That’s the trade I’m happiest with: I spread load across servers <em>and</em> keep a single source of truth, persistent DB.</p><h3>Serving reads from the Bidding Service itself</h3><p>Here’s a choice I’d defend hard: reads for the live bid are served straight from the Bidding Service’s memory. Why build an entire separate read path?</p><p>Every server handling a live auction <em>already has the current bid data in memory</em> — it needs it to validate incoming bids. So the cheapest correct thing is to serve “what’s the current bid?” right out of that in-memory state. I’d much rather do that than stand up a full read-replica fleet and a fancy CQRS pattern for a read that’s already sitting in RAM.</p><h3>Closing the auction correctly</h3><p>If I naively fire a “winner!” notification exactly at end_at.</p><p>So the Post Processor doesn’t trust the instant of end_at. It polls (using a partial index on status, end_at) for auctions whose end time is older than <em>now minus a grace window</em> — say 10 seconds. By then every server&#39;s buffer has drained, the conditional writes have settled, and the Bids DB genuinely holds the final winner. Then, and only then, the notification goes out.</p><p>A 10-second delay on “you won” is invisible to a user. A wrong winner that can’t be taken back is a disaster. Easy call.</p><h3>The audit path: where bid history actually lives</h3><p>Every single bid — winning or not — is published to a Bids Audit Queue.</p><p>That queue feeds an OLAP consumer (think ClickHouse). <em>That’s</em> where user-facing bid history lives: “you bid $400, you were outbid at $450, you bid $500…” By tuning the number of consumers I keep the lag in a comfortable range, and I get a cheap, queryable, append-only record of everything that ever happened — perfect for history views and dispute resolution, kept well off the hot transactional path.</p><p>The OLTP database stays lean (it only tracks the ratchet of the current max), and the “show me everything” workload goes to a store built exactly for it.</p><p>Right tool, right job.</p><h3>CDC as a correctness backstop</h3><p>Buffering bids in memory means a server can crash mid-window and lose bids it hadn’t flushed. I accept this — it’s the price of buffering — and for most auctions, a rare “something went wrong, try again” is a fine answer. Because responses only go out after the batch commits, a crash means the bidder gets an error, not a false “you’re winning.”</p><p>But when I want <em>stronger</em> guarantees, I lean on a CDC pipeline off the Bids DB. It can drive de-duplicated notifications to the user (who could have lost confirmation due to server crash). The point: I treat absolute accuracy as a dial, not a default.</p><h3>A word on the money movement</h3><p>Payment is out of scope, but keeping money <em>out of the hot path</em> is itself a design decision worth showing.</p><p>My preference: a block → confirm → release flow on the wallet service, kept off the live bidding path. Block some amount when a user registers, confirm via CDC when they win, release via TTL or CDC when they don’t. And set each user’s ceiling at registration based on credit/KYC — in a separate, offline flow, never in the hot path.</p><p>I’m not putting a synchronous wallet call in front of a hot auction. Pull funds before, or settle after. The variants you can offer — pre-fund a max bid, pay a token amount to participate, or KYC-only with a credit ceiling — are all just different answers to the same question: <em>what’s the minimum you must check in the hot path, and what can wait?</em></p><h3>Closing thought</h3><p>The interesting engineering isn’t in making the happy path work — it’s in deciding, explicitly, <em>how wrong you’re willing to be, and for how long.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a8a59347b656" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/designing-an-auction-service-where-i-chose-correctness-and-where-i-didnt-a8a59347b656">Designing an Auction Service: Where I Chose Correctness, and Where I Didn’t</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Our CI/CD Pipeline Took 24 Minutes. These 7 GitHub Actions Fixes Got It Under 7.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/beyond-localhost/github-actions-ci-cd-pipeline-speed-62c671d12f3c?source=rss----7ab1ae1562b8---4"><img src="https://cdn-images-1.medium.com/max/2000/1*oFRNvTgjLvZOP5kLwfNNFA.jpeg" width="2000"></a></p><p class="medium-feed-snippet">No new tools and no bigger runners. Just YAML changes you can copy into your repo today.</p><p class="medium-feed-link"><a href="https://medium.com/beyond-localhost/github-actions-ci-cd-pipeline-speed-62c671d12f3c?source=rss----7ab1ae1562b8---4">Continue reading on Beyond Localhost »</a></p></div>]]></description>
            <link>https://medium.com/beyond-localhost/github-actions-ci-cd-pipeline-speed-62c671d12f3c?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/62c671d12f3c</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[DevopsLesson]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 01:01:03 GMT</pubDate>
            <atom:updated>2026-11-05T16:45:27.597Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Festive Sales 2026: Digital Shelf Analytics and AI Optimization]]></title>
            <link>https://medium.com/beyond-localhost/festive-sales-2026-digital-shelf-analytics-and-ai-optimization-d1926c986eed?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/d1926c986eed</guid>
            <category><![CDATA[ecommerce-intelligence]]></category>
            <category><![CDATA[ai-optimization]]></category>
            <category><![CDATA[ecommerce-solution]]></category>
            <category><![CDATA[digital-shelf]]></category>
            <category><![CDATA[digital-shelf-analytics]]></category>
            <dc:creator><![CDATA[Minakshi Kushwaha]]></dc:creator>
            <pubDate>Sat, 31 Oct 2026 15:01:02 GMT</pubDate>
            <atom:updated>2026-10-31T15:01:02.549Z</atom:updated>
            <content:encoded><![CDATA[<h3>Win the Festive Season Sales with Digital Shelf Analytics &amp; Agentic Media Optimization by mFilterIt</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JTEV75ihWUrw5WVo.png" /></figure><p>The festive season is here, and once again it is the most demanding stretch of the year for ecommerce brands. Traffic peaks during the wave of sales on ecommerce and quick commerce platforms. Shopper intent is at its highest; the battle for the digital shelf, and ultimately the conversion, is real.</p><p>This year, the stakes are bigger. Redseer expected festive 2026 to be the strongest in five years, with online festive GMV of USD 15–16 billion, around 25% growth and 180–185 million shoppers. (<a href="https://redseer.com/reports/india-online-retail-2026-the-fastest-year-in-five-and-the-new-engines-behind-it/">Source: Redseer</a>) The first two days of sales alone crossed ₹24,000 crore, nearly six times a normal day. And two in every three online shoppers in those first two days came from smaller towns.</p><p>But the shape of the season has changed. Demand is spreading well beyond the metros, with online sales expected to grow by up to 29%. (<a href="https://www.fortuneindia.com/amp/story/business-news/up-to-29-growth-in-online-sales-expected-as-festive-demand-spreads-across-india/166095">Source: Fortune India</a>) Quick commerce is projected to grow 110–120% this festive period.</p><p>The season hasn’t been a straight line up, either. After the opening rush, sales fizzled as higher prices in the electronics category made shoppers hold back. Brands now have to win a shopper who is spread across more towns and compares prices harder than ever.</p><p>That means more shelves to watch, prices moving every hour, and more ad decisions than any team can make manually. Every missed keyword, every out-of-stock product and every lost rank still hands the sale to a competitor. What has changed is how fast it happens.</p><p>In this article, we look at the challenges brands face during festive sales in 2026, and how data-driven <a href="https://www.mfilterit.com/blog/power-of-digital-shelf-analytics">digital shelf analytics</a>, now paired with agentic AI, helps brands move from spotting a problem to fixing it in time.</p><h3>Challenges Ecommerce Brands Face During Festive Season Sales</h3><h3>Too Many Shelves, Too Little Visibility</h3><p>Your share of shelf is no longer one search page. It is Amazon, Flipkart, Myntra and Nykaa, plus Blinkit, Zepto, Instamart and newer quick commerce apps, each showing different results by city and even by dark store. A SKU can rank first in Bengaluru and be missing in Pune. Without SKU-level, location-level visibility, these gaps surface only after the sales are lost.</p><h3>Stockouts at the Worst Possible Moment</h3><p>Quick commerce transforms festive demand into quick decisions measured in minutes. If your product is not in stock in an important pin code during high-demand times, people will not stop; they will go with the next available item from the list. Moreover, many brands are spending money on advertising campaigns that bring them no profit.</p><h3>Competitive Pricing That Move by the Hour</h3><p>Flash deals, reseller undercutting and MAP violations spike during sales. Shoppers are better armed too: AI assistants like Amazon’s Rufus now show price history, so inflated “discounts” get exposed quickly. Brands need real-time visibility into their own and competitors’ pricing to respond without eroding margins or brand value.</p><h3>Product Pages Written for People, Now Read by AI</h3><p>AI shopping assistants and review summaries increasingly shape what shoppers see first. Incomplete titles, missing attributes, low-quality images and inconsistent content across sellers hurt both search ranking and how AI tools describe your product. Your PDP and reviews are now part of discovery, not just conversion.</p><h3>Media That Can’t See the Digital Shelf</h3><p>This is the biggest gap. Media sees the campaign: spend, clicks and ROAS. Ecommerce sees the market: rank, share of shelf, price and availability. During festive peaks, the two rarely meet. Bids keep climbing on keywords where you’re out of stock, budget stays locked on a platform where your price is uncompetitive, and a rival overtakes your hero keyword while campaigns run on autopilot. The result is wasted spend on low-intent areas and missed revenue where demand is real.</p><p>Click Here to Read Full Blog: <a href="https://tinyurl.com/2kr4hz9h">https://tinyurl.com/2kr4hz9h</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d1926c986eed" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/festive-sales-2026-digital-shelf-analytics-and-ai-optimization-d1926c986eed">Festive Sales 2026: Digital Shelf Analytics and AI Optimization</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[White Box Testing Beyond Localhost: The Code Paths That Only Run in Real Environments]]></title>
            <link>https://medium.com/beyond-localhost/white-box-testing-beyond-localhost-the-code-paths-that-only-run-in-real-environments-b5b3258613df?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/b5b3258613df</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[white-box-testing]]></category>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[localhost]]></category>
            <dc:creator><![CDATA[Sancharini Panda]]></dc:creator>
            <pubDate>Sat, 31 Oct 2026 01:01:01 GMT</pubDate>
            <atom:updated>2026-10-31T01:01:01.785Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="White Box Testing Beyond Localhost" src="https://cdn-images-1.medium.com/max/1024/1*iGR5UMgmxacumutMsAkIpw.png" /></figure><p>There’s a whole category of code that has never executed on a developer’s laptop, and probably never will. It’s the branch that only fires when an environment variable says “production.” It’s the fallback that kicks in when a secret manager is slow to respond, and the logic that switches behavior per cloud region. Local test runs can show 85% coverage while that code sits in the untouched 15%, waiting for its first real outing during a deploy.</p><p>White box testing is supposed to reveal exactly this kind of gap, since it’s all about knowing which code paths ran. But coverage is usually measured in one place: wherever the tests happen to run, which is often a laptop or a CI container that looks nothing like the deployed environment. Taking white box testing beyond localhost means measuring what runs in the places where code actually lives.</p><h3>The code paths local environments never reach</h3><p>Most environment-only paths fall into a few recognizable groups.</p><p>Configuration branches are the obvious ones. Code that checks whether it’s running in staging or production, reads region-specific settings, or enables stricter limits outside development. Locally, those branches take the default path every time.</p><p>Feature flags add more. A flag that’s on in production but off everywhere else means the “on” path only ever runs for real users. Once a rollout finishes, the “off” path often goes untested too, even though it still ships.</p><p>Credential and secret handling is another quiet one. Cloud SDKs resolve credentials through a chain of sources. The AWS SDKs, for example, check environment variables, shared config files and then the instance or container role. On a laptop, credentials usually come from a local profile. In a deployed container, they come from the role. Different branch, different failure modes, and only one of them ever gets exercised locally.</p><p>Then there’s everything tied to real infrastructure: retry behavior against managed databases, timeouts against real network latency, caching that only exists behind a CDN, and multi-tenant settings that only exist with real tenant data.</p><h3>Measuring white box coverage in a deployed environment</h3><p>The good news is that most ecosystems can measure coverage in a running service, not just inside a test runner.</p><p>Go’s a nice example. You can build the whole binary with coverage switched on, and when the program shuts down, it drops its coverage data into a folder you choose. Java folks usually reach for the JaCoCo agent instead. Attach it to the running service and you can grab a coverage dump whenever you like, no restart needed. Node writes coverage data when the NODE_V8_COVERAGE environment variable points at a directory. Python’s coverage.py can wrap a running service process too.</p><p>In practice, teams tend to ship a coverage-enabled build to staging, or to some short-lived test environment, and then throw realistic traffic at it for a while. When that’s done, they pull the coverage data and turn it into a report. What about production? Mostly, no. Instrumentation isn’t free, so it usually stays in staging and test environments, with maybe a small canary running it briefly if a team really wants production numbers.</p><h3>Driving realistic traffic through the environment</h3><p>Coverage only shows what got exercised, so the traffic matters as much as the instrumentation. A handful of smoke tests won’t reach the interesting branches.</p><p>Replaying real API traffic, captured from production or from regular usage of staging, is one of the most effective ways to drive a deployed service through realistic paths. It brings along the odd requests, unusual parameters and edge cases that scripted tests rarely include. For branches that real traffic rarely reaches, like error handling and failovers, deliberate tests still have to fill the gap.</p><h3>Finding the branches that only run in real environments</h3><p>Here’s where the comparison gets useful. Generate one coverage report from local or CI test runs, and another from the deployed environment under realistic traffic. Then compare them branch by branch.</p><p>Branches covered in both are the well-understood ones. Branches covered only in the deployed environment are exactly the environment-specific paths this whole exercise is hunting for, and they deserve explicit tests that pin their behavior down. Branches covered in neither are the riskiest of all. They might be dead code. Or they might be the production-only fallback that nobody’s ever seen run.</p><p>A quick review of that last group with the team that owns the service usually sorts it out fast. Some branches get deleted. Others get turned into deliberate tests, using a staging configuration or a flag combination that finally reaches them.</p><h3>Black box vs white box testing in real environments</h3><p>Black box and white box testing come at this from opposite ends. A black box test doesn’t know or care what the code looks like. It just hits the deployed service and checks whether the answer matches what was promised. <a href="https://keploy.io/blog/community/white-box-testing"><strong>White box testing</strong></a> goes the other way, using knowledge of the code to see which internal paths actually got exercised.</p><p>In a deployed environment, the two work best together. Black box tests confirm the service behaves correctly, and white box coverage shows how much of the environment-specific code those tests reached. On the tooling side, white box coverage typically comes from tools like JaCoCo, coverage.py, Go’s built-in coverage and c8 for Node, while black box API testing tools such as Keploy generate tests from real traffic and also report statement and branch coverage for those replayed runs, which links the outside view to the inside one.</p><h3>Keeping environment-specific paths tested over time</h3><p>A one-time coverage comparison is useful. Making it routine is better.</p><p>Teams that keep this up usually run the deployed-environment coverage pass on a schedule, or before major releases. They track how many branches are only covered outside localhost, and they treat any new environment-only branch as something that needs a deliberate test before it ships. Feature flags get special attention, with each flag state covered at least once and dead flags cleaned out regularly so their branches don’t linger.</p><p>Parity helps too. The closer staging configuration is to production, the more environment-specific paths get exercised before release, rather than for the first time in front of customers.</p><h3>Conclusion</h3><p>Coverage numbers measured on a laptop describe the code that runs on a laptop. Real environments switch on branches local runs never touch, from credential chains to feature flags to region-specific logic, and those branches often carry the highest stakes. White box testing that follows code into deployed environments finally puts numbers on that hidden part of the codebase.</p><p>And honestly, the comparison is where it gets interesting. Line up the laptop numbers next to the staging numbers and something usually jumps out, often a branch that, as far as anyone can tell, has only ever run for real customers. Writing a proper test for that one branch tends to shift how a team thinks about coverage for good.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b5b3258613df" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/white-box-testing-beyond-localhost-the-code-paths-that-only-run-in-real-environments-b5b3258613df">White Box Testing Beyond Localhost: The Code Paths That Only Run in Real Environments</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[3 AI Tools Every DevOps Engineer Should Know]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/beyond-localhost/ai-tools-devops-engineers-2026-21b5400219ff?source=rss----7ab1ae1562b8---4"><img src="https://cdn-images-1.medium.com/max/2000/1*v5GGcV5iX2SyH1Hxe9rYqA.jpeg" width="2000"></a></p><p class="medium-feed-snippet">One for your code, one for production, one for your pipeline: Amazon CodeGuru, Sysdig, and Harness.</p><p class="medium-feed-link"><a href="https://medium.com/beyond-localhost/ai-tools-devops-engineers-2026-21b5400219ff?source=rss----7ab1ae1562b8---4">Continue reading on Beyond Localhost »</a></p></div>]]></description>
            <link>https://medium.com/beyond-localhost/ai-tools-devops-engineers-2026-21b5400219ff?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/21b5400219ff</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[ai-tools]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[DevopsLesson]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 18:01:01 GMT</pubDate>
            <atom:updated>2026-10-30T20:39:29.388Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[N+1 Queries: Your ORM Is Lying to You and Your Database Is Paying the Bill]]></title>
            <link>https://medium.com/beyond-localhost/n-1-queries-your-orm-is-lying-to-you-and-your-database-is-paying-the-bill-03a0f184285e?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/03a0f184285e</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[backend-engineering]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[database-design]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[Nsikan Patrick Adaowo]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 11:01:02 GMT</pubDate>
            <atom:updated>2026-10-30T11:01:01.960Z</atom:updated>
            <content:encoded><![CDATA[<p>Your ORM is supposed to make database access easier, so why is your application making hundreds of database queries to return a single response?</p><p>You define your models, establish your relationships, and let the ORM handle the details. No complicated SQL. No messy joins. Just clean, readable application code.</p><p>Except there’s a problem.</p><p>That innocent-looking loop might be querying the database once for every record it retrieves. Your code stays simple, but your database pays for every additional round trip.</p><p>And adding an index might make those queries faster without fixing the underlying problem.</p><p>The issue isn’t always how efficiently you’re querying your database. Sometimes, it’s how many times you’re querying it in the first place.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*A-0eBZkVVeda8TDw.png" /></figure><p>This is the N+1 query problem. And it’s particularly frustrating because the code that introduces it can look perfectly reasonable.</p><p>An ORM promises to make database interactions easier to work with. It gives you models, relationships, and convenient methods for retrieving data. But convenient database access is not the same thing as efficient database access.</p><p>Your application code might look simple while the database handles a workload that was never necessary.</p><h3>What Exactly Is an N+1 Query Problem?</h3><p>Imagine you’re building a SaaS application with users and their projects.</p><p>You need an endpoint that returns a list of users alongside the projects each user owns.</p><p>The data model is straightforward:</p><ul><li>A User can own many Project records</li><li>Each Project belongs to one User.</li></ul><p>In a relational database, this is a familiar one-to-many relationship.</p><p>The naive implementation might look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UzeJXFGDNuwFN35Q.png" /></figure><p>Nothing about this code looks particularly alarming.</p><p>You retrieve the users, loop through them, and fetch each user’s projects. The code is readable, the relationship is clear, and the response has the shape you need.</p><p>But look at what happens at the database level.</p><p>The first query retrieves the users:</p><pre>SELECT *<br>FROM &quot;User&quot;;</pre><p>Then, for every user returned, the application executes another query:</p><pre>SELECT *<br>FROM &quot;Project&quot;<br>WHERE &quot;userId&quot; = $1;</pre><p>If the first query returns 50 users, the database receives:</p><ul><li>1 query to retrieve the users.</li><li>50 additional queries to retrieve their projects.</li></ul><p>That’s 51 queries to construct one response.</p><p>If the endpoint returns 500 users, it can execute 501 queries.</p><p>This is why the problem is called N+1: one initial query, followed by N additional queries.</p><p>The exact number depends on the data and the code path, but the underlying issue remains the same: the number of database queries grows with the number of records returned by the initial query.</p><p>And that growth can happen without any obvious change to the service’s complexity or readability.</p><h3>Why N+1 Queries Are More Expensive Than They Look</h3><p>A common mistake is to assume that an individual query is cheap, so executing many of them must also be cheap.</p><p>That assumption ignores the work surrounding each query.</p><p>A database query isn’t just the time PostgreSQL spends executing SQL. The application and database must also coordinate the operation.</p><p>Depending on the driver, connection pool, network, and execution path, each query may involve:</p><ol><li>Sending a request from the application to the database.</li><li>Parsing, planning, or executing the statement as applicable.</li><li>Reading or modifying the relevant data.</li><li>Returning the result to the application.</li><li>Converting the result into application-level objects.</li></ol><p>Some of these costs can be reduced through prepared statements, connection reuse, or other optimizations. But repeating a query still creates work.</p><p>Consider a simplified example.</p><p>Suppose each additional query takes an average of 5 milliseconds from the application’s perspective, including the relevant round-trip and processing overhead.</p><p>With 100 users, the endpoint executes 101 queries.</p><p>If those additional queries run sequentially, the repeated database operations alone could account for roughly 500 milliseconds, before adding the initial query, serialization, and other application work.</p><p>That number is illustrative, not a performance benchmark. Actual latency depends on the environment and workload.</p><p>The important point is that a query taking only a few milliseconds can become a serious problem when repeated hundreds of times.</p><p>And sequential execution isn’t the only concern.</p><h3>The database is doing unnecessary work</h3><p>Every additional query consumes some combination of database CPU, connection time, query execution resources, memory, and I/O.</p><p>Even when an index makes each lookup efficient, the database still has to process every request.</p><p>An index can make a query faster. It cannot eliminate the need to execute that query 100 times.</p><h3>Your connection pool becomes a bottleneck</h3><p>Database connections are finite resources.</p><p>When an application generates large numbers of queries, those queries must compete for available connections and database capacity.</p><p>This becomes particularly problematic when multiple requests are doing the same thing simultaneously.</p><p>One endpoint might execute 101 queries. Fifty concurrent requests could generate thousands of database operations in a short period.</p><p>The result can be increased connection wait time, growing latency, and reduced throughput.</p><h3>Your tail latency gets worse</h3><p>Users don’t experience your endpoint as a collection of individually fast queries. They experience the total time required to receive a response.</p><p>A route that performs unnecessary database operations can have acceptable average latency under light traffic and terrible p95 or p99 latency under load.</p><p>This is why N+1 problems often become obvious only after an application starts receiving meaningful traffic.</p><p>The application didn’t suddenly become badly written. The workload exposed an inefficiency that was already present.</p><h3>The ORM Didn’t Lie. It Hid the Cost.</h3><p>Let’s be precise about the title.</p><p>Your ORM isn’t necessarily generating bad SQL. In the example above, each individual SQL statement is reasonable.</p><p>The problem is the way the application uses the ORM.</p><p>Object-relational mappers make relational data easier to work with by representing tables and relationships as application-level objects. That abstraction is useful, but it can make the cost of accessing related records less obvious.</p><p>Consider this TypeORM example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tDOIDEz9HJoO9kbx.png" /></figure><p>The repository methods are straightforward. The relationship is expressed through objects rather than handwritten SQL.</p><p>But the database still sees a query for the users followed by a query for each user’s projects.</p><p>The same pattern can appear in Prisma, TypeORM, Sequelize, Django ORM, Hibernate, and other data-access tools.</p><p>The syntax changes. The database workload does not.</p><p>This is an important distinction: <strong>an ORM can simplify how you express a query without guaranteeing that the overall query strategy is efficient.</strong></p><p>A method call that looks like a single operation at the application level may generate multiple SQL statements. And a loop containing a database call can multiply those statements in ways that are easy to overlook during code review.</p><p>The right question isn’t simply, “Does this code use an ORM correctly?”</p><p>It’s also, “How many database operations does this code generate for a typical request?”</p><h3>The First Fix: Fetch Related Data Intentionally</h3><p>Let’s return to the original requirement.</p><p>We want to retrieve users and their projects.</p><p>We don’t actually need to retrieve each user’s projects independently. We already know the complete set of users we’re interested in.</p><p>That gives us a few better options.</p><h3>Option A: Use a nested relation query</h3><p>With Prisma, you can request the related projects as part of the same logical query:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/908/0*wnGlac_3pkepXHdz.png" /></figure><p>Instead of manually fetching projects inside a loop, you describe the response shape you need.</p><p>Depending on the ORM version and relation-loading strategy, Prisma may load the related records using a database join or multiple batched SQL queries. Either way, this avoids the original pattern of issuing one independent project query for every user.</p><p>That distinction matters.</p><p>A nested ORM query doesn’t universally mean one SQL statement. Some ORMs fetch relations using a join; others issue separate queries and combine the results in application code. Some support both strategies.</p><p>What matters first is eliminating the repeated per-record lookup. After that, you can evaluate which relation-loading strategy best fits the workload.</p><p><a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/relation-queries">This Prisma’s documentation explains the available relation-query strategies and their trade-offs</a></p><h3>Option B: Fetch the parent records, then batch the children</h3><p>Sometimes you need more control over how the data is assembled.</p><p>You can retrieve the users first, collect their IDs, and fetch all matching projects in a single additional query.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7e2RWS9zhY1CxCOI.png" /></figure><p>Assume userId is a numeric field in this example. If your schema uses UUIDs or strings, use the corresponding TypeScript type.</p><p>The important part is this filter:</p><pre>where: {<br>  userId: {<br>    in: userIds,<br>  },<br>}</pre><p>Instead of executing one query per user, the application asks PostgreSQL to retrieve projects belonging to the entire set of user IDs.</p><p>For a non-empty user list, this approach uses two logical database reads: one for users and one for projects.</p><p>The application then groups the projects by userId and assembles the response.</p><p>This is a useful pattern when:</p><ul><li>You need custom response shaping.</li><li>You want separate control over parent and child queries.</li><li>The relation-loading behavior of your ORM isn’t suitable for the endpoint.</li><li>You need to reuse the fetched records for additional application-level processing.</li></ul><p>There are trade-offs. You now own the grouping logic, and a large list of IDs may require batching or pagination. You also need to ensure that the response doesn’t load an unbounded number of child records.</p><p>But the query count no longer grows by one for every user.</p><p>That’s the key improvement.</p><h3>A JOIN Is Not Automatically the Best Answer</h3><p>At this point, it can be tempting to conclude that every N+1 problem should be fixed with a SQL JOIN.</p><p>Often, a join is a good solution. But treating it as a universal rule replaces one simplistic assumption with another.</p><p>Consider:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/704/0*zb75dvl_SXPOcMIp.png" /></figure><p>This retrieves users and their projects through a single SQL statement.</p><p>For a straightforward one-to-many relationship, it can be an effective solution.</p><p>But joins change the shape of the result set.</p><p>If one user has ten projects, that user’s columns can appear across ten result rows. If you join multiple one-to-many relationships, the number of intermediate rows can grow dramatically.</p><p>For example, a user has 10 projects and 20 notifications. Joining both collections without carefully controlling the query shape can produce up to 200 combinations for that user.</p><p>This is sometimes called row multiplication or join explosion.</p><p>The database may still execute the query efficiently for a particular workload, but the application can end up processing far more data than it actually needs.</p><p>The practical lesson is not to avoid joins. It is to understand what the join produces.</p><p>Use a join when the relationship and result shape make sense. Consider separate batched queries when they avoid unnecessary row multiplication or make pagination easier to reason about. Measure both approaches when the workload justifies it.</p><p><strong>The objective is not to minimize the SQL statement count at any cost. It is to minimize unnecessary work while preserving the data and consistency your endpoint requires.</strong></p><h3>Why Adding an Index Doesn’t Fix the Root Problem</h3><p>Suppose the original query is slow:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-t9mDyNg_-GCZz8z.png" /></figure><p>That may be the right index for the query, depending on the table size, data distribution, and execution plan.</p><p>But the N+1 pattern remains.</p><p>If the application retrieves 100 users and issues one project query for each, the database still receives 100 project queries. The index may make those lookups faster, but it doesn’t remove their round-trips or execution overhead.</p><p>There is another detail worth remembering: indexes aren’t free. They consume storage and add maintenance work to inserts, updates, and deletes.</p><p>An index should support <a href="https://nsikanadaowo.substack.com/p/database-design-starts-with-access">a meaningful access pattern</a>, not compensate blindly for a query strategy that should be redesigned.</p><p>A useful debugging sequence is:</p><ol><li>Count how many times the query executes per request.</li><li>Identify whether the repetitions are necessary.</li><li>Change the query shape to batch or load the related data appropriately.</li><li>Inspect the execution plan of the remaining important queries.</li><li>Add or adjust indexes when the evidence supports doing so.</li></ol><p>Use PostgreSQL’s EXPLAIN (ANALYZE, BUFFERS) to investigate query execution, remembering that ANALYZE executes the statement. For statements that modify data, take appropriate precautions and understand the side effects before running it.</p><p>Query-plan analysis and query-count analysis answer different questions. You need both when both are relevant.</p><h3>The GraphQL Trap: One Resolver, One Query, Repeated Many Times</h3><p>N+1 problems are particularly easy to introduce in GraphQL.</p><p>Imagine a GraphQL API that returns users and their projects:</p><pre>query {<br>  users {<br>    id<br>    name<br>    projects {<br>      id<br>      name<br>    }<br>  }<br>}</pre><p>The users resolver fetches the users.</p><p>Then the projects field resolver runs for each returned user.</p><p>If that resolver performs an independent database query, the request can produce the familiar N+1 pattern.</p><p>The confusing part is that each resolver may be perfectly reasonable in isolation.</p><p>The users resolver knows how to fetch users. The projects resolver knows how to fetch projects for a user. Both do their jobs correctly.</p><p>The inefficiency appears when the GraphQL execution model invokes the projects resolver repeatedly without coordinating those database reads.</p><p>One common solution is a DataLoader-style batching mechanism.</p><p>Conceptually, the resolver asks for projects for a particular user:</p><pre>const projects = await projectLoader.load(user.id);</pre><p>Instead of immediately executing a separate query for every call, the loader collects multiple user IDs and fetches the corresponding projects in a batch.</p><p>The batch function might look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QueMuVWDBh3TUJ8P.png" /></figure><p>The returned array must correspond to the order of the incoming IDs, including empty results. That ordering contract is important for DataLoader implementations.</p><p>In production, you would normally use a DataLoader implementation or equivalent batching utility rather than building all of its caching and scheduling behavior yourself.</p><p>Also, create loaders with the appropriate request scope when cached results depend on the current user or authorization context. A global loader cache can accidentally reuse data across requests if it’s designed incorrectly.</p><p>DataLoader isn’t just a performance trick. It addresses a structural problem: field-level resolvers often need a way to coordinate data access across many related objects.</p><h3>Why Promise.all() Is Not a Real Fix</h3><p>One of the most common attempts to fix a slow loop is to make the queries concurrent.</p><p>Instead of:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*h9WjJzE6OX-XIcfi.png" /></figure><p>A developer writes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1008/0*qg_hQfFHLR-FJ-af.png" /></figure><p>The second implementation removes the sequential wait between queries.</p><p>That can reduce wall-clock latency for small, bounded workloads. But it still issues one query per user.</p><p>With 100 users, you’re still generating roughly 100 project queries.</p><p>Worse, you’re now attempting to issue them concurrently. Depending on the database pool and workload, this can increase contention, create a queue of waiting queries, and consume resources more aggressively.</p><p>Concurrency changes when the work runs. It does not eliminate the work.</p><p>There are legitimate reasons to execute independent queries concurrently. The mistake is using concurrency as a substitute for reducing unnecessary database operations.</p><p>Before asking, “How can I make these queries run faster?”, ask, “Why am I running so many queries at all?”</p><h3>Not Every Repeated Query Is an N+1 Problem</h3><p>A good engineer also knows when not to apply a pattern mechanically.</p><p>Imagine you’re processing a small batch of three records. Fetching related data separately might be perfectly acceptable if it makes the code simpler and the performance requirements are met.</p><p>Or suppose each item requires a different query with distinct filters, authorization rules, or business logic. Those queries may not be safely reducible to one batch.</p><p>The defining concern isn’t simply that a query appears multiple times. It’s that a query is being repeated per result when the underlying data could be retrieved more efficiently as a group.</p><p>You should also distinguish N+1 from other database performance problems.</p><ul><li><strong>Missing indexes:</strong> The database must examine too many rows to answer a query.</li><li><strong>Over-fetching:</strong> The application retrieves more columns or records than it needs.</li><li><strong>Inefficient joins:</strong> A query creates excessive intermediate rows or performs expensive operations.</li><li><strong>Connection pool exhaustion:</strong> Requests wait for database connections to become available.</li><li><strong>N+1 queries:</strong> The application issues unnecessary additional queries as the number of parent records grows.</li></ul><p>These problems can occur together, but they require different diagnoses.</p><p>A slow endpoint is a symptom, not a diagnosis. Don’t start adding indexes, introducing Redis, or rewriting your ORM queries until you understand which cost is actually dominating the request.</p><h3>How to Detect N+1 Queries Before Production</h3><p>The most reliable approach is to observe what the application sends to the database.</p><h3>Log Prisma queries during development</h3><p>Prisma supports query-event logging that can help you inspect generated SQL and execution durations.</p><p>For example, depending on your Prisma version and client setup:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/856/0*gDqx-nZBxlijWs1N.png" /></figure><p>This is a simplified example of setting up a query listener. In a NestJS application, integrate the listener into your Prisma service or database instrumentation setup rather than creating a separate client for every service.</p><p>Avoid indiscriminately logging sensitive parameter values in production. <a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/advanced/query-optimization-performance">Query logs can expose personal information or other confidential data</a>, and excessive logging can introduce its own performance overhead.</p><h3>Look for repeated query shapes</h3><p>Suppose your logs contain:</p><pre>SELECT ... FROM &quot;User&quot;;</pre><pre>SELECT ... FROM &quot;Project&quot;<br>WHERE &quot;userId&quot; = $1;</pre><pre>SELECT ... FROM &quot;Project&quot;<br>WHERE &quot;userId&quot; = $1;</pre><pre>SELECT ... FROM &quot;Project&quot;<br>WHERE &quot;userId&quot; = $1;</pre><p>The repeated query shape is a clue.</p><p>The parameter changes, but the SQL structure stays the same. If the same route triggers this pattern repeatedly, investigate whether the query is being executed once for every parent record.</p><p>Don’t assume that repeated SQL automatically means N+1. A query may legitimately execute multiple times. Correlate the events with the request, route, and data-access code.</p><h3>Measure query count per request</h3><p>Execution duration alone doesn’t tell the full story.</p><p>A route that executes 40 queries taking 2 milliseconds each may look healthy when you inspect individual query durations. Yet the route could be doing far more database work than its response requires.</p><p>Useful measurements include:</p><ul><li>Number of database queries per request.</li><li>Repeated query shapes and execution counts.</li><li>Total database time per request.</li><li>Endpoint latency, including p50, p95, and p99.</li><li>Connection pool wait time and utilization.</li><li>Rows returned and, where available, rows examined or read.</li></ul><p>For deeper investigation, query monitoring and tracing tools can connect a slow endpoint to the SQL statements it generates. <a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/advanced/query-optimization-performance">Prisma’s query-optimization documentation</a> describes query logging and other diagnostic techniques:</p><h3>Test with realistic data volumes</h3><p>An N+1 problem may be invisible when your test database contains only two users.</p><p>With two users, the endpoint performs three queries. That may seem harmless.</p><p>With 200 users, the same code performs 201 queries.</p><p>Performance tests should use representative record counts and relationships. Where practical, test query counts as well as response time. A test that verifies only the returned JSON can pass even when the implementation generates hundreds of unnecessary queries.</p><h3>A Practical Checklist for Reviewing Database Access</h3><p>When reviewing a NestJS service, GraphQL resolver, or repository method, ask:</p><p><strong>1. Am I querying inside a loop?</strong></p><p>Look for database calls inside for, for...of, map, forEach, or other repeated execution paths. Not every occurrence is wrong, but each deserves scrutiny.</p><p><strong>2. Can I fetch the related records in one batch?</strong></p><p>If you already have a collection of IDs, consider a query using IN, an ORM’s nested relation loading, or a DataLoader-style batching mechanism.</p><p><strong>3. Am I returning only the data the endpoint needs?</strong></p><p>Use select or the equivalent to avoid loading large text fields, unnecessary relations, or entire entities when the response requires only a few fields.</p><p><strong>4. Is the response bounded?</strong></p><p>Pagination matters. Fetching all users and all their projects in one giant query can replace an N+1 problem with an unbounded data-loading problem.</p><p><strong>5. Does the chosen loading strategy fit the relationship?</strong></p><p>A join may be effective for one relation but expensive when several large collections are loaded together. Compare the query shapes and actual execution costs.</p><p><strong>6. Have I measured the result?</strong></p><p>Count the queries before and after the change. Compare endpoint latency and database resource usage under representative load.</p><p><strong>7. Is the data access still correct?</strong></p><p>Optimization must preserve authorization, filtering, ordering, pagination, and any required consistency guarantees. A faster query that returns the wrong records is not an optimization.</p><h3>Final Thoughts: Your ORM Is Only as Smart as the Queries You Ask It to Make</h3><p>N+1 queries are frustrating because they rarely look like a serious engineering mistake at first.</p><p>The code is readable. The relationships are modeled correctly. The endpoint returns the expected response.</p><p>The problem is hidden in the execution pattern.</p><p>An ORM can make database interactions easier to express, but it cannot remove the need to understand how those interactions translate into actual database work.</p><p>And the fix isn’t always to write raw SQL, add an index, introduce a cache, or force every relation into a join. Sometimes, the most effective improvement is simply to stop asking the database the same question one record at a time.</p><p>Think about the workload your code creates, not just the code you write.</p><p>When an endpoint becomes slow, don’t stop at asking whether each query is efficient. Ask whether all those queries need to exist in the first place.</p><p>Because the fastest unnecessary query is the one your application never sends.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=03a0f184285e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/n-1-queries-your-orm-is-lying-to-you-and-your-database-is-paying-the-bill-03a0f184285e">N+1 Queries: Your ORM Is Lying to You and Your Database Is Paying the Bill</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The MCP Connection Was Open. The Session Was Gone.]]></title>
            <link>https://medium.com/beyond-localhost/the-mcp-connection-was-open-the-session-was-gone-1a81817a3247?source=rss----7ab1ae1562b8---4</link>
            <guid isPermaLink="false">https://medium.com/p/1a81817a3247</guid>
            <category><![CDATA[model-context-protocol]]></category>
            <category><![CDATA[debugging]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 01:01:02 GMT</pubDate>
            <atom:updated>2026-10-30T01:01:02.318Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1YuMXsDd4GSxGAuv" /><figcaption>Photo by <a href="https://unsplash.com/@jouwdan?utm_source=medium&amp;utm_medium=referral">Jordan Harrison</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>A Claude Desktop user had Mac MCP connected, left it alone for a while, and came back to a peculiar failure. The application still appeared to have a connection. The next tool call did not work.</p><p>They had already found a way around it. One shell script started a local relay and supplied the MCP credential. A second script sent a ping every ten minutes. It was a practical fix for their own machine, but it bothered me that they needed either script in the first place.</p><p>Mac MCP lets AI clients work with native macOS apps through a local server. The same user said Codex worked without the extra steps, which initially sent me looking for a Claude Desktop-specific bug. I spent a while in the wrong place. The clients weren&#39;t the whole story.</p><p>When I followed the setup path end to end, I found three separate problems. Our instructions pushed Claude Desktop users toward a needlessly complicated connection. The server could forget an idle MCP session after 30 minutes. And an old exported API key could override the one the setup command had just read. Put together, they made a fairly ordinary connection look unreliable.</p><h4>A setup command that sent people in the wrong direction</h4><p>I started with the easiest failure to reproduce from the source: our documentation and CLI guidance.</p><p>Mac MCP already had a local stdio bridge. Its purpose is straightforward. A client such as Claude Desktop launches a command, exchanges MCP messages over standard input and output, and the bridge forwards those messages to the local HTTP server. The HTTP credential stays with Mac MCP instead of being copied into the client’s configuration.</p><p>Yet an older connection-config path could make Claude Desktop look unsupported. Someone following that path could reasonably conclude that they needed to use an HTTP proxy, assemble an Authorization header and keep the connection alive themselves.</p><p>The supported setup is now:</p><pre>mac-mcp connect --client claude-desktop</pre><p>That command creates a Claude Desktop entry invoking mac-mcp stdio. It is a local process, not a public web endpoint. The client’s configuration does not need a literal API key or remote URL. This does not make the credential disappear; it moves responsibility for reading and protecting it to the component that owns the local server.</p><p>That was annoying to admit. We&#39;d already built a local bridge for exactly this job, yet our instructions sent people somewhere else. The user&#39;s relay script was, in part, a workaround for our documentation.</p><h4>The connection and the session had different lifetimes</h4><p>Mac MCP’s server-side MCP session idle timeout defaults to 1,800 seconds, or 30 minutes. It can be configured from 60 seconds up to a day. After an eligible idle interval, the server can discard the MCP session identifier. A transport or client UI may still look connected even though the server no longer recognizes the session associated with the next request.</p><p>The word “connected” hides the problem. The Claude process can still be running. The HTTP endpoint can still answer. The API key can still be valid. None of that tells you whether the server recognizes the session ID attached to the next request.</p><p>There is a nuance worth making explicit. A genuinely open event stream can keep the current MCP session alive; the current server integration documents that behavior. The failure is not that every open SSE stream expires after thirty minutes. The danger is assuming that a visible client connection, a relay process or a previously successful handshake proves the server still has the session. A client may be idle without an active request that preserves it, or its server may have restarted.</p><p>Increasing the idle timeout would have hidden some of the reported failures. It wouldn’t have solved the general case. A Mac sleeps overnight. An app is updated. A process restarts. A network connection breaks. Eventually the client needs to recognize that session state is gone and negotiate a fresh one.</p><p>I looked at the problem from the message boundary rather than the clock. The bridge has to retain the original initialize message and protocol version. When a later request receives a clear “session not found” response, it can initialize a fresh session, send the initialized notification, then forward the request under the new session ID.</p><h4>The retry rule that mattered more than reconnecting</h4><p>If a connection attempt fails before reaching the server, a bounded retry is reasonable. In the bridge, connection errors can wait briefly for a server that is starting or restarting. If the server stays unavailable, the caller receives a clear error instead of an indefinitely spinning status.</p><p>If the server explicitly returns a missing-session error before processing the tool request, the bridge can rebuild the MCP session and try that request again.</p><p>But a connection dropping during the response is different. The server may have already performed the work. An email could have been sent, a file edited or a command started, even if the client never received the answer.</p><p>I did not want a reliability fix that quietly duplicated someone’s action. For a transport error mid-reply, the bridge reports that the request was interrupted and tells the caller to check the outcome before trying again. It does not auto-resend the original tool call.</p><p>This is the security boundary I care about as much as the session boundary. Recovering a transport must not silently grant more authority or repeat an operation with unknown effects. Permission checks still happen on the server, under the same user’s configuration, after a new authenticated session is established. A recovered connection is not permission to skip approvals, and a successful retry is not proof of exactly-once execution.</p><h4>The correct API key came from the wrong place</h4><p>The user had the right key in Mac MCP’s .env. The local setup read that file, so the client side looked correct. But an older MCP_API_KEY remained exported in the environment that launched the server. The old value took precedence. The client and server were using different credentials, and the visible configuration made it hard to explain why.</p><p>The change was deliberately specific. Mac MCP now treats MCP_API_KEY from its own .env as authoritative over an inherited value when they differ. It emits a warning explaining which source won, without printing either key. This is not a broad rewrite of environment-variable precedence; other settings do not automatically receive the same rule.</p><p>That choice is about both consistency and containment. A setup tool and server should not disagree about the credential merely because of an old shell export. But logging the actual secrets in order to diagnose the disagreement would create another problem.</p><p>The bridge also forwards the credential as an HTTP Bearer header to the local MCP endpoint. Nothing about session renewal should turn an authentication failure into an unauthenticated connection or cause it to fall back to a weaker mode. A wrong key is a wrong key; the bridge reports a connection rejection and stops there.</p><h4>What the evidence establishes, and what it doesn’t</h4><p>The session timeout tests verify the 1,800-second default, the lower and upper bounds and the behavior for invalid configuration. The source also sets the actual MCP session manager’s timeout from that configured value. Those tests establish the configuration rule, not the behavior of every remote MCP client.</p><p>The stdio bridge tests use a fake Streamable HTTP server. One test invalidates the server’s known sessions, then checks that the bridge replays initialization before forwarding the next request. Other tests cover a temporarily unavailable server, a server that never comes back, a rejected credential and a listener that encounters a missing session.</p><p>The test I find most important deliberately interrupts a request while its reply is being read. It checks that the request is sent once, that the bridge returns an interrupted-result error and that no automatic second call is made. This isn’t a proof of exactly-once execution in the world. It’s a regression guard against our own code creating an obvious duplicate.</p><p>The session-timeout test documentation also records a shortened-timeout end-to-end exercise using a 60-second interval. I regard that as evidence for the specific recovery scenario tested, not as a permanent cross-version compatibility guarantee. The committed unit tests are easier for someone else to inspect and reproduce.</p><p>There are still boundaries I can’t prove from those tests. They don’t establish that every Claude Desktop release behaves identically, that every proxy handles streaming correctly or that an arbitrary mutating tool can be replayed safely after a lost response. I haven’t independently confirmed that the original user’s scripts have been removed from their machine. Their intention to simplify their setup is encouraging feedback, not final validation.</p><h4>What I should have caught earlier</h4><p>I had treated these as three unrelated bugs. The docs were wrong over here, the timeout was over there, and the environment variable behaved differently depending on how the server was launched. From the user&#39;s side, it was one problem: getting Claude Desktop to work required two homemade scripts.</p><p>I should have installed it as a first-time user would, following only our documentation. I should have left the client idle long enough to see what happened. And I should have checked which API key the running server actually used before assuming the client&#39;s key was wrong.</p><p>The replacement has less ceremony: one supported setup command, no hand-built relay, no keepalive cron. The bridge still has to handle restarts, missing sessions and broken connections. What changed is that it knows which failures are safe to recover from, and which ones need a person to check what happened.</p><h4>Four questions I’ll ask next time</h4><p>When a user says their MCP tools stop working after idle time, my first response won’t be “restart everything.” I’ll ask whether the local server is reachable; whether the server and client are using the same effective credential; whether the server still recognizes the MCP session; and whether the transport can negotiate a new session without replaying an operation whose outcome is unknown.</p><p>The code and tests are public:<br>- <a href="https://github.com/bulutarkan/mac-mcp/blob/main/mcp_server/stdio_bridge.py">https://github.com/bulutarkan/mac-mcp/blob/main/mcp_server/stdio_bridge.py</a><br>- <a href="https://github.com/bulutarkan/mac-mcp/blob/main/mcp_server/security.py">https://github.com/bulutarkan/mac-mcp/blob/main/mcp_server/security.py</a><br>- <a href="https://github.com/bulutarkan/mac-mcp/blob/main/tests/test_stdio_bridge.py">https://github.com/bulutarkan/mac-mcp/blob/main/tests/test_stdio_bridge.py</a><br>- <a href="https://github.com/bulutarkan/mac-mcp/blob/main/tests/test_session_idle_timeout.py">https://github.com/bulutarkan/mac-mcp/blob/main/tests/test_session_idle_timeout.py</a><br>- <a href="https://github.com/bulutarkan/mac-mcp/blob/main/docs/connect.md">https://github.com/bulutarkan/mac-mcp/blob/main/docs/connect.md</a></p><p>I had treated a working-looking connection as evidence that the next call would work. It wasn&#39;t. A client can appear fine after the server has forgotten its session. The next call should recover from that without guessing whether an interrupted operation needs to run again.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a81817a3247" width="1" height="1" alt=""><hr><p><a href="https://medium.com/beyond-localhost/the-mcp-connection-was-open-the-session-was-gone-1a81817a3247">The MCP Connection Was Open. The Session Was Gone.</a> was originally published in <a href="https://medium.com/beyond-localhost">Beyond Localhost</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>