<?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: Currently Buffering</title>
    <description>The latest articles on DEV Community by Currently Buffering (@idlemode).</description>
    <link>https://dev.to/idlemode</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944931%2F684ea604-fa4a-40d7-bf53-481c0eccefdb.png</url>
      <title>DEV Community: Currently Buffering</title>
      <link>https://dev.to/idlemode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/idlemode"/>
    <language>en</language>
    <item>
      <title>I instrumented 95 DataLoaders in a production GraphQL API — here's what I found</title>
      <dc:creator>Currently Buffering</dc:creator>
      <pubDate>Thu, 21 May 2026 23:26:03 +0000</pubDate>
      <link>https://dev.to/idlemode/i-instrumented-95-dataloaders-in-a-production-graphql-api-heres-what-i-found-4416</link>
      <guid>https://dev.to/idlemode/i-instrumented-95-dataloaders-in-a-production-graphql-api-heres-what-i-found-4416</guid>
      <description>&lt;p&gt;DataLoader is the standard fix for GraphQL's N+1 query problem. Batch your database calls per request, cache within the request lifecycle, done.&lt;/p&gt;

&lt;p&gt;But once DataLoader is in production, you're flying blind. Which loaders are actually called per request? Is your cache hit rate 15% or 60%? Should your batch size be 10 or 50? APM tools tell you resolver latency, but they don't understand DataLoader batching.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://dataloader-ai.com" rel="noopener noreferrer"&gt;dataloader-ai&lt;/a&gt; to answer those questions. Then I tested it for real by instrumenting 95 DataLoader instances in &lt;a href="https://github.com/opencollective/opencollective-api" rel="noopener noreferrer"&gt;Open Collective's GraphQL API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: invisible batching
&lt;/h2&gt;

&lt;p&gt;Open Collective runs one of the largest open-source GraphQL APIs on the web. Their &lt;code&gt;server/graphql/loaders/&lt;/code&gt; directory contains 96 DataLoader instances across 20 files — loaders for collectives, expenses, transactions, members, comments, orders, and more.&lt;/p&gt;

&lt;p&gt;Without instrumentation, none of these questions are answerable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which loaders fire per request?&lt;/strong&gt; You can guess from the schema, but you don't know for sure without tracing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are batches efficient?&lt;/strong&gt; A loader called 20 times in a request should ideally create 1 batch of 20 — not 20 batches of 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the cache hit rate?&lt;/strong&gt; DataLoader's cache is per-request, but hit rate varies wildly depending on query shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the batch size right?&lt;/strong&gt; Too small = more round trips. Too big = slow batches. The default is often wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tool: dataloader-ai
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/currentlybuffering/dataloader-ai" rel="noopener noreferrer"&gt;dataloader-ai&lt;/a&gt; is a drop-in wrapper for the &lt;code&gt;dataloader&lt;/code&gt; package. Same API, zero config:&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;// before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DataLoader&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;dataloader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchLoadUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DataLoaderAI&lt;/span&gt; &lt;span class="p"&gt;}&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;dataloader-ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoaderAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchLoadUsers&lt;/span&gt;&lt;span class="p"&gt;,&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;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same &lt;code&gt;load()&lt;/code&gt;/&lt;code&gt;loadMany()&lt;/code&gt;/&lt;code&gt;clear()&lt;/code&gt;/&lt;code&gt;prime()&lt;/code&gt; API. Under the hood it tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache hit rate&lt;/strong&gt; per loader (with visual bar in terminal)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avg and p95 latency&lt;/strong&gt; per batch function&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch efficiency&lt;/strong&gt; (rolling sparkline of batch sizes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch-size recommendations&lt;/strong&gt; based on a configurable latency target&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It prints a live report to your terminal every 5 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▲ dataloader-ai 14:23:01
──────────────────────────────────────────────────────
user
  cache [████████████████░░░░░░░░] 64.2%
  avg=12.4ms p95=18.1ms batched=47 avoided=86 savings=$0.0086
  batch efficiency ▄▄█▄▅█▆▅██▄▆▇
  recommendation ↑ increase 10 → 12

product
  cache [████████░░░░░░░░░░░░░░░░] 34.1%
  avg=8.7ms p95=14.3ms batched=31 avoided=42 savings=$0.0042
  batch efficiency █▄▅▄██▄▅▆▄▅
  recommendation ↓ decrease 10 → 8

──────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key required. No account. No data leaves your machine. It works in &lt;strong&gt;local-first mode&lt;/strong&gt; — the terminal output &lt;em&gt;is&lt;/em&gt; the product. An optional cloud dashboard exists for teams who want historical trends and alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment: Open Collective's API
&lt;/h2&gt;

&lt;p&gt;I forked &lt;a href="https://github.com/opencollective/opencollective-api" rel="noopener noreferrer"&gt;opencollective/opencollective-api&lt;/a&gt; and replaced 95 of 96 &lt;code&gt;DataLoader&lt;/code&gt; instances with &lt;code&gt;DataLoaderAI&lt;/code&gt;, adding a descriptive &lt;code&gt;name&lt;/code&gt; to each:&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;// before&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoader&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;ids&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="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataLoaderAI&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;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;number&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="p"&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;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;collective-by-id&lt;/span&gt;&lt;span class="dl"&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 changes were mechanical — 20 files, 397 insertions, 379 deletions. You can see the full &lt;a href="https://github.com/currentlybuffering/opencollective-api/pull/1" rel="noopener noreferrer"&gt;fork PR here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I found
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server/graphql/loaders/index.ts&lt;/code&gt; is the hotspot&lt;/strong&gt; — 43 inline DataLoader instances in a single file (1,401 lines). This is where most collective, expense, and transaction loaders live. If you're going to instrument anything, start here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named loaders make debugging 10x easier.&lt;/strong&gt; Before, every loader was an anonymous &lt;code&gt;new DataLoader(fn)&lt;/code&gt;. After, each one has a name like &lt;code&gt;collective-by-slug&lt;/code&gt;, &lt;code&gt;expense-attached-files&lt;/code&gt;, or &lt;code&gt;tier-total-donated&lt;/code&gt;. When the terminal report prints, you immediately know which loader is slow or under-batching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;readonly&lt;/code&gt; array pattern matters.&lt;/strong&gt; DataLoaderAI tracks batch efficiency by counting keys per batch call. TypeScript's &lt;code&gt;readonly number[]&lt;/code&gt; (vs &lt;code&gt;number[]&lt;/code&gt;) makes this explicit — the batch function receives an immutable snapshot of keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One loader stayed vanilla.&lt;/strong&gt; The &lt;code&gt;buildLoaderForAssociation&lt;/code&gt; helper in &lt;code&gt;helpers.ts&lt;/code&gt; is a generic utility that creates loaders dynamically — it's not a named, domain-specific loader. It's the right call to leave it as-is rather than add a generic &lt;code&gt;name&lt;/code&gt; that doesn't tell you anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the recommendation engine works
&lt;/h2&gt;

&lt;p&gt;This is not ML. It's honest heuristics, and I want to be transparent about that.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BatchSizeOptimizer&lt;/code&gt; maintains a rolling window of batch latencies (default: last 20 batches). Every 5 batches, it checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;strong&gt;avg latency &amp;lt; 70% of target&lt;/strong&gt; → increase batch size by 20% (you have headroom)&lt;/li&gt;
&lt;li&gt;If &lt;strong&gt;avg latency &amp;gt; 130% of target&lt;/strong&gt; OR &lt;strong&gt;p95 &amp;gt; 200% of target&lt;/strong&gt; → decrease by 20% (you're overloading)&lt;/li&gt;
&lt;li&gt;Otherwise → hold (near-optimal)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default target is 50ms. If your batch function averages 12ms and your target is 50ms, the recommendation is: "you can safely batch more keys per call — increase from 10 to 12." That's a 20% reduction in round trips with zero risk.&lt;/p&gt;

&lt;p&gt;This is transparent. You can see exactly why each recommendation is made. You can configure the target latency, min/max batch size, and window size. No black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic example
&lt;/h2&gt;

&lt;p&gt;The SDK ships with a &lt;a href="https://github.com/currentlybuffering/dataloader-ai/tree/main/src/examples/realistic-ecommerce" rel="noopener noreferrer"&gt;realistic ecommerce example&lt;/a&gt; — an Apollo Server with 5 DataLoaderAI loaders (users, products, categories, reviews, orders) and a load-test script that fires 5 different query patterns.&lt;/p&gt;

&lt;p&gt;Run it:&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://github.com/currentlybuffering/dataloader-ai
&lt;span class="nb"&gt;cd &lt;/span&gt;dataloader-ai/src/examples/realistic-ecommerce
npm &lt;span class="nb"&gt;install
&lt;/span&gt;node index.ts
&lt;span class="c"&gt;# in another terminal:&lt;/span&gt;
node load-test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal report shows all 5 loaders with live metrics. The &lt;code&gt;orders&lt;/code&gt; loader (15-35ms simulated DB latency) consistently gets "increase batch size" recommendations. The &lt;code&gt;category&lt;/code&gt; loader (3-7ms) holds steady. The &lt;code&gt;reviews&lt;/code&gt; loader shows the most cache-hit variance because review queries overlap differently per request pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for your GraphQL server
&lt;/h2&gt;

&lt;p&gt;If you're running DataLoader in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add names to your loaders.&lt;/strong&gt; Even if you don't use dataloader-ai, naming your loaders makes debugging dramatically easier. Just add a &lt;code&gt;name&lt;/code&gt; property to your DataLoader options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check your batch efficiency.&lt;/strong&gt; Are you getting 1 batch of N keys, or N batches of 1 key? If resolvers call &lt;code&gt;.load()&lt;/code&gt; late in the cycle (after awaits), DataLoader can't batch them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure cache hit rate per query.&lt;/strong&gt; A query that fetches the same user 5 times in one request should have 80% cache hit rate on the user loader. If it's 0%, something is wrong with your per-request cache lifecycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tune batch sizes to your actual latency.&lt;/strong&gt; The default &lt;code&gt;maxBatchSize&lt;/code&gt; in DataLoader is &lt;code&gt;Infinity&lt;/code&gt;. Most teams set it to something arbitrary (10, 50, 100) without measuring. Use your actual batch function latency to pick the right value.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx dataloader-ai demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No install, no account, no API key. The demo simulates a GraphQL server and prints live metrics to your terminal.&lt;/p&gt;

&lt;p&gt;For your own server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;dataloader-ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then swap &lt;code&gt;DataLoader&lt;/code&gt; → &lt;code&gt;DataLoaderAI&lt;/code&gt; with a &lt;code&gt;name&lt;/code&gt; option. That's it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local mode&lt;/strong&gt;: free forever, terminal metrics, no data leaves your machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud dashboard&lt;/strong&gt;: free during beta, historical trends + alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK&lt;/strong&gt;: MIT-licensed, &lt;a href="https://github.com/currentlybuffering/dataloader-ai" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/dataloader-ai" rel="noopener noreferrer"&gt;on npm&lt;/a&gt; (1,400+ downloads/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm the solo developer behind dataloader-ai. Built it because I kept running into the same observability gap in GraphQL servers. Would love feedback from anyone running DataLoader in production.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>node</category>
      <category>dataloader</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
