<?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: Daniel Akudbilla</title>
    <description>The latest articles on DEV Community by Daniel Akudbilla (@daniel_akudbilla_999ccff6).</description>
    <link>https://dev.to/daniel_akudbilla_999ccff6</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%2F2909057%2F3d781239-62a1-4645-8613-7b48547ed190.jpg</url>
      <title>DEV Community: Daniel Akudbilla</title>
      <link>https://dev.to/daniel_akudbilla_999ccff6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_akudbilla_999ccff6"/>
    <language>en</language>
    <item>
      <title>Worth Reading</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:07:46 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/worth-reading-1dgf</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/worth-reading-1dgf</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7" class="crayons-story__hidden-navigation-link"&gt;Killing N+1 Queries: From Per-Row Lookups to Flat, Batched Reads&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/daniel_akudbilla_999ccff6" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F2909057%2F3d781239-62a1-4645-8613-7b48547ed190.jpg" alt="daniel_akudbilla_999ccff6 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/daniel_akudbilla_999ccff6" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Daniel Akudbilla
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Daniel Akudbilla
                
              
              &lt;div id="story-author-preview-content-4190427" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/daniel_akudbilla_999ccff6" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F2909057%2F3d781239-62a1-4645-8613-7b48547ed190.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Daniel Akudbilla&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 20&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7" id="article-link-4190427"&gt;
          Killing N+1 Queries: From Per-Row Lookups to Flat, Batched Reads
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/performance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;performance&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/database"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;database&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/backend"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;backend&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sql"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sql&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Killing N+1 Queries: From Per-Row Lookups to Flat, Batched Reads</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:06:46 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/killing-n1-queries-from-per-row-lookups-to-flat-batched-reads-32f7</guid>
      <description>&lt;p&gt;You ship a read endpoint. It lists some things, say devices or orders or invoices, and for each row it pulls in a bit of related data. With five rows in the demo it's instant. Six months and a few thousand rows later, the same endpoint takes seconds and sits at the top of your slow-query dashboard.&lt;/p&gt;

&lt;p&gt;Usually the culprit isn't a slow query. It's a fast query run thousands of times. That's the N+1 problem. I recently fixed two different versions of it in the same codebase, so this post walks through both, plus the fix, plus a test that stops it coming back.&lt;/p&gt;

&lt;p&gt;The examples use a made-up domain: a dashboard listing active devices, where each row needs the device's site, that site's latest billing window, a premium-plan flag, and an install date. The domain doesn't matter. The shapes do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What N+1 means
&lt;/h2&gt;

&lt;p&gt;You run 1 query to fetch a list of N items, then N more queries (one per item) for the related data. One plus N.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1 query: get the list&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deviceRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;// N queries: one lookup per device, inside the loop&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;site&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;siteRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;billingRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latestWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;date&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assetRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;installDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lookups per device and 1,000 devices is 3,001 queries to render one page. Each query is tiny. But each one pays for a network round-trip, connection checkout, planning, and result marshalling, and they run sequentially: query 2 waits for query 1 to come back. So latency ends up dominated by the number of round-trips rather than the work inside any single one. This is why the code feels fine in a test against localhost and falls over against a database one network hop away.&lt;/p&gt;

&lt;p&gt;What makes it hard to catch in review is that nothing looks wrong. Every line is a reasonable single-row fetch. The problem only becomes visible once you notice where the loop is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shape 1: the assembly loop
&lt;/h2&gt;

&lt;p&gt;The example above is the classic form. You have a collection, and inside the &lt;code&gt;map&lt;/code&gt; or &lt;code&gt;for&lt;/code&gt; you reach back to the database once or more per element to assemble the final object.&lt;/p&gt;

&lt;p&gt;The real version I fixed did five things per row: fetch the parent record, fetch the parent's latest status, re-fetch an id it already had through a second table, fetch an attribute record, and run an "is this premium?" check that turned out to be several queries by itself. On a few hundred rows that's thousands of statements per request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shape 2: the N+1 hiding inside a predicate
&lt;/h2&gt;

&lt;p&gt;This one is sneakier. Somewhere there's a helper like &lt;code&gt;isPremium(planId)&lt;/code&gt;, and at the call site it reads like plain business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;isPremium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;priceRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// query 1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;component&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;componentRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// query per price!&lt;/span&gt;
        &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;PREMIUM_TYPES&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems stacked on top of each other. First, the function is itself an N+1: one query for the plan's prices, then one more per price to resolve the component type. Second, it gets called inside a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;premiumPlanIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;planIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filterTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mutableSetOf&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;isPremium&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total cost: &lt;code&gt;plans × (1 + prices per plan)&lt;/code&gt;. A predicate that looks like a single &lt;code&gt;if&lt;/code&gt; is doing a nested fan-out. These are the worst N+1s to spot, because the expensive part is one function call deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: batch first, assemble in memory
&lt;/h2&gt;

&lt;p&gt;Both shapes have the same cure. Do all the fetching up front, in a fixed number of set-based queries, then build the object graph from in-memory maps. The loop stops touching the database entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Collect the keys
&lt;/h3&gt;

&lt;p&gt;Before the loop, gather every id you'll need, and de-duplicate. If 1,000 devices sit on 50 sites, you want to fetch 50 sites, not 1,000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deviceRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;siteIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. One batched query per relationship
&lt;/h3&gt;

&lt;p&gt;Replace &lt;code&gt;findById(x)&lt;/code&gt; in a loop with a single &lt;code&gt;IN&lt;/code&gt; query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Kotlin + Exposed, but every ORM has an equivalent (WHERE id IN (...))&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;findSitesByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Site&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@transaction&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// never send IN ()&lt;/span&gt;
    &lt;span class="nc"&gt;SiteTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;SiteTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;inList&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toSite&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the empty-input guard. Depending on your driver, an &lt;code&gt;IN ()&lt;/code&gt; with no values is either a SQL error or an accidental full-table scan, so short-circuit before you build the query.&lt;/p&gt;

&lt;p&gt;Then turn each result into a lookup map keyed on whatever you'll match by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sitesById&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;siteRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteIds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;associateBy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;datesByKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assetRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;installDatesBySiteIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// Map&amp;lt;Pair&amp;lt;SiteId, Serial&amp;gt;, Date&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Assemble against the maps
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;site&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sitesById&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datesByKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNumber&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query count no longer depends on the number of devices. It's a fixed handful, one per relationship. 5 rows or 50,000, same count.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Latest per group" without a query per group
&lt;/h2&gt;

&lt;p&gt;One per-row lookup deserves special mention: "the most recent record for this parent". Done naively that's one &lt;code&gt;ORDER BY created_at DESC LIMIT 1&lt;/code&gt; per parent. You can get all of them in one query with PostgreSQL's &lt;code&gt;DISTINCT ON&lt;/code&gt; (elsewhere, &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY ...)&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;latestWindowsBySiteIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BillingWindow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@transaction&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;BillingWindowTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;BillingWindowTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt; &lt;span class="n"&gt;inList&lt;/span&gt; &lt;span class="n"&gt;siteIds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;BillingWindowTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nc"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;BillingWindowTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endDate&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nc"&gt;SortOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// newest first within each site&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withDistinctOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BillingWindowTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// keep first row per site&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat: check your NULL ordering. If a NULL end-date means "still open", you need to know how your database sorts NULLs. Postgres puts NULLs first under &lt;code&gt;DESC&lt;/code&gt;, so an open window is correctly picked as the latest here. Get this wrong and the batched version silently disagrees with the old per-row one, which is exactly the kind of bug that deserves a comment and a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collapsing the nested predicate
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;isPremium&lt;/code&gt; fan-out disappears once you stop asking one plan at a time. A plan is premium iff any of its price components is a premium type. So fetch all prices for all plans in query 1, fetch all the referenced components in query 2, and match in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;findPremiumPlanIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;emptySet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;priceRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByPlanIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;planIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// query 1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;emptySet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;premiumComponentIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;componentRepo&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;    &lt;span class="c1"&gt;// query 2&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;PREMIUM_TYPES&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mutableSetOf&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;                                            &lt;span class="c1"&gt;// in-memory join&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;premiumComponentIds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapNotNullTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mutableSetOf&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two queries, total, no matter how many plans or components are involved. The old loop cost &lt;code&gt;plans × (1 + prices)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't lose per-item error isolation
&lt;/h2&gt;

&lt;p&gt;There's a behavioural trap when you hoist work out of a loop. In the old code, if one item's dependency was missing, you could catch that iteration, mark the item as skipped, and keep going. Batch everything up front carelessly and a single bad row can now fail the whole request.&lt;/p&gt;

&lt;p&gt;The fix is to batch the fetching but keep the validation per item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;site&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sitesById&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"No site for device ${device.id}"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windowsBySite&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;siteId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"No billing window for site ${device.siteId}"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;buildRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSuccess&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The maps are shared, but each item still gets its own verdict. A device with no billing window lands in &lt;code&gt;skipped&lt;/code&gt; with a clear reason and everything else succeeds. Same semantics as the old loop, at the cost of a single batched read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with a test that counts statements
&lt;/h2&gt;

&lt;p&gt;A performance fix you can't measure is one you can't defend. "It feels faster" gets refactored away in six months. Milliseconds are noisy and machine-dependent. The durable, deterministic metric is how many SQL statements the operation issues, which happens to be exactly the thing an N+1 blows up.&lt;/p&gt;

&lt;p&gt;So I wrote an integration test that runs both implementations against the same seeded data at several sizes, counting statements with a logger that increments on every executed statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`batched&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="n"&gt;far&lt;/span&gt; &lt;span class="n"&gt;fewer&lt;/span&gt; &lt;span class="n"&gt;statements&lt;/span&gt; &lt;span class="n"&gt;than&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;per-item&lt;/span&gt; &lt;span class="nf"&gt;loop`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sizes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newCounts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableListOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

    &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;seedItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upTo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. Both implementations must agree on the result. Correctness first.&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;oldResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;oldPerItemImplementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;newBatchedImplementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;newResult&lt;/span&gt; &lt;span class="n"&gt;shouldBe&lt;/span&gt; &lt;span class="n"&gt;oldResult&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Then compare statement counts.&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;oldStatements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countStatements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;oldPerItemImplementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newStatements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countStatements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;newBatchedImplementation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;newCounts&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;newStatements&lt;/span&gt;

        &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[perf] n=$n | old=$oldStatements | new=$newStatements"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;newStatements&lt;/span&gt; &lt;span class="n"&gt;shouldBeLessThan&lt;/span&gt; &lt;span class="n"&gt;oldStatements&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The real assertion: the new count is flat across every size.&lt;/span&gt;
    &lt;span class="n"&gt;newCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="n"&gt;shouldBe&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[perf] n=5  | old=11 | new=4
[perf] n=20 | old=41 | new=4
[perf] n=40 | old=81 | new=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two assertions carry the story. &lt;code&gt;shouldBeLessThan&lt;/code&gt; proves the fix is strictly better at every size. The flat-count check proves the new count is identical at 5, 20, and 40 items: O(N) vs O(1) round-trips, encoded as a regression test. If someone reintroduces a per-row lookup, that assertion fails on the spot.&lt;/p&gt;

&lt;p&gt;Two notes that saved me grief. Assert equivalence before performance, because a fast wrong answer is still wrong. And count statements with the logger attached, but measure wall-clock in a separate run without it, since the logger distorts timings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being academic
&lt;/h2&gt;

&lt;p&gt;At 5 rows nobody notices an N+1. The reason to fix it is what happens as data grows. Since the queries run sequentially, latency is roughly &lt;code&gt;statements × round-trip time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Take the assembly loop with 5 lookups per row (&lt;code&gt;1 + N×5&lt;/code&gt; statements) against a batched version at a fixed 6, and assume a healthy 2 ms per round-trip:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows (N)&lt;/th&gt;
&lt;th&gt;Old statements&lt;/th&gt;
&lt;th&gt;New&lt;/th&gt;
&lt;th&gt;Old wall-clock&lt;/th&gt;
&lt;th&gt;New wall-clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;501&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;td&gt;~12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;5,001&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~10 s&lt;/td&gt;
&lt;td&gt;~12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;50,001&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~100 s&lt;/td&gt;
&lt;td&gt;~12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ten seconds at 1,000 rows means timeouts and a paged on-call engineer. The nested-predicate shape is worse because it multiplies: put &lt;code&gt;isPremium&lt;/code&gt; (4 queries per plan) inside the assembly loop and you're at roughly &lt;code&gt;N × M&lt;/code&gt;, thousands of statements from that one check before counting anything else. Batching collapses the whole product back to a constant. The win here isn't shaving 20% off a query. It's changing the growth curve.&lt;/p&gt;

&lt;p&gt;Wall-clock is also just the symptom the user sees. A request that holds a connection for 10 seconds while it drips out 5,000 queries starves the connection pool for everyone else, so healthy endpoints start failing because of one unbatched loop somewhere else in the service. Every statement gets parsed and planned, and thousands of trivial &lt;code&gt;WHERE id = ?&lt;/code&gt; calls burn more database CPU in aggregate than two set-based queries doing the same work. Transactions and locks stay open longer. And the pain concentrates at p99: the average user with a small result set is fine, while the power user with the biggest account hits the worst case every single time. That power user is often your most valuable customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist
&lt;/h2&gt;

&lt;p&gt;When an endpoint's latency scales with its result size:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the loop that touches the database. Any &lt;code&gt;map&lt;/code&gt;/&lt;code&gt;for&lt;/code&gt;/&lt;code&gt;filter&lt;/code&gt; whose body calls a repository is a suspect. Look inside helper functions too, since the worst N+1s hide one call deep.&lt;/li&gt;
&lt;li&gt;Name the shape: &lt;code&gt;1 + N&lt;/code&gt;? &lt;code&gt;1 + N × k&lt;/code&gt;? A nested &lt;code&gt;N × M&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Hoist the fetching out of the loop: collect ids, de-duplicate, one &lt;code&gt;IN&lt;/code&gt; query per relationship.&lt;/li&gt;
&lt;li&gt;Build maps and assemble in memory. The loop reads from &lt;code&gt;Map&lt;/code&gt;s, never the database.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;DISTINCT ON&lt;/code&gt; or window functions for "latest per group".&lt;/li&gt;
&lt;li&gt;Collapse nested predicates into "fetch all, match in memory".&lt;/li&gt;
&lt;li&gt;Guard empty inputs so you never emit &lt;code&gt;IN ()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Preserve per-item error isolation if the old loop had it.&lt;/li&gt;
&lt;li&gt;Lock it in with a statement-count test that asserts a flat count across sizes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;N+1 is a round-trip problem. The individual queries are fine; the count is the enemy. The fix is always the same shape, and the metric that keeps it fixed is statements per operation. Count it, assert it stays flat, and the win becomes a property of the system instead of a mood.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>database</category>
      <category>backend</category>
      <category>sql</category>
    </item>
    <item>
      <title>The dotenv gotcha that will eat an afternoon of your life.</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Sat, 04 Jul 2026 23:59:30 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/the-dotenv-gotcha-that-will-eat-an-afternoon-of-your-life-2ha9</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/the-dotenv-gotcha-that-will-eat-an-afternoon-of-your-life-2ha9</guid>
      <description>&lt;p&gt;If you've changed a value in &lt;code&gt;.env&lt;/code&gt; and your app keeps using the old one, this is your bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;code&gt;dotenv&lt;/code&gt; will not overwrite an environment variable that's already set in your shell. If your &lt;code&gt;.env&lt;/code&gt; edits aren't taking effect, something else already claimed that variable first. Fix: &lt;code&gt;dotenv.config({ override: true })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a bug that looks impossible until you know the one rule behind it. &lt;strong&gt;&lt;code&gt;dotenv&lt;/code&gt; will not overwrite an environment variable that's already set.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's really the whole article. But the way this bites people is worth walking through, because the symptoms point everywhere except the actual cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 The symptom
&lt;/h2&gt;

&lt;p&gt;I had an app reading its OpenAI key from a &lt;code&gt;.env&lt;/code&gt; file. Every API call came back &lt;code&gt;429 insufficient_quota&lt;/code&gt;, the classic "you're out of credits" error.&lt;/p&gt;

&lt;p&gt;So I checked the obvious things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key in &lt;code&gt;.env&lt;/code&gt;? Valid. A raw &lt;code&gt;curl&lt;/code&gt; with it returned &lt;code&gt;200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Billing on the account? Fine.&lt;/li&gt;
&lt;li&gt;The right model? Yep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same key worked from &lt;code&gt;curl&lt;/code&gt; and failed from the app. Same machine, same file, same key. That combination shouldn't be possible, until you realize the app and the &lt;code&gt;curl&lt;/code&gt; call weren't actually using the same key at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕵️ The cause
&lt;/h2&gt;

&lt;p&gt;My shell had an old &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; exported from a previous experiment, a dead key with no quota left. My &lt;code&gt;curl&lt;/code&gt; test used the key directly, so it used the good one. But when the app loaded &lt;code&gt;.env&lt;/code&gt; with dotenv, dotenv saw that &lt;code&gt;process.env.OPENAI_API_KEY&lt;/code&gt; was already set by the shell, and left the dead key in place.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file was correct the whole time. It was just being ignored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is what everyone writes:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// And this is dotenv's rule:&lt;/span&gt;
&lt;span class="c1"&gt;// if process.env.FOO already exists, the .env value is DISCARDED.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧪 Prove it
&lt;/h2&gt;

&lt;p&gt;Proving it takes about ten seconds. Print the key your code actually sees versus the one in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;using key ending in:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that tail doesn't match your &lt;code&gt;.env&lt;/code&gt;, you've found it ✅. Something in your shell (&lt;code&gt;~/.zshrc&lt;/code&gt;, &lt;code&gt;~/.zshenv&lt;/code&gt;, a &lt;code&gt;docker run -e&lt;/code&gt;, a CI secret, a &lt;code&gt;launchctl setenv&lt;/code&gt;) is shadowing the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Why dotenv does this on purpose
&lt;/h2&gt;

&lt;p&gt;This isn't a bug. It's standard twelve-factor app behavior, and it's the right default. &lt;strong&gt;Real environment variables should outrank a local file.&lt;/strong&gt; In production, you want the value your platform injected (from Kubernetes, ECS, Heroku, your secrets manager) to win over a stray &lt;code&gt;.env&lt;/code&gt; that got left in the image by accident.&lt;/p&gt;

&lt;p&gt;The trap only shows up in development, where a leftover &lt;code&gt;export&lt;/code&gt; in your dotfiles quietly outranks the file you're actively editing. You change &lt;code&gt;.env&lt;/code&gt;, nothing happens, and you start suspecting the network, the API, or the phase of the moon.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ The fixes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fix 1: &lt;code&gt;override: true&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Fix 2: clean your shell&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local dev, small/medium apps&lt;/td&gt;
&lt;td&gt;Matching prod behavior exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Effort&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One line, once&lt;/td&gt;
&lt;td&gt;Manual cleanup, ongoing discipline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None in prod (no &lt;code&gt;.env&lt;/code&gt; there to override)&lt;/td&gt;
&lt;td&gt;Easy to forget and re-export later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Fix 1: make your file win (dev-friendly)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;override&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;.env&lt;/code&gt; beats the shell during local development. It's still safe in production. If there's no &lt;code&gt;.env&lt;/code&gt; file on the server, there's nothing to override, so your platform's real environment variables win by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 2: keep dotenv's default behavior, clean your shell instead&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPENAI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# find the stale value&lt;/span&gt;
&lt;span class="nb"&gt;unset &lt;/span&gt;OPENAI_API_KEY     &lt;span class="c"&gt;# kill it for this session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then track down where it's exported and remove the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; OPENAI_API_KEY ~/.z&lt;span class="k"&gt;*&lt;/span&gt; ~/.bash&lt;span class="k"&gt;*&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reach for this option if you specifically want real environment variables to keep top priority, matching production behavior exactly.&lt;/p&gt;

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

&lt;p&gt;When a config value refuses to change no matter what you put in &lt;code&gt;.env&lt;/code&gt;, don't debug the value. Debug which value your process is actually reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THE_VAR&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, and it turns an afternoon of staring at billing dashboards into a ten-second "oh" 😅. Environment variables set outside your app always outrank your &lt;code&gt;.env&lt;/code&gt;, unless you explicitly say otherwise. Now you won't forget it.&lt;/p&gt;

&lt;p&gt;If this saved you an afternoon, you probably know someone else who's about to lose one too. Share the word&lt;/p&gt;

&lt;p&gt;Happy Coding!&lt;/p&gt;

</description>
      <category>node</category>
      <category>dotenv</category>
      <category>debugging</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Built an AI Agent to Check the Weather. It Was Gloriously Unnecessary.</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Sat, 04 Jul 2026 18:41:37 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/i-built-an-ai-agent-to-check-the-weather-it-was-gloriously-unnecessary-53i9</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/i-built-an-ai-agent-to-check-the-weather-it-was-gloriously-unnecessary-53i9</guid>
      <description>&lt;p&gt;Let me get the confession out of the way early: I built an AI agent that, given a city name, tells you about the place and then checks the weather.&lt;/p&gt;

&lt;p&gt;You know what already checks the weather? A weather API. One &lt;code&gt;fetch&lt;/code&gt;. No model, no tokens, no monthly bill with the word "usage" in it. I knew this the whole time I was building. Built the agent anyway.&lt;/p&gt;

&lt;p&gt;Here's the story of that, and a small argument for why doing the unnecessary thing was actually the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the thing does
&lt;/h2&gt;

&lt;p&gt;It's called the Travel &amp;amp; Weather Guide. You type in a city Lisbon, Kyoto, wherever and it gives you back four things: a couple of sentences on what the place is known for, the live weather right now, what people tend to do there this time of year, and a straight verdict on whether now is a good time to visit.&lt;/p&gt;

&lt;p&gt;It runs two ways. A web page with a search box:&lt;/p&gt;

&lt;p&gt;The Travel &amp;amp; Weather Guide web UI:&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%2Fd2ztpfy1gs7log7pkxn6.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%2Fd2ztpfy1gs7log7pkxn6.png" alt="Web UI of the travel and weather guide" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And a terminal version, for when you want to feel like a hacker while asking about the weather in Bali:&lt;/p&gt;

&lt;p&gt;The CLI starting up, printing "Travel &amp;amp; Weather Guide (type exit to quit)" and prompting for a city name]&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%2Fxjtznhw8amcuptqntytq.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%2Fxjtznhw8amcuptqntytq.png" alt="CLI for the travel and weather guide" width="799" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both talk to the same agent underneath. The weather itself comes from &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt;, which is free and needs no key. The "thinking" comes from a model via the &lt;a href="https://github.com/openai/openai-agents-js" rel="noopener noreferrer"&gt;OpenAI Agents SDK&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works (spoiler: not MCP)
&lt;/h2&gt;

&lt;p&gt;The interesting part is how the model gets the weather, because it doesn't. The model can't make an HTTP request or run code, it can only generate text. So instead of fetching anything itself, it asks an internal &lt;em&gt;tool&lt;/em&gt; to do it, and waits for the answer.&lt;br&gt;
In the SDK that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getWeather&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_weather&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fetch current weather for a given city using Open-Meteo.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The city or town name to check weather for&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// geocode the city, then hit the forecast endpoint&lt;/span&gt;
    &lt;span class="c1"&gt;// ...return "Lisbon, Portugal: 24°C, wind 12 km/h"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;tool()&lt;/code&gt; wrapper turns my Zod schema into a description the model can read. So now the model &lt;em&gt;knows&lt;/em&gt; a &lt;code&gt;get_weather(city)&lt;/code&gt; function exists — but it can't run it. It can only ask.&lt;/p&gt;

&lt;p&gt;The loop goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You ask about Lisbon.&lt;/li&gt;
&lt;li&gt;The model thinks, "I need weather data," and emits a structured tool call: &lt;code&gt;get_weather({ city: "Lisbon" })&lt;/code&gt;. No code has run yet. That's just the model raising its hand.&lt;/li&gt;
&lt;li&gt;The SDK's runner sees the request, runs my actual JavaScript function locally, and that function makes the plain &lt;code&gt;fetch&lt;/code&gt; to Open-Meteo.&lt;/li&gt;
&lt;li&gt;The result gets handed back to the model.&lt;/li&gt;
&lt;li&gt;The model writes the final answer using that data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;People sometimes assume this is MCP. It isn't. MCP is for tools that live in a &lt;em&gt;separate&lt;/em&gt; server you connect to over a protocol. This tool is just a function in my own process. The model requests, my code runs, the result goes back. All in-memory, one repo, no protocol.&lt;/p&gt;

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

&lt;p&gt;Look at that loop again. To tell you it's 24°C in Lisbon, the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sends the full conversation and tool definitions to a model,&lt;/li&gt;
&lt;li&gt;waits for the model to decide it wants the weather,&lt;/li&gt;
&lt;li&gt;runs the &lt;code&gt;fetch&lt;/code&gt; I could have run in the first place,&lt;/li&gt;
&lt;li&gt;sends everything &lt;em&gt;back&lt;/em&gt; to the model,&lt;/li&gt;
&lt;li&gt;waits again while it writes a paragraph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two model round-trips and a pile of tokens, wrapped around a single HTTP call that a first-week bootcamp student could write. The &lt;code&gt;fetch&lt;/code&gt; was always the easy part. I built a very expensive middle manager to decide when to make it.&lt;/p&gt;

&lt;p&gt;And here's the thing: this isn't just my weird little project. It's the whole moment we're in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything is becoming an agent now
&lt;/h2&gt;

&lt;p&gt;There's a quiet shift happening. A lot of things that used to be a function call, a cron job, or an &lt;code&gt;if&lt;/code&gt; statement are being rebuilt as agents. Not because the old way stopped working. The old way is fast and basically free. It's just that "agent" is the shape everything wants to be right now.&lt;/p&gt;

&lt;p&gt;Some of that is genuinely good. When a task is fuzzy, open-ended, needs judgment, or strings together tools in ways you can't predict up front, an agent earns its tokens. That's the real use case, and it's legitimately new.&lt;/p&gt;

&lt;p&gt;But a lot of it is us wrapping perfectly deterministic problems in a probabilistic, token-metered layer because it &lt;em&gt;feels&lt;/em&gt; like the future. We took things that were already solved and made them "smart," which mostly means slower, pricier, and occasionally confidently wrong about the weather. The wins are real. So is the tax. Both things are true at the same time, and nobody wants to say the second one out loud.&lt;/p&gt;

&lt;p&gt;My weather agent is a tiny, honest monument to that tax. It is a &lt;code&gt;fetch&lt;/code&gt; cosplaying as artificial intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why did I build it?
&lt;/h2&gt;

&lt;p&gt;Not to solve a world problem. Let's be very clear. Nobody was out there suffering from a lack of AI-narrated weather. Climate change did not get one degree cooler because my agent can tell you it's warm in Lisbon.&lt;/p&gt;

&lt;p&gt;I built it because I did not want to be the guy who &lt;em&gt;talks&lt;/em&gt; about agents at parties without having actually built one. You know that guy. I refuse to become that guy.&lt;/p&gt;

&lt;p&gt;The agentic era is happening whether I have opinions about tokens or not.&lt;/p&gt;

&lt;p&gt;Turns out the best way to understand the expensive absurdity of the agentic era is to commit a small, cheerful act of it yourself.&lt;/p&gt;

&lt;p&gt;So no, you should not build an agent to check the weather. You should just call the API.&lt;/p&gt;

&lt;p&gt;I, however, have now called the API. Through an agent. On purpose. And I regret nothing, except slightly the tokens. Cheers&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/Dakudbilla/openai-travel-weather-agent" rel="noopener noreferrer"&gt;Dakudbilla/openai-travel-weather-agent&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# clone and install&lt;/span&gt;
git clone https://github.com/Dakudbilla/openai-travel-weather-agent.git
&lt;span class="nb"&gt;cd &lt;/span&gt;openai-travel-weather-agent
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# add your OpenAI key&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env      &lt;span class="c"&gt;# then paste your key into .env: OPENAI_API_KEY=sk-...&lt;/span&gt;

npm start                 &lt;span class="c"&gt;# web app → http://localhost:3000&lt;/span&gt;
npm run cli               &lt;span class="c"&gt;# or the terminal version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weather data is free (Open-Meteo, no key), so the only thing you'll pay for is the tokens.You can get a free openai account generate the API key for it. &lt;/p&gt;

&lt;p&gt;Happy Coding &lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>javascript</category>
      <category>openai</category>
    </item>
    <item>
      <title>How to Push to a Private GitHub Repository Using a Fine-Grained Personal Access Token</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Sat, 29 Mar 2025 12:39:25 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/how-to-push-to-a-private-github-repository-using-a-fine-grained-personal-access-token-39ll</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/how-to-push-to-a-private-github-repository-using-a-fine-grained-personal-access-token-39ll</guid>
      <description>&lt;p&gt;If you have an existing local Git repository and need to push it to a &lt;strong&gt;private GitHub repository&lt;/strong&gt;, but GitHub is rejecting your credentials, you may need to use a &lt;strong&gt;fine-grained personal access token (PAT)&lt;/strong&gt;. This guide will walk you through the process step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Generate a Fine-Grained Personal Access Token (PAT)
&lt;/h2&gt;

&lt;p&gt;GitHub has moved away from password authentication for Git operations, so you must use a personal access token (PAT) instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.1 Navigate to GitHub Settings&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;GitHub Personal Access Tokens&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"Generate new token (fine-grained)"&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1.2 Configure Token Permissions&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository Access:&lt;/strong&gt; Select the specific repository you want to push to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Under "Repository permissions," set &lt;strong&gt;"Read and Write"&lt;/strong&gt; access for "Contents."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration:&lt;/strong&gt; Choose an expiration date or select "No expiration" (not recommended for security reasons).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Generate token"&lt;/strong&gt; and &lt;strong&gt;copy&lt;/strong&gt; the token immediately (you won’t see it again).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Set the Remote URL with the PAT
&lt;/h2&gt;

&lt;p&gt;Now, update your Git remote URL to authenticate using the token.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.1 Navigate to Your Local Repository&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Open a terminal and move to your project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your/local/repository
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2.2 Update the Remote URL&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Run the following command, replacing &lt;code&gt;{your-personal-token}&lt;/code&gt;, &lt;code&gt;{github_username}&lt;/code&gt;, and &lt;code&gt;{private-repo-name}&lt;/code&gt; accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote set-url origin https://&lt;span class="o"&gt;{&lt;/span&gt;your-personal-token&lt;span class="o"&gt;}&lt;/span&gt;@github.com/&lt;span class="o"&gt;{&lt;/span&gt;github_username&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;private-repo-name&lt;span class="o"&gt;}&lt;/span&gt;.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The token will be stored in your Git configuration. For security reasons, it’s best to use a Git credential manager instead of embedding it in the URL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Push Your Code to GitHub
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3.1 Add and Commit Changes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you haven’t already committed your changes, do so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3.2 Push to the Private Repository&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now, push your code to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is set up correctly, your repository should now be updated on GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Verify Your Push
&lt;/h2&gt;

&lt;p&gt;Go to your GitHub repository and refresh the page. You should see your files successfully pushed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: Use a Credential Manager for Better Security
&lt;/h2&gt;

&lt;p&gt;Instead of storing your token in the remote URL, you can configure a credential manager to securely store and automatically use your token when needed.&lt;/p&gt;

&lt;p&gt;For macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git credential-osxkeychain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git credential-manager-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git credential-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, configure Git to use the credential manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; credential.helper store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, you won’t need to include your token in the remote URL manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By following these steps, you should be able to push to a private GitHub repository using a fine-grained personal access token. This method ensures secure authentication without exposing your GitHub password. Always remember to keep your access tokens safe and use GitHub's credential storage features for added security.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Cloning a Private GitHub Repository Using HTTPS</title>
      <dc:creator>Daniel Akudbilla</dc:creator>
      <pubDate>Mon, 03 Mar 2025 20:52:56 +0000</pubDate>
      <link>https://dev.to/daniel_akudbilla_999ccff6/cloning-a-private-github-repository-using-https-3p4k</link>
      <guid>https://dev.to/daniel_akudbilla_999ccff6/cloning-a-private-github-repository-using-https-3p4k</guid>
      <description>&lt;p&gt;If you're here, you're either struggling to clone a private GitHub repository or simply trying to learn how. Let's dive straight into the solution.&lt;br&gt;
You have encountered this error&lt;br&gt;
&lt;code&gt;Cloning into 'repo_name'...&lt;br&gt;
remote: Write access to repository not granted.&lt;br&gt;
fatal: unable to access 'https://github.com/username/repo_name.git/': The requested URL returned error: 403&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: Generate a Personal Access Token (PAT)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To clone a private repository using HTTPS, you need a &lt;strong&gt;Personal Access Token (PAT)&lt;/strong&gt; instead of a password. Follow these steps to generate one:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;1. Log in to GitHub&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and sign in to your account.&lt;/li&gt;
&lt;li&gt;Click on your profile picture in the top-right corner.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;2. Navigate to Developer Settings&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Scroll down and click on &lt;strong&gt;Developer settings&lt;/strong&gt; (found in the left sidebar).&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Personal access tokens&lt;/strong&gt;, select &lt;strong&gt;Fine-grained tokens&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate new token&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name the token&lt;/strong&gt; (e.g., &lt;em&gt;GitHub Repo Access&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select the resource owner:&lt;/strong&gt; Choose your GitHub username (or your organization if cloning an organization repo).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set an expiration date:&lt;/strong&gt; Choose an appropriate duration for security purposes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository access:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Select &lt;strong&gt;All repositories&lt;/strong&gt; (if you want access to all your repos).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommended:&lt;/strong&gt; Select only the specific repositories you need access to.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Under &lt;strong&gt;Repository permissions&lt;/strong&gt;, find &lt;strong&gt;Contents&lt;/strong&gt; and set it to &lt;strong&gt;Read and Write&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Find &lt;strong&gt;Administration&lt;/strong&gt; and set it to &lt;strong&gt;Read and Write&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Grant additional permissions as needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmdd5rnoqiixwdqxj2wsg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmdd5rnoqiixwdqxj2wsg.png" alt="Permissions options to be selected" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;4. Generate and Copy the Token&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Generate Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Copy the generated token and store it securely, as it will not be shown again.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: Clone the Private Repository&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With your &lt;strong&gt;Personal Access Token (PAT)&lt;/strong&gt; ready, follow these steps to clone your private repository:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;1. Open Your Terminal&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;On &lt;strong&gt;Windows&lt;/strong&gt;, use &lt;strong&gt;Git Bash&lt;/strong&gt; or Command Prompt.&lt;br&gt;
On &lt;strong&gt;Mac/Linux&lt;/strong&gt;, use the Terminal.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;2. Run the Clone Command&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use the following command to clone your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://USERNAME:TOKEN@github.com/USERNAME/REPOSITORY_NAME.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USERNAME&lt;/code&gt; → Your GitHub username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TOKEN&lt;/code&gt; → Your generated Personal Access Token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REPOSITORY_NAME&lt;/code&gt; → The name of your repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://johnDoe:ghp_ABC123xyz@github.com/johnDoe/my-private-repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is correct, your repository should start cloning successfully.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I personally struggled with this issue for a while and decided to share my findings after thorough research. I hope this guide helps you avoid the same frustration. If you found this useful, share it with others who might need it!&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Happy coding!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>git</category>
    </item>
  </channel>
</rss>
