<?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: ZyVOP</title>
    <description>The latest articles on DEV Community by ZyVOP (@zyvop).</description>
    <link>https://dev.to/zyvop</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%2F3954256%2Fd38d7012-ce73-4451-a739-b1b94dc54075.png</url>
      <title>DEV Community: ZyVOP</title>
      <link>https://dev.to/zyvop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zyvop"/>
    <language>en</language>
    <item>
      <title>Beyond Autocomplete: How AI Editors Actually Understand Your Codebase</title>
      <dc:creator>ZyVOP</dc:creator>
      <pubDate>Fri, 29 May 2026 05:49:04 +0000</pubDate>
      <link>https://dev.to/zyvop/beyond-autocomplete-how-ai-editors-actually-understand-your-codebase-3ang</link>
      <guid>https://dev.to/zyvop/beyond-autocomplete-how-ai-editors-actually-understand-your-codebase-3ang</guid>
      <description>&lt;p&gt;The first time an AI editor suggests the exact function signature you needed — one that lives three files away in a utility module you half-forgot existed — it feels like magic. Then it happens again. And again.&lt;/p&gt;

&lt;p&gt;It's not magic. It's not luck. And it's definitely not just autocomplete with a fancier model.&lt;/p&gt;

&lt;p&gt;This post is the no-hand-waving answer to: &lt;em&gt;how does it actually know that?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old World: Single-File Thinking
&lt;/h2&gt;

&lt;p&gt;Classic IntelliSense worked on Abstract Syntax Trees. You type:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;User&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="c1"&gt;//    ^ IDE parses User class → offers .id, .email, .save()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful. But it only answered: &lt;em&gt;"given what I can see in this one file, what tokens are syntactically valid next?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It couldn't answer: &lt;em&gt;"the *&lt;code&gt;parseUser()&lt;/code&gt;&lt;/em&gt; helper in &lt;em&gt;&lt;code&gt;utils/auth.ts&lt;/code&gt;&lt;/em&gt; has a null-email edge case — your test in &lt;em&gt;&lt;code&gt;__tests__/auth.spec.ts&lt;/code&gt;&lt;/em&gt; already covers it, so don't re-invent it."*&lt;/p&gt;

&lt;p&gt;That's not a subtle difference. That's the difference between a dictionary and a colleague.&lt;/p&gt;

&lt;p&gt;The first ML-based tools (TabNine early models, Kite) tried to go further — training on millions of GitHub repos to predict likely next tokens. But they had the same blind spot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# TabNine circa 2020 — knows that catch blocks often contain:
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# → suggests: logger.error(e) / raise / return None
&lt;/span&gt;    &lt;span class="c1"&gt;# Based on: "what do most codebases do here?"
&lt;/span&gt;    &lt;span class="c1"&gt;# NOT based on: "what does THIS codebase do here?"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model was a well-read stranger. Knowledgeable about programming in general. Completely ignorant about &lt;em&gt;your&lt;/em&gt; code in particular.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift: What Goes Into the Context Window
&lt;/h2&gt;

&lt;p&gt;The core change in modern AI editors is deceptively simple: &lt;strong&gt;the model sees more of your codebase at once.&lt;/strong&gt; But "more" isn't just quantity — it's a qualitative shift in what reasoning becomes possible.&lt;/p&gt;

&lt;p&gt;Here's how the context window has grown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Window&lt;/th&gt;
&lt;th&gt;What fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codex / early Copilot (2021)&lt;/td&gt;
&lt;td&gt;4k tokens&lt;/td&gt;
&lt;td&gt;~1 file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4 Turbo (2023)&lt;/td&gt;
&lt;td&gt;128k tokens&lt;/td&gt;
&lt;td&gt;~30–40 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude 3.5 / Gemini 1.5 (2024)&lt;/td&gt;
&lt;td&gt;200k tokens&lt;/td&gt;
&lt;td&gt;~entire small project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you trigger a suggestion today, the context window assembled for the model typically 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;[CONTEXT ASSEMBLED FOR: "write a sendWelcomeEmail function"]

1. Current file (full):          api/users.ts
2. Recently visited:             services/email.ts, db/models/user.ts
3. Imported by current file:     utils/auth.ts, config/constants.ts
4. Type definitions:             types/User.d.ts, types/Email.d.ts
5. Related tests:                __tests__/users.spec.ts
6. Config:                       tsconfig.json, package.json
7. RAG-retrieved chunks:         [see next section]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model never sees your whole repo. It sees a &lt;strong&gt;curated slice&lt;/strong&gt; — assembled by a retrieval pipeline that runs &lt;em&gt;before&lt;/em&gt; the model ever processes your query.&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG: The Invisible Brain Before the Brain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt; is the engine behind every "how did it know that?" moment. Here's exactly what happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Indexing: Your Codebase Becomes Vectors
&lt;/h3&gt;

&lt;p&gt;When you open a project, the editor quietly builds a semantic index. Every function, class, type, and docstring gets converted into a vector embedding — a list of numbers representing its &lt;em&gt;meaning&lt;/em&gt;, not its syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Source code chunk:&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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 embedding model processes it:&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="c1"&gt;//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;span class="c1"&gt;//  768 numbers encoding "email validation logic"&lt;/span&gt;

&lt;span class="c1"&gt;// Another chunk, completely different file:&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should reject emails without @ symbol&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notanemail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="c1"&gt;//  Similar vector = semantically related&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chunks with similar meaning cluster near each other in vector space — even if they share zero literal words.&lt;/p&gt;

&lt;p&gt;The chunking isn't by line count. It uses &lt;strong&gt;Tree-sitter&lt;/strong&gt; (a structural parser) to split at meaningful code boundaries: one function per chunk, one class per chunk. This ensures each chunk is a semantically complete unit, not an arbitrary slice of text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Retrieval: Semantic Search at Query Time
&lt;/h3&gt;

&lt;p&gt;When you write a comment or ask a question, your query gets embedded the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your query: "write a function that sends a welcome email to new users"

→ Query vector: [0.19, -0.79, 0.51, 0.85, -0.14, 0.63, ...]

Vector DB finds nearest neighbours:
  ✓ validateEmail()         — distance: 0.12  (email logic)
  ✓ UserProfile interface   — distance: 0.18  (user data shape)  
  ✓ emailService.sendGrid() — distance: 0.21  (email sending)
  ✓ NEW_USER_TEMPLATE const — distance: 0.24  (email templates)
  ✓ user.spec.ts fixture    — distance: 0.31  (test patterns)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those results contain the words "welcome" or "send." The retrieval found them because they're &lt;em&gt;conceptually related&lt;/em&gt; — not textually matched.&lt;/p&gt;

&lt;p&gt;This is what separates modern AI editors from grep. &lt;strong&gt;Grep finds literal matches. RAG finds conceptual neighbours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most editors also run a BM25 lexical search in parallel (a fast keyword search) and merge both result sets — so you get the best of semantic understanding &lt;em&gt;and&lt;/em&gt; exact-name matching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Reranking: Quality Filtering
&lt;/h3&gt;

&lt;p&gt;Raw retrieval returns the 50-100 most similar chunks. A reranker model then re-scores them by reading the query &lt;em&gt;and&lt;/em&gt; each chunk together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bi-encoder retrieval (fast, less accurate):
  → Scores chunks independently against query vector

Cross-encoder reranking (slow, highly accurate):
  → Reads query + chunk together
  → Models their actual interaction
  → Re-ranks the candidate set

Final top-10 chunks injected into context window ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model sees only the reranked top results. It never knows a 50-candidate shortlist was assembled and filtered before it got involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tree-sitter: The Structural Skeleton
&lt;/h2&gt;

&lt;p&gt;Tree-sitter is an incremental, error-tolerant parser that maintains a live syntax tree of your code — updated character-by-character as you type, even when your code has syntax errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You type this (incomplete, invalid syntax):&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;
  &lt;span class="c1"&gt;//                   ^ cursor here, code is broken&lt;/span&gt;

&lt;span class="c1"&gt;// Tree-sitter still produces:&lt;/span&gt;
&lt;span class="nx"&gt;FunctionDeclaration&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processOrder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;Parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;VariableDeclaration&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MemberExpression &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incomplete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Identifier&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Identifier&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even broken code gets a useful tree. This does three critical things for AI editors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Precise chunk boundaries for RAG.&lt;/strong&gt; Instead of splitting on line 50, line 100, line 150... the RAG pipeline splits at &lt;code&gt;FunctionDeclaration&lt;/code&gt; ends. Every embedded chunk is a complete, coherent unit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scope and symbol resolution.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// scope: module&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// scope: function (shadows outer)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// which userId? Tree-sitter knows.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI gets precise scope information, not guesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Surgical edits.&lt;/strong&gt; When applying a suggested change, the editor uses Tree-sitter node ranges to replace exactly &lt;code&gt;lines 14-28, columns 2-47&lt;/code&gt; — not fragile line-number approximations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semantic Graph: Thinking in Relationships
&lt;/h2&gt;

&lt;p&gt;Beyond RAG, the best editors maintain a live &lt;strong&gt;semantic graph&lt;/strong&gt; — a map of every relationship between every symbol in your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nodes:  functions, classes, types, constants, interfaces
Edges:  calls, imports, implements, uses, tests

Example subgraph:
  checkout()
    ├─ calls → validateCart()
    │            ├─ calls → getInventory()
    │            └─ uses  → CartItem (type)
    ├─ calls → processPayment()
    │            └─ calls → stripe.charge()
    ├─ uses  → Order (type)
    │            └─ uses  → LineItem (type)
    └─ tested by → checkout.spec.ts
                     ├─ uses fixture → mockCart
                     └─ uses fixture → mockPayment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This graph enables reasoning that no single file can provide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact analysis before you change anything:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You're about to change:&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// adding:&lt;/span&gt;
  &lt;span class="nl"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// ← new field&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Semantic graph instantly knows:&lt;/span&gt;
&lt;span class="c1"&gt;// → 23 files reference this interface&lt;/span&gt;
&lt;span class="c1"&gt;// → 4 will have type errors (missing accountId)&lt;/span&gt;
&lt;span class="c1"&gt;// → 2 tests use User fixtures that need updating&lt;/span&gt;
&lt;span class="c1"&gt;// → 1 DB migration needs to add the column&lt;/span&gt;
&lt;span class="c1"&gt;// → GraphQL schema needs a new field&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the graph, the AI &lt;em&gt;infers&lt;/em&gt; this from whatever's in the context window. With the graph, it &lt;em&gt;knows&lt;/em&gt; it precisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parametric vs. Retrieved Knowledge
&lt;/h2&gt;

&lt;p&gt;The model has two completely different sources of "knowledge" — and confusing them explains most AI editor failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parametric knowledge&lt;/strong&gt; — baked into weights during training:&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;// Ask: "how does Promise.allSettled differ from Promise.all?"&lt;/span&gt;
&lt;span class="c1"&gt;// Model answers from parametric knowledge — reliable, no retrieval needed&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;// → Rejects immediately if ANY promise rejects&lt;/span&gt;
&lt;span class="c1"&gt;// → Returns values array only on full success&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;// → Waits for ALL promises to finish&lt;/span&gt;
&lt;span class="c1"&gt;// → Returns [{status, value/reason}, ...] always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Retrieved knowledge&lt;/strong&gt; — read fresh from your codebase every request:&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;// Ask: "write a new admin route using our middleware pattern"&lt;/span&gt;
&lt;span class="c1"&gt;// Model reads YOUR code to understand YOUR conventions:&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieved chunk from api/users.ts:&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;// Retrieved chunk from api/orders.ts:&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;// Now generates for your new route — matching YOUR patterns:&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;// ✓ Same middleware order&lt;/span&gt;
&lt;span class="c1"&gt;// ✓ Same response shape&lt;/span&gt;
&lt;span class="c1"&gt;// ✓ Same meta helper&lt;/span&gt;
&lt;span class="c1"&gt;// ✓ Same query-forwarding pattern&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dangerous gap: asking about a library where parametric knowledge is version 3.x but your project uses version 4.x. The model will confidently generate wrong code. Always check suggestions involving third-party libraries against your actual installed version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic Loops: When the AI Iterates Like You Do
&lt;/h2&gt;

&lt;p&gt;Static suggestions work for small tasks. Complex tasks need feedback. Agentic mode closes the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task: "Add input validation to the POST /orders endpoint"

Loop iteration 1:
  AI reads:  api/orders.ts, types/Order.ts, existing validators
  AI writes: validation middleware using zod schema
  AI runs:   npm test -- orders
  AI reads:  "FAIL: missing validation for lineItems array"

Loop iteration 2:
  AI reads:  types/LineItem.ts (retrieved by semantic search)
  AI writes: adds lineItems schema to zod validator
  AI runs:   npm test -- orders
  AI reads:  "FAIL: test expects 422 status, got 400"

Loop iteration 3:
  AI reads:  api/users.ts (how validation errors are returned elsewhere)
  AI writes: changes error status to match project convention (422)
  AI runs:   npm test -- orders
  AI reads:  "PASS: 8 tests passed"
  AI done ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each iteration, the model re-reads the codebase with fresh context informed by what it just learned. It's not lucky — it's using the same feedback loop you use, powered by everything we've described: retrieval to find related code, Tree-sitter to apply precise edits, the semantic graph to understand what else might be affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Seams Show
&lt;/h2&gt;

&lt;p&gt;None of this is perfect. Each failure mode maps to a specific architectural layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval miss:&lt;/strong&gt; The AI ignores code you &lt;em&gt;know&lt;/em&gt; exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cause: RAG didn't retrieve the right chunks.
Fix:   @mention the file explicitly in your prompt.
       "In the style of @api/users.ts, write a..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stale index:&lt;/strong&gt; The AI reasons about code you deleted last week.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cause: Background indexing lagged after a large refactor.
Fix:   Trigger a manual reindex. Start a fresh chat session.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Parametric version mismatch:&lt;/strong&gt; Confident but wrong API usage.&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;// AI generates (using parametric knowledge of Mongoose v5):&lt;/span&gt;
&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="c1"&gt;// Your project uses Mongoose v8 (async/await preferred):&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// ← no .exec() needed&lt;/span&gt;

&lt;span class="nx"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Parametric&lt;/span&gt; &lt;span class="nx"&gt;knowledge&lt;/span&gt; &lt;span class="nx"&gt;doesn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;t match your installed version.
Fix:   Include your package.json or the actual function signature
       in your prompt context.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Context window saturation:&lt;/strong&gt; The AI "forgets" things in long sessions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cause: Window is full. Old context gets dropped.
Fix:   Start a new conversation for each new task.
       Don't rely on the AI retaining earlier context indefinitely.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Gap Has Closed Farther Than Most Developers Realise
&lt;/h2&gt;

&lt;p&gt;The developer who tried Copilot in 2022, found it useful for boilerplate, and formed the opinion "AI coding = fancy tab complete" is working with a two-year-old mental model.&lt;/p&gt;

&lt;p&gt;The current generation — used well — doesn't feel like a faster way to type. It feels like a collaborator who has read your entire codebase, understands your architecture, follows your conventions, and can reason about your specific system rather than code in general.&lt;/p&gt;

&lt;p&gt;That's not because models got smarter (though they did). It's because of the full stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your query
    ↓
RAG pipeline        — finds relevant code across your entire repo
    ↓
Tree-sitter         — provides precise structural grounding
    ↓
Semantic graph      — maps relationships between symbols
    ↓
Long context window — holds enough to reason coherently
    ↓
Language model      — combines parametric + retrieved knowledge
    ↓
Agentic loop        — iterates until tests pass
    ↓
Your editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every layer compounds. Remove any one of them and the experience degrades sharply.&lt;/p&gt;

&lt;p&gt;The autocomplete era is genuinely over.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/beyond-autocomplete-how-ai-editors-actually-understand-your-codebase-nrcoj" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aieditors</category>
      <category>howitworks</category>
      <category>rag</category>
      <category>cursor</category>
    </item>
    <item>
      <title>pg.Pool vs PgBouncer: When Client-Side Pooling Is Not Enough</title>
      <dc:creator>ZyVOP</dc:creator>
      <pubDate>Thu, 28 May 2026 14:37:14 +0000</pubDate>
      <link>https://dev.to/zyvop/pgpool-vs-pgbouncer-when-client-side-pooling-is-not-enough-7ag</link>
      <guid>https://dev.to/zyvop/pgpool-vs-pgbouncer-when-client-side-pooling-is-not-enough-7ag</guid>
      <description>&lt;p&gt;Every Node.js PostgreSQL tutorial shows you &lt;code&gt;pg.Pool&lt;/code&gt;. You set &lt;code&gt;max: 20&lt;/code&gt;, you get 20 reusable connections, and that works fine for a single-server app with moderate traffic. The problems start when you add a second server, or scale to 10 pods, or deploy to Lambda, or hit the PostgreSQL default connection limit of 100.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg.Pool&lt;/code&gt; is per-process. PgBouncer is per-database. That distinction determines everything about when you need one versus the other.&lt;/p&gt;

&lt;p&gt;This guide covers exactly when &lt;code&gt;pg.Pool&lt;/code&gt; alone is sufficient, when PgBouncer becomes necessary, how to configure each correctly, and the tradeoffs you accept when adding PgBouncer's transaction pooling mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pg.Pool Actually Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pg.Pool&lt;/code&gt; maintains a pool of TCP connections from a single Node.js process to PostgreSQL. When your code calls &lt;code&gt;pool.query()&lt;/code&gt;, it borrows a connection from the pool, runs the query, and returns the connection. No connection setup overhead on every query — the TCP handshake and authentication happen once at pool startup.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max&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="c1"&gt;// 20 connections from THIS process to Postgres&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key word is "this process." If you run 5 Node.js processes (5 pods, 5 PM2 workers, 5 Lambda cold starts), you have 5 × 20 = 100 connections to PostgreSQL. Postgres's default &lt;code&gt;max_connections&lt;/code&gt; is 100. You just hit the ceiling on the first query burst.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Connection Math That Breaks Production
&lt;/h2&gt;

&lt;p&gt;PostgreSQL, by default, allows 100 simultaneous connections. On a 2GB server, each connection uses around 5–10MB of memory — meaning a theoretical maximum of around 180–200 connections before memory becomes the constraint, though conservative guidance is 20–30 application connections with PgBouncer handling the multiplexing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario: 10 Node.js pods, each with pool.max = 20

10 pods × 20 connections = 200 connections → exceeds Postgres default limit

Result: "FATAL: sorry, too many clients already"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pool is not global — it is per-process and often per-container. Pools multiply with pods and processes. PgBouncer can smooth spikes, but it fundamentally changes what "pooling" means.&lt;/p&gt;

&lt;p&gt;The moment you run more than one process, &lt;code&gt;pg.Pool&lt;/code&gt; alone is no longer sufficient to control the total connection count to Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PgBouncer Actually Does
&lt;/h2&gt;

&lt;p&gt;PgBouncer sits between your application and Postgres as a TCP proxy. Your application connects to PgBouncer (port 6432). PgBouncer maintains a small pool of actual Postgres connections and queues application requests against them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 Node.js pods × 20 connections = 200 connections to PgBouncer
PgBouncer                         = 20 connections to Postgres

Postgres sees 20 connections regardless of how many pods you run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PgBouncer in transaction mode handles the multiplexing — you trade an extra network hop for a hard ceiling on actual Postgres connections. The things you lose — session-level &lt;code&gt;SET&lt;/code&gt;, advisory locks, &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; — are real, but for standard API workloads they are not typically on the critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Pooling Modes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Session mode&lt;/strong&gt; — one Postgres connection per client session. Same as no pooling. Not useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction mode&lt;/strong&gt; — one Postgres connection per transaction. Released back to the pool after &lt;code&gt;COMMIT&lt;/code&gt; or &lt;code&gt;ROLLBACK&lt;/code&gt;. This is the mode to use. It prevents connection-aging issues with certain Postgres extensions and forces natural, gradual connection recycling rather than a single big reset event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statement mode&lt;/strong&gt; — one Postgres connection per statement. Breaks anything using multi-statement transactions. Avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  When pg.Pool Alone Is Sufficient
&lt;/h2&gt;

&lt;p&gt;You do not need PgBouncer if all of the following are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You run a &lt;strong&gt;single Node.js process&lt;/strong&gt; (one server, one container, no horizontal scaling)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your total connections (&lt;code&gt;max&lt;/code&gt; × process count) stay *&lt;em&gt;under 80% of Postgres *&lt;/em&gt;&lt;code&gt;max_connections&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You do not use serverless or autoscaling (Lambda, Fargate, Cloud Run)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your queries are &lt;strong&gt;short-lived&lt;/strong&gt; (under 5 seconds) — long queries holding connections are less dangerous with a single process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a single VPS running one Node.js container with &lt;code&gt;pool.max = 20&lt;/code&gt;, &lt;code&gt;pg.Pool&lt;/code&gt; is all you need. Keep it simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  When PgBouncer Becomes Necessary
&lt;/h2&gt;

&lt;p&gt;Add PgBouncer when any of these apply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple processes or pods.&lt;/strong&gt; Even two PM2 workers double your connection count. Four PM2 workers with &lt;code&gt;connection_limit = 22&lt;/code&gt; each = 88 connections. With PgBouncer, all 4 workers share 20 server connections in transaction mode. PostgreSQL is never overwhelmed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless or autoscaling.&lt;/strong&gt; When deployed on Lambda, which opens a new connection on every cold start, adding PgBouncer in front of Postgres to multiplex connections drops connection overhead from 50ms per request to near-zero. Lambda functions cannot maintain persistent pools — PgBouncer provides the persistence layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection count approaching **&lt;code&gt;max_connections&lt;/code&gt;&lt;/strong&gt;.** The signal that tells you something is wrong is &lt;code&gt;pg_stat_activity&lt;/code&gt; showing a pile of idle connections next to a handful of active ones. Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourdb'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- If idle &amp;gt;&amp;gt; active, you have idle connections wasting Postgres memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring pg.Pool Correctly
&lt;/h2&gt;

&lt;p&gt;The pool size formula: (CPU cores × 2) + effective spindle count, as a starting point — but for most web API workloads, 10–20 per process is the right range.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/db.ts&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;Pool&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;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Maximum connections this process will open&lt;/span&gt;
  &lt;span class="c1"&gt;// For a single-process app: 10-20 is the right range&lt;/span&gt;
  &lt;span class="c1"&gt;// For multi-process: (Postgres max_connections × 0.8) / process_count&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&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;DB_POOL_SIZE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="c1"&gt;// Kill queries running longer than 5 seconds&lt;/span&gt;
  &lt;span class="c1"&gt;// The most important guard against connection exhaustion&lt;/span&gt;
  &lt;span class="na"&gt;statement_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Fail fast if no connection available — do not queue indefinitely&lt;/span&gt;
  &lt;span class="na"&gt;connectionTimeoutMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Release idle connections after 30 seconds&lt;/span&gt;
  &lt;span class="na"&gt;idleTimeoutMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Recycle connections after 7,500 queries&lt;/span&gt;
  &lt;span class="c1"&gt;// Prevents slow memory drift in long-running Postgres backends&lt;/span&gt;
  &lt;span class="c1"&gt;// Documented 2026 recommendation: set this for stable long-running servers&lt;/span&gt;
  &lt;span class="na"&gt;maxUses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Log pool errors — do not let them disappear silently&lt;/span&gt;
&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="nx"&gt;err&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pool error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;&lt;code&gt;maxUses&lt;/code&gt; prevents memory leaks — PostgreSQL backend processes can slowly leak memory over thousands of queries. Setting &lt;code&gt;maxUses: 7500&lt;/code&gt; recycles connections regularly, keeping memory stable. This is especially important for long-running server processes.&lt;/p&gt;

&lt;p&gt;Also: match pool idle timeouts to infrastructure — if your load balancer has a 60-second idle timeout, set your pool's &lt;code&gt;idleTimeoutMillis&lt;/code&gt; to 50 seconds (slightly lower). Mismatched timeouts cause "connection terminated unexpectedly" errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring PgBouncer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/pgbouncer/pgbouncer.ini
&lt;/span&gt;
&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;yourdb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=5432 dbname=yourdb&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;listen_port&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;6432&lt;/span&gt;
&lt;span class="py"&gt;listen_addr&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1       # Localhost only — never expose to internet&lt;/span&gt;
&lt;span class="py"&gt;auth_type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;md5&lt;/span&gt;
&lt;span class="py"&gt;auth_file&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/pgbouncer/userlist.txt&lt;/span&gt;

&lt;span class="c"&gt;; Transaction mode — one connection per transaction
&lt;/span&gt;&lt;span class="py"&gt;pool_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;transaction&lt;/span&gt;

&lt;span class="c"&gt;; Max connections your app sends to PgBouncer (all pods combined)
&lt;/span&gt;&lt;span class="py"&gt;max_client_conn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;

&lt;span class="c"&gt;; Max actual Postgres connections PgBouncer opens
; Rule: (Postgres max_connections × 0.8) - reserved_for_superuser
&lt;/span&gt;&lt;span class="py"&gt;default_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;75&lt;/span&gt;

&lt;span class="c"&gt;; Minimum connections kept open (warm pool for faster response)
&lt;/span&gt;&lt;span class="py"&gt;min_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="c"&gt;; Kill server connections idle longer than this
&lt;/span&gt;&lt;span class="py"&gt;server_idle_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;600&lt;/span&gt;

&lt;span class="c"&gt;; Kill client connections idle longer than this
&lt;/span&gt;&lt;span class="py"&gt;client_idle_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0    ; 0 = disabled (app manages its own connections)&lt;/span&gt;

&lt;span class="c"&gt;; Silence logs for expected events
&lt;/span&gt;&lt;span class="py"&gt;log_connections&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;log_disconnections&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;log_pooler_errors&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/pgbouncer/userlist.txt
# Format: "username" "md5hash"
# Generate hash: echo -n "passwordusername" | md5sum → prefix with "md5"
"appuser" "md5abc123..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;pg.Pool&lt;/code&gt; then points at PgBouncer instead of Postgres directly:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Port 6432 = PgBouncer, not 5432 = Postgres directly&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Points to PgBouncer host:6432&lt;/span&gt;
  &lt;span class="na"&gt;max&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="c1"&gt;// Per-process limit — PgBouncer handles the Postgres side&lt;/span&gt;
  &lt;span class="na"&gt;statement_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&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;h2&gt;
  
  
  What You Lose in Transaction Mode
&lt;/h2&gt;

&lt;p&gt;Transaction pooling mode is not free. These features require a persistent server connection across multiple transactions — they break or behave unexpectedly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session-level **&lt;code&gt;SET&lt;/code&gt;&lt;/strong&gt; commands** — &lt;code&gt;SET search_path = myschema&lt;/code&gt; applies to the session. In transaction mode, the connection is returned to the pool after each transaction, so session settings are lost. Use schema-qualified names instead: &lt;code&gt;SELECT * FROM myschema.users&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advisory locks&lt;/strong&gt; — &lt;code&gt;pg_advisory_lock()&lt;/code&gt; is session-scoped. Does not work in transaction mode. Use row-level locks with &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; — requires a persistent connection. Route &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; through a dedicated connection that bypasses PgBouncer, or use a separate Redis pub/sub instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prepared statements&lt;/strong&gt; — &lt;code&gt;PREPARE&lt;/code&gt; is session-scoped. In transaction mode, disable them: &lt;code&gt;options=-c statement_cache_mode=describe&lt;/code&gt; in the PgBouncer database string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# pgbouncer.ini — disable prepared statements for transaction mode
&lt;/span&gt;&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;yourdb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=5432 dbname=yourdb options=-c statement_cache_mode=describe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in your pg.Pool 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Disable prepared statements — not supported in PgBouncer transaction mode&lt;/span&gt;
  &lt;span class="c1"&gt;// When using Prisma, set: datasources.db.url contains ?pgbouncer=true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring Connection Health
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check current connection state in Postgres&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_start&lt;/span&gt;&lt;span class="p"&gt;)))::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_age_secs&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourdb'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Connections by application&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourdb'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;idle&lt;/code&gt; count much higher than &lt;code&gt;active&lt;/code&gt; → over-provisioned pool or idle connections not being released&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;idle in transaction&lt;/code&gt; count growing → queries running inside open transactions that have not committed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;active&lt;/code&gt; count at &lt;code&gt;max_connections&lt;/code&gt; → pool exhaustion imminent&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add to Prometheus (from the earlier metrics section) and alert on &lt;code&gt;pg_pool_waiting &amp;gt; 5&lt;/code&gt; for 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Single process or container?
└── pg.Pool alone, max: 10-20, statement_timeout: 5000

Multiple processes/pods but total connections &amp;lt; 80% of max_connections?
└── pg.Pool alone, size it correctly per-process

Multiple pods OR serverless OR total connections near max_connections?
└── Add PgBouncer in transaction mode
    └── pg.Pool → PgBouncer (500 client connections)
    └── PgBouncer → Postgres (20-75 server connections)
    └── Audit: no advisory locks, no LISTEN/NOTIFY, no session SET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PgBouncer adds one more thing to operate and debug. Do not add it before you need it. Do add it before you hit &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt; in production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/pg-pool-vs-pgbouncer-when-client-side-pooling-is-not-enough-62get" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pgbouncervspgpool</category>
      <category>connection</category>
      <category>pgbouncertransactionmode</category>
      <category>pgpoolmaxconnections</category>
    </item>
    <item>
      <title>Docker for Developers: Stop "It Works on My Machine" Forever</title>
      <dc:creator>ZyVOP</dc:creator>
      <pubDate>Thu, 28 May 2026 07:21:38 +0000</pubDate>
      <link>https://dev.to/zyvop/docker-for-developers-stop-it-works-on-my-machine-forever-35pj</link>
      <guid>https://dev.to/zyvop/docker-for-developers-stop-it-works-on-my-machine-forever-35pj</guid>
      <description>&lt;h2&gt;
  
  
  Why Docker Matters for Developers
&lt;/h2&gt;

&lt;p&gt;Before Docker, setting up a project on a new machine was a ritual of pain. Clone the repo. Install the right Node version. Oh wait, you need a specific version of PostgreSQL. And Redis. And now your global npm packages conflict with another project. And somehow it works fine on your colleague's MacBook but not on yours.&lt;/p&gt;

&lt;p&gt;Docker eliminates this entirely. A container packages everything the app needs — the runtime, system libraries, dependencies, configuration — into a single portable unit. Run it on your laptop, your teammate's Linux machine, or a cloud server, and the behavior is identical. The environment is part of the code.&lt;/p&gt;

&lt;p&gt;For teams, the productivity gain compounds. Onboarding goes from "follow this 20-step setup guide and pray" to &lt;code&gt;docker compose up&lt;/code&gt;. New developers are productive within minutes, not days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers vs Virtual Machines
&lt;/h2&gt;

&lt;p&gt;Before going further, it helps to know what a container actually is — because it's not a virtual machine.&lt;/p&gt;

&lt;p&gt;A virtual machine emulates an entire computer, including its own operating system kernel. It's heavy: VMs take minutes to boot and consume gigabytes of RAM.&lt;/p&gt;

&lt;p&gt;A container shares the host machine's OS kernel but isolates the application's filesystem, processes, and network. It's lightweight: containers start in seconds and use only the memory your app actually needs.&lt;/p&gt;

&lt;p&gt;Docker is the tool that creates and manages these containers. A &lt;strong&gt;Docker image&lt;/strong&gt; is a read-only snapshot of a filesystem — your app's code, dependencies, and runtime baked in. A &lt;strong&gt;container&lt;/strong&gt; is a running instance of that image. One image can run as many containers as you want simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Your First Dockerfile
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Dockerfile&lt;/code&gt; is the recipe for building your image. It's a text file with a sequence of instructions that Docker executes layer by layer.&lt;/p&gt;

&lt;p&gt;Here's a basic Node.js Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "src/index.js"]&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Walking through each line:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FROM node:20-alpine&lt;/code&gt; — Every image starts from a base image. &lt;code&gt;node:20-alpine&lt;/code&gt; is the official Node.js 20 image built on Alpine Linux, a minimal distro that keeps the image small (~170MB vs ~1GB for the full Debian-based image).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WORKDIR /app&lt;/code&gt; — Sets the working directory inside the container. All subsequent commands run from here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COPY package*.json ./&lt;/code&gt; — Copies &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; into the container before copying source code. This is intentional — keep reading.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RUN npm ci&lt;/code&gt; — Installs dependencies. &lt;code&gt;npm ci&lt;/code&gt; (clean install) is preferred over &lt;code&gt;npm install&lt;/code&gt; in Docker because it's faster, deterministic, and respects the lockfile exactly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COPY . .&lt;/code&gt; — Copies the rest of your source code into the container.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt; — Documents that the container listens on port 3000. This doesn't publish the port — it's metadata for whoever runs the container.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CMD ["node", "src/index.js"]&lt;/code&gt; — The default command to run when the container starts. Use the array form (exec form) rather than a string — it avoids running your process as a child of a shell, which causes issues with signal handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Layer Caching — The Most Important Docker Concept
&lt;/h2&gt;

&lt;p&gt;Each instruction in a Dockerfile creates a layer. Docker caches each layer and only rebuilds from the point where something changed. This makes rebuilds dramatically faster — but only if you structure your Dockerfile to take advantage of it.&lt;/p&gt;

&lt;p&gt;Consider what happens if you copy everything first and then install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# 🚫 Bad — cache busts on every source code change&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

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

&lt;/div&gt;



&lt;p&gt;Every time you change a single line of source code, Docker invalidates the cache at the &lt;code&gt;COPY&lt;/code&gt; step — and then re-runs &lt;code&gt;npm ci&lt;/code&gt;, downloading all your dependencies again. On a project with 500 packages, that's painful.&lt;/p&gt;

&lt;p&gt;The fix is to copy dependency files first and install, then copy source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ Good — dependencies only reinstall when package.json changes&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now when you change source code, Docker uses the cached layer for &lt;code&gt;npm ci&lt;/code&gt; and only rebuilds from the &lt;code&gt;COPY . .&lt;/code&gt; step onward. Your build goes from 60 seconds to 3 seconds.&lt;/p&gt;

&lt;p&gt;This pattern applies to any ecosystem. In Python, copy &lt;code&gt;requirements.txt&lt;/code&gt; and run &lt;code&gt;pip install&lt;/code&gt; before copying source. In Go, copy &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;go.sum&lt;/code&gt; and run &lt;code&gt;go mod download&lt;/code&gt; first. Same principle everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Stage Builds: Keeping Production Images Small
&lt;/h2&gt;

&lt;p&gt;Your development image needs build tools, compilers, dev dependencies, and test frameworks. Your production image needs none of that — just the compiled output and runtime dependencies. Multi-stage builds let you use a heavy image for building and copy only the result into a minimal final image.&lt;/p&gt;

&lt;p&gt;Here's a TypeScript app with a multi-stage build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ── Stage 1: Build ────────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; tsconfig.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src ./src&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build          &lt;span class="c"&gt;# Outputs compiled JS to /app/dist&lt;/span&gt;

&lt;span class="c"&gt;# ── Stage 2: Production ───────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Only copy production dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="c"&gt;# Copy compiled output from the builder stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The final image contains only the Alpine base, production node_modules, and your compiled code. No TypeScript compiler, no source files, no dev dependencies. A typical image shrinks from 800MB to under 150MB. Smaller images mean faster pulls, smaller attack surfaces, and lower storage costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The .dockerignore File
&lt;/h2&gt;

&lt;p&gt;Just like &lt;code&gt;.gitignore&lt;/code&gt;, a &lt;code&gt;.dockerignore&lt;/code&gt; file tells Docker what to exclude when copying your project into the image. Without it, you're copying &lt;code&gt;node_modules&lt;/code&gt; (hundreds of MB), &lt;code&gt;.git&lt;/code&gt; history, test files, and local env files into every build context — which slows down builds and risks leaking sensitive files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# .dockerignore&lt;/span&gt;
node_modules
.git
.gitignore
*.md
.env
.env.*
dist
coverage
.nyc_output
.DS_Store
Dockerfile*
docker-compose*

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

&lt;/div&gt;



&lt;p&gt;Always create this file before your first &lt;code&gt;docker build&lt;/code&gt;. It's one of those things that's much easier to add upfront than to chase down why your builds are slow later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Compose: Running Multi-Service Apps Locally
&lt;/h2&gt;

&lt;p&gt;Real applications don't run in isolation. They have a database, a cache, maybe a message queue, maybe a background worker. Docker Compose lets you define and run all of these together in a single configuration file.&lt;/p&gt;

&lt;p&gt;Here's a &lt;code&gt;docker-compose.yml&lt;/code&gt; for a Node.js app with PostgreSQL and Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://postgres:password@db:5432/myapp&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://cache:6379&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;A few things worth understanding here:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;volumes: - .:/app&lt;/code&gt; — This mounts your local source code directory into the container at &lt;code&gt;/app&lt;/code&gt;. When you edit a file on your host machine, the change is immediately reflected inside the container. Combined with a dev server that watches for file changes (like &lt;code&gt;nodemon&lt;/code&gt;), you get hot reloading inside Docker.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;- /app/node_modules&lt;/code&gt; — This is an anonymous volume that prevents the host's &lt;code&gt;node_modules&lt;/code&gt; from overwriting the container's. Without this, your macOS &lt;code&gt;node_modules&lt;/code&gt; would overwrite the Linux ones inside the container, causing native module failures.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;depends_on&lt;/code&gt;** with **&lt;code&gt;condition: service_healthy&lt;/code&gt; — This tells Docker to wait until the database passes its healthcheck before starting the app. Without this, your app might start before PostgreSQL is ready to accept connections, resulting in a connection error on boot.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postgres_data&lt;/code&gt;** named volume** — Database data is stored in a named volume managed by Docker, not in your project directory. This persists across container restarts and &lt;code&gt;docker compose down&lt;/code&gt; commands. Running &lt;code&gt;docker compose down -v&lt;/code&gt; removes the volumes too — useful for a clean reset.&lt;/p&gt;

&lt;p&gt;To start everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up          &lt;span class="c"&gt;# Start all services, watch logs&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# Start in background (detached)&lt;/span&gt;
docker compose down        &lt;span class="c"&gt;# Stop and remove containers&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;     &lt;span class="c"&gt;# Stop, remove containers AND volumes (full reset)&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Useful Docker Commands You'll Actually Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Running commands inside a container:&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;# Open a shell in a running container&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;app sh

&lt;span class="c"&gt;# Run a one-off command (e.g. database migration)&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;app npm run migrate

&lt;span class="c"&gt;# Run a command in a new container (not the running one)&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; app npm run seed

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inspecting what's happening:&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;# Follow logs for all services&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Follow logs for a specific service&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; app

&lt;span class="c"&gt;# See running containers and resource usage&lt;/span&gt;
docker stats

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cleaning up:&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;# Remove stopped containers, unused networks, dangling images&lt;/span&gt;
docker system prune

&lt;span class="c"&gt;# Remove everything including unused images (be careful)&lt;/span&gt;
docker system prune &lt;span class="nt"&gt;-a&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rebuilding after Dockerfile changes:&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;# Force a rebuild without cache&lt;/span&gt;
docker compose build &lt;span class="nt"&gt;--no-cache&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Development vs Production Compose Files
&lt;/h2&gt;

&lt;p&gt;You'll often want slightly different configurations for development and production — maybe development has volume mounts and a dev server, while production uses the built image. Docker Compose supports this with file layering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Base config used everywhere
docker-compose.yml

# Development overrides
docker-compose.dev.yml

# Production overrides
docker-compose.prod.yml

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

&lt;/div&gt;



&lt;p&gt;Run with a specific override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up

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

&lt;/div&gt;



&lt;p&gt;Or set &lt;code&gt;COMPOSE_FILE&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker-compose.yml:docker-compose.dev.yml

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

&lt;/div&gt;



&lt;p&gt;This keeps the shared configuration DRY while allowing environment-specific differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Docker gives you reproducible environments, consistent behavior across machines, and a clean way to run complex multi-service applications locally without installing anything directly on your host. The key concepts to internalize: layer caching makes builds fast (copy dependency files before source code), multi-stage builds keep production images lean, &lt;code&gt;.dockerignore&lt;/code&gt; prevents bloated build contexts, and Docker Compose orchestrates multi-service apps with a single command.&lt;/p&gt;

&lt;p&gt;Once Docker becomes part of your workflow, you'll wonder how you ever managed without it. Onboarding new developers, reproducing bugs, running integration tests — all of it gets simpler when the environment is defined in code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/docker-for-developers-stop-it-works-on-my-machine-forever-b5kpu" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>containers</category>
      <category>dockerfile</category>
      <category>dockercompose</category>
    </item>
    <item>
      <title>Node.js Performance Profiling: Finding the Bottleneck Before Your Users Do</title>
      <dc:creator>ZyVOP</dc:creator>
      <pubDate>Wed, 27 May 2026 15:22:51 +0000</pubDate>
      <link>https://dev.to/zyvop/nodejs-performance-profiling-finding-the-bottleneck-before-your-users-do-bnm</link>
      <guid>https://dev.to/zyvop/nodejs-performance-profiling-finding-the-bottleneck-before-your-users-do-bnm</guid>
      <description>&lt;p&gt;There is a specific kind of production bug that is worse than a crash: a performance regression. A crash is visible. It pages someone, generates an error in Sentry, and gets fixed. A performance regression sits in the background — requests take 400ms instead of 80ms, the event loop lags under load, a specific endpoint times out occasionally. Users leave. Support tickets accumulate. The team assumes it is infrastructure.&lt;/p&gt;

&lt;p&gt;It is almost never infrastructure. It is almost always code.&lt;/p&gt;

&lt;p&gt;This guide covers the full profiling workflow: identifying the problem from metrics, profiling the CPU and event loop, reading flame graphs, and fixing the specific patterns responsible for most Node.js performance regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know What You Are Looking For First
&lt;/h2&gt;

&lt;p&gt;Before profiling, you need to know which metric is degraded. Guessing which code to optimize without measurement is how you spend a day optimizing a function that runs 10 times per day.&lt;/p&gt;

&lt;p&gt;The signals that indicate specific problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High CPU, slow requests&lt;/strong&gt; → CPU-bound work blocking the event loop. Look for synchronous operations, expensive computations, or tight loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normal CPU, high latency&lt;/strong&gt; → I/O-bound or event loop lag. Look for unoptimized database queries, N+1 patterns, missing indexes, or slow external API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory climbing&lt;/strong&gt; → Leak. See the memory leaks guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High CPU under moderate load&lt;/strong&gt; → Garbage collector thrashing. Many short-lived allocations, or large objects being created repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specific endpoint slow, others fine&lt;/strong&gt; → Query or logic problem scoped to that code path. Profile that endpoint specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;clinic.js&lt;/strong&gt; — the most useful suite for Node.js profiling. Three tools in one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;clinic doctor&lt;/code&gt; — identifies the category of problem (CPU, I/O, memory, event loop)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;clinic flame&lt;/code&gt; — CPU flame graph, shows where time is spent&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;clinic bubbleprof&lt;/code&gt; — async profiling, shows where the event loop waits&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;0x&lt;/strong&gt; — single-command flame graphs. Faster to use than clinic when you already know it is a CPU issue.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--prof&lt;/code&gt;** + **&lt;code&gt;node --prof-process&lt;/code&gt; — V8's built-in profiler, no dependencies required, produces similar data to flame graphs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g clinic 0x autocannon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1 — clinic doctor (Diagnose First)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clinic doctor&lt;/code&gt; runs your app under load and produces a report that tells you which category of problem you have before you spend time on the wrong kind of profiling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Start your app under clinic doctor
clinic doctor -- node src/server.js

# In another terminal, apply load
autocannon -c 100 -d 30 http://localhost:3000/api/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the load stops, clinic doctor opens a report in your browser showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Event loop delay&lt;/strong&gt; — if high, you have synchronous blocking or very heavy async operations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU usage&lt;/strong&gt; — if consistently at 100%, you have CPU-bound work&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt; — if climbing, you have a leak&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Handles/requests&lt;/strong&gt; — if handles grow without requests growing proportionally, something is not being cleaned up&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The report recommends which clinic tool to use next. Follow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — clinic flame (CPU Profiling)
&lt;/h2&gt;

&lt;p&gt;When doctor indicates a CPU problem, &lt;code&gt;clinic flame&lt;/code&gt; produces a flame graph — a visualization where the width of each bar represents how much CPU time that function consumed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clinic flame -- node src/server.js

# Apply focused load on the slow endpoint
autocannon -c 50 -d 20 http://localhost:3000/api/search?q=laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reading a flame graph:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The bottom of the graph is the call stack entry point&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each bar above represents a function called by the one below it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Width = time spent in that function (wider = more time)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The top of each stack is where execution was when the sample was taken&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Look for wide bars near the top&lt;/strong&gt; — these are the expensive functions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common patterns in Node.js flame graphs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wide bar in JSON.parse or JSON.stringify&lt;/strong&gt; — you are serializing large objects frequently. Consider streaming responses or reducing payload size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wide bar in a regex function&lt;/strong&gt; — a regex is more expensive than expected, often because it is catastrophically backtracking. Test your regexes with &lt;code&gt;rexploit&lt;/code&gt; or similar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wide bar in bcrypt or crypto&lt;/strong&gt; — expected for hashing, but if it is in the hot path (every request, not just login), something is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wide bar in your own business logic&lt;/strong&gt; — investigate that function. Is it doing a computation that could be cached? Is it called more often than expected?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Profile a specific script rather than a server
0x --open -- node src/scripts/generate-report.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — clinic bubbleprof (Async/I/O Profiling)
&lt;/h2&gt;

&lt;p&gt;When doctor indicates I/O or event loop problems — not CPU — use bubbleprof. It shows where your code is waiting, not where it is running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clinic bubbleprof -- node src/server.js
autocannon -c 50 -d 20 http://localhost:3000/api/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bubbleprof shows a graph of async operations — database queries, HTTP calls, file I/O — and how long each one takes. Wide nodes are long waits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential awaits that could be parallel:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SLOW — these run one after another
const user    = await getUser(userId);
const orders  = await getOrders(userId);
const profile = await getProfile(userId);
// Total time: getUser + getOrders + getProfile

// FAST — these run in parallel
const [user, orders, profile] = await Promise.all([
  getUser(userId),
  getOrders(userId),
  getProfile(userId),
]);
// Total time: max(getUser, getOrders, getProfile)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Missing connection pool configuration:&lt;/strong&gt; If database operations show up as long waits, check your pool size. The default &lt;code&gt;pg&lt;/code&gt; pool is 10 connections. Under 100 concurrent requests, requests queue waiting for a connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const pool = new Pool({
  connectionString: env.DATABASE_URL,
  max:             20,              // Increase pool size
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,  // Fail fast if no connection available
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 — Event Loop Monitoring in Production
&lt;/h2&gt;

&lt;p&gt;Flame graphs are taken in controlled environments. Production can behave differently. Add event loop lag monitoring to your metrics so you see regressions as they happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/lib/metrics.ts
import { monitorEventLoopDelay } from 'perf_hooks';

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

// Gauge for Prometheus
const eventLoopLag = new client.Gauge({
  name: 'nodejs_event_loop_lag_p99_ms',
  help: 'Node.js event loop lag 99th percentile in milliseconds',
});

// Sample every 10 seconds
setInterval(() =&amp;gt; {
  // histogram values are in nanoseconds
  const p99Ms = histogram.percentile(99) / 1_000_000;
  eventLoopLag.set(p99Ms);
  histogram.reset();
}, 10_000);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event loop lag above 100ms is a warning. Above 500ms, users are noticeably affected. Above 1000ms, requests are timing out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — The Common Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Synchronous Operations in the Hot Path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// BLOCKS the event loop — no other requests can be handled during this
const data = fs.readFileSync('/large/file.json');
const parsed = JSON.parse(data);

// Non-blocking — event loop stays free
const data = await fs.promises.readFile('/large/file.json', 'utf-8');
const parsed = JSON.parse(data);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never use &lt;code&gt;*Sync&lt;/code&gt; functions (&lt;code&gt;readFileSync&lt;/code&gt;, &lt;code&gt;execSync&lt;/code&gt;, &lt;code&gt;writeFileSync&lt;/code&gt;) in request handlers. They block the entire Node.js event loop for their duration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expensive Computations
&lt;/h3&gt;

&lt;p&gt;Move CPU-intensive work off the main thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';

// In your route handler
function runInWorker(scriptPath: string, data: unknown): Promise {
  return new Promise((resolve, reject) =&amp;gt; {
    const worker = new Worker(scriptPath, { workerData: data });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) =&amp;gt; {
      if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
    });
  });
}

// For truly CPU-intensive work (report generation, image processing):
router.post('/reports/generate', authenticate, async (req, res) =&amp;gt; {
  const report = await runInWorker('./workers/reportGenerator.js', {
    tenantId: req.tenant.id,
    params:   req.body,
  });
  res.json(report);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reducing Allocations in Hot Paths
&lt;/h3&gt;

&lt;p&gt;Object and array allocations in tight loops create GC pressure. Reuse objects where possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Creates a new object on every request — GC pressure at scale
app.use((req, res, next) =&amp;gt; {
  req.context = {
    requestId: randomUUID(),
    startTime: Date.now(),
    user:      null,
  };
  next();
});

// Acceptable — the allocation is necessary. But avoid allocating
// inside loops or functions called thousands of times per second.
// Profile first. Optimize only what the flame graph shows is hot.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caching Repeated Computations
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Recomputed on every request — if this is slow, cache it
async function getMenuItems(tenantId: string) {
  return db.query('SELECT * FROM menu_items WHERE tenant_id = $1', [tenantId]);
}

// Cache with LRU — computed once, served from memory
import { LRUCache } from 'lru-cache';

const menuCache = new LRUCache({
  max: 100,
  ttl: 5 * 60 * 1000,  // 5 minutes
});

async function getMenuItems(tenantId: string) {
  const cached = menuCache.get(tenantId);
  if (cached) return cached;

  const result = await db.query(
    'SELECT * FROM menu_items WHERE tenant_id = $1',
    [tenantId]
  );
  menuCache.set(tenantId, result.rows);
  return result.rows;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Profiling Workflow in One Sequence
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Detect: Grafana shows event loop lag or p99 latency spike
                ↓
2. Reproduce: Identify which endpoint or operation is slow
                ↓
3. Diagnose: clinic doctor → which category (CPU, I/O, memory, event loop)
                ↓
4. Profile:
   CPU issue  → clinic flame or 0x
   I/O issue  → clinic bubbleprof
   Memory     → heap snapshots (see memory leaks guide)
                ↓
5. Identify: Find the wide bar in the flame graph or the long wait in bubbleprof
                ↓
6. Fix: Apply the appropriate pattern (parallel awaits, caching, worker thread, remove sync op)
                ↓
7. Verify: Run autocannon before and after. Compare p99 latency.
                ↓
8. Monitor: Confirm event loop lag drops in Grafana after deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profiling is not something you do once. Set up the metrics, watch them in production, and run a profiling session when they degrade. The regression that would have taken days to diagnose by reading code takes 30 minutes when you can see exactly where time is being spent.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/node-js-performance-profiling-finding-the-bottleneck-before-your-users-do-7i1je" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nodejsperformanceprofiling</category>
      <category>clinicjsnodejs</category>
      <category>flamegraphnodejs</category>
      <category>0xprofiling</category>
    </item>
  </channel>
</rss>
