<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Akash's Engineering]]></title><description><![CDATA[Akash's Engineering]]></description><link>https://blog.akashpanchal.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 15:16:22 GMT</lastBuildDate><atom:link href="https://blog.akashpanchal.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Context Engineering Part 2: The Sliding Window Trap]]></title><description><![CDATA[The most common solution to AI amnesia is also the worst: sliding windows. It seems reasonable—keep only the most recent messages, drop the oldest when full. But this seemingly logical approach create]]></description><link>https://blog.akashpanchal.com/context-engineering-part-2-sliding-window-trap</link><guid isPermaLink="true">https://blog.akashpanchal.com/context-engineering-part-2-sliding-window-trap</guid><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Mon, 16 Mar 2026 07:21:54 GMT</pubDate><content:encoded><![CDATA[<p>The most common solution to AI amnesia is also the worst: sliding windows. It seems reasonable—keep only the most recent messages, drop the oldest when full. But this seemingly logical approach creates more problems than it solves.</p>
<h2><strong>The Sliding Window Approach</strong></h2>
<p>Here's how sliding windows work:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/cfbe312e-e653-4a3f-92dd-9e5720a68392.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Implementation</strong></h3>
<pre><code class="language-plaintext">class SlidingWindow:
    def __init__(self, window_size=10):
        self.window_size = window_size
        self.messages = []

    def add_message(self, message):
        self.messages.append(message)
        if len(self.messages) &gt; self.window_size:
            self.messages = self.messages[-self.window_size:]

    def get_context(self):
        return self.messages
</code></pre>
<p>Simple. Clean. And <strong>deeply flawed</strong>.</p>
<h2><strong>The Critical Problem: You Lose What Matters Most</strong></h2>
<p>Consider this real conversation:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/6a899dbf-8e54-4254-bc6e-d3d74a5842b9.png" alt="" style="display:block;margin:0 auto" />

<p><strong>LOST</strong>: We're building an inventory system, using Django, with PostgreSQL, needing real-time tracking.</p>
<p>The AI knows about AWS and 10K users, but has no idea <em>what we're actually building</em>.</p>
<h2><strong>System Prompt Vulnerability</strong></h2>
<p>Even worse—losing the system prompt:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/ad663dfe-f7d1-413f-867f-da3557019c4d.png" alt="" style="display:block;margin:0 auto" />

<p>This isn't hypothetical—it's a real vulnerability in production systems.</p>
<h2><strong>When Sliding Window Works</strong></h2>
<p>Despite flaws, it works for:</p>
<ul>
<li><p>Quick Q&amp;A (independent questions)</p>
</li>
<li><p>Translation tasks (no long-term context)</p>
</li>
<li><p>Stateless API calls (self-contained requests)</p>
</li>
<li><p>Real-time chat support (only recent messages matter)</p>
</li>
</ul>
<h2><strong>Priority Sliding Window: A Small Fix</strong></h2>
<p>Always keep the system prompt:</p>
<pre><code class="language-plaintext">def priority_sliding(messages, window_size):
    system_msgs = [m for m in messages if m['role'] == 'system']
    other_msgs = [m for m in messages if m['role'] != 'system']
    
    available_space = window_size - len(system_msgs)
    recent_msgs = other_msgs[-available_space:]
    
    return system_msgs + recent_msgs
</code></pre>
<p><strong>Better:</strong> System instructions preserved.<br /><strong>Still bad:</strong> Early conversation context lost.</p>
<h2><strong>The Core Issue</strong></h2>
<p>Sliding windows treat all messages as equally disposable. But in reality, some context is more valuable than others.</p>
<p>This brings us to token-based management—our next topic.</p>
<hr />
<p><em>Read</em> <em><strong>Part 3: Beyond Message Counting to learn how smart token allocation solves some of these problems.</strong></em></p>
<p>#AI #ContextEngineering #SoftwareDevelopment #Tech</p>
]]></content:encoded></item><item><title><![CDATA[Context Engineering Part 1: Why Your AI Chatbot Forgets Everything]]></title><description><![CDATA[Every Large Language Model has amnesia. And it's not a bug—it's a fundamental design constraint that costs companies millions in lost productivity and wrong code decisions.
In this first part of our C]]></description><link>https://blog.akashpanchal.com/context-engineering-part-1-why-your-ai-chatbot-forgets-everything</link><guid isPermaLink="true">https://blog.akashpanchal.com/context-engineering-part-1-why-your-ai-chatbot-forgets-everything</guid><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Thu, 12 Mar 2026 02:16:30 GMT</pubDate><content:encoded><![CDATA[<p>Every Large Language Model has amnesia. And it's not a bug—it's a fundamental design constraint that costs companies millions in lost productivity and wrong code decisions.</p>
<p>In this first part of our Context Engineering series, we'll explore the root cause of AI memory loss and why understanding the context window is critical for building production AI systems.</p>
<h2><strong>The Context Window: AI's Working Memory</strong></h2>
<p>Think of a context window as a whiteboard in a meeting room. You can only write so much on it before you run out of space. When you do, you must erase something old to write something new—and whatever you erase is <em>gone</em>.</p>
<h3><strong>What is a Context Window?</strong></h3>
<p>Every LLM can only "see" a finite amount of text at any given time. This finite text space is called the <strong>context window</strong>. It's measured in <strong>tokens</strong> (roughly 0.75 words per token).</p>
<p>Here's how context windows have evolved over time:</p>
<table>
<thead>
<tr>
<th><strong>Model</strong></th>
<th><strong>Year</strong></th>
<th><strong>Context Window</strong></th>
<th><strong>Approx. Pages</strong></th>
</tr>
</thead>
<tbody><tr>
<td>GPT-3</td>
<td>2022</td>
<td>4K tokens</td>
<td>~3 pages</td>
</tr>
<tr>
<td>GPT-4</td>
<td>2023</td>
<td>32K tokens</td>
<td>~25 pages</td>
</tr>
<tr>
<td>Claude 3</td>
<td>2024</td>
<td>200K tokens</td>
<td>~150 pages</td>
</tr>
<tr>
<td>Gemini 1.5 Pro</td>
<td>2024</td>
<td>1M tokens</td>
<td>~750 pages</td>
</tr>
<tr>
<td>GPT-4.1</td>
<td>2025</td>
<td>1M tokens</td>
<td>~750 pages</td>
</tr>
<tr>
<td>Llama 4 Scout</td>
<td>2026</td>
<td>10M tokens</td>
<td>~7,500 pages</td>
</tr>
</tbody></table>
<h3><strong>The Real-World Impact</strong></h3>
<p>The context window directly controls five critical factors in your AI applications:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/3f19b837-d78a-4092-9558-0a7ae24f85b1.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>The Amnesia Problem: A Concrete Example</strong></h2>
<p>Here's a perfectly normal conversation that breaks without proper context management:</p>
<pre><code class="language-plaintext"># Turn 1
user: "My name is Akash, I'm building a React app with TypeScript."
assistant: "Nice to meet you, Akash! What features are you implementing?"

# Turn 2  
user: "User authentication and a real-time dashboard."
assistant: "Great choices. What's your backend stack?"

# Turn 3
user: "Node.js with PostgreSQL and Redis for caching."
assistant: "Solid stack! What specific issue are you facing?"

# Turn 4
user: "The WebSocket connections keep dropping."
assistant: "Let me help debug that. Can you share your config?"

# ...20 turns later...

# Turn 24
user: "What tech stack am I using again?"
assistant: "I'm not sure — could you remind me?"  # ← CONTEXT LOST
</code></pre>
<p>The model didn't forget because it's bad. It forgot because <strong>those early messages were pushed out of the context window</strong>.</p>
<h2><strong>What Happens When the Window Overflows?</strong></h2>
<p>When new messages arrive but the window is full, one of four things happens:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/4c63327e-7cc4-4d8c-84c2-78a2d991723c.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>The Four Failure Modes</strong></h3>
<table>
<thead>
<tr>
<th><strong>Failure Mode</strong></th>
<th><strong>What You See</strong></th>
<th><strong>Example</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Context Drift</strong></td>
<td>Model loses the original topic</td>
<td>Started discussing React, now answering about Python</td>
</tr>
<tr>
<td><strong>Repetition</strong></td>
<td>Model re-asks for information already provided</td>
<td>"What framework are you using?" (you said React 5 turns ago)</td>
</tr>
<tr>
<td><strong>Information Loss</strong></td>
<td>Important details silently dropped</td>
<td>User's constraints, preferences, prior decisions — gone</td>
</tr>
<tr>
<td><strong>Context Overflow</strong></td>
<td>Hard crash, no response</td>
<td><code>Error: This model's maximum context length is 4097 tokens</code></td>
</tr>
</tbody></table>
<h2><strong>Context Window vs Human Memory</strong></h2>
<p>Humans don't have this problem (at least not this badly). Here's why:</p>
<img src="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/5c6490f3-2247-4101-b3ae-81fad3fb8a77.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>The Fundamental Challenge</strong></h2>
<p>The fundamental challenge of context engineering is: <strong>How do we give LLMs something resembling human memory management—selective, prioritized, and graceful—within a rigid token budget?</strong></p>
<p>This is the question the rest of this series answers. We'll explore:</p>
<ol>
<li><p><strong>Naive solutions</strong> (sliding windows) and why they fail</p>
</li>
<li><p><strong>Smarter strategies</strong> (token-based management)</p>
</li>
<li><p><strong>Compression techniques</strong> (summarization)</p>
</li>
<li><p><strong>Modern approaches</strong> (RAG, tool use, memory systems)</p>
</li>
<li><p><strong>The current frontier</strong> (long context models and their limitations)</p>
</li>
</ol>
<h2><strong>Why This Matters for Your Business</strong></h2>
<p>Poor context management isn't just an annoyance—it has real business impact:</p>
<ul>
<li><p><strong>Lost Productivity</strong>: Teams spend time re-explaining context</p>
</li>
<li><p><strong>Wrong Decisions</strong>: AI makes contradictory recommendations</p>
</li>
<li><p><strong>Poor User Experience</strong>: Chatbots feel forgetful and unintelligent</p>
</li>
<li><p><strong>Increased Costs</strong>: Inefficient token usage leads to higher API bills</p>
</li>
<li><p><strong>Security Risks</strong>: Important constraints and requirements get lost</p>
</li>
</ul>
<h2><strong>What's Next?</strong></h2>
<p>In Part 2, we'll dive into the most common solution—the sliding window—and explore why this seemingly reasonable approach is actually a trap that causes more problems than it solves.</p>
<hr />
<h2><strong>Key Takeaways</strong></h2>
<ol>
<li><p><strong>Context windows are the fundamental constraint</strong> of working with LLMs</p>
</li>
<li><p><strong>Every other challenge flows from this limitation</strong></p>
</li>
<li><p><strong>Simple solutions fail in production systems</strong></p>
</li>
<li><p><strong>Context engineering is a critical production discipline</strong></p>
</li>
</ol>
<hr />
<p><em>This is Part 1 of a 6-part series on Context Engineering.</em> <em><strong>Read Part 2: The Sliding Window Trap</strong></em> <em>to learn about common pitfalls and their solutions.</em></p>
<p><strong>References:</strong></p>
<ul>
<li><p>Karpathy, A. (2025). "Context Engineering" — X/Twitter post</p>
</li>
<li><p>Liu, N., et al. (2024). "Lost in the Middle: How Language Models Use Long Contexts" — Transactions of the ACL</p>
</li>
</ul>
<hr />
<p><em>Found this helpful?</em> <a href="https://linkedin.com/in/akashpanchal"><em>Follow me on LinkedIn</em></a> <em>for more insights on AI engineering and subscribe to get notified about the next parts in this series.</em></p>
]]></content:encoded></item><item><title><![CDATA[I Solved Claude Code's Biggest Flaw: Context Compaction Amnesia]]></title><description><![CDATA[How a frustrating afternoon led to a breakthrough that changed how I use AI forever
The Breaking Point
It was 3 AM on a Tuesday. I'd been working with Claude Code for six hours straight, architecting ]]></description><link>https://blog.akashpanchal.com/crux-for-claude-code</link><guid isPermaLink="true">https://blog.akashpanchal.com/crux-for-claude-code</guid><category><![CDATA[claude-code]]></category><category><![CDATA[plugins]]></category><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Wed, 25 Feb 2026 17:29:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/618f409be61d1c383da7b550/7eab1865-5664-44d7-9518-ec4d77dbe53e.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>How a frustrating afternoon led to a breakthrough that changed how I use AI forever</em></p>
<h2><strong>The Breaking Point</strong></h2>
<p>It was 3 AM on a Tuesday. I'd been working with Claude Code for six hours straight, architecting a complex microservices system. The conversation was long, detailed, and productive.</p>
<p>Until it wasn't.</p>
<p><strong>Me</strong>: "Let's use PostgreSQL for the user database since our team knows SQL well and we can't hire MongoDB experts."</p>
<p><strong>Claude (6 hours later)</strong>: "You know, for this user data use case, MongoDB might actually be better with its flexible schema..."</p>
<p>I stared at the screen, dumbfounded.</p>
<p>Six hours. Dozens of architectural decisions. And Claude had completely forgotten the most important constraint: <strong>our team only knows SQL</strong>.</p>
<p>This wasn't just annoying. It was actively harmful. All that context, all those decisions, and the AI was suggesting something that would require us to hire three new developers.</p>
<p>That's when I realized: Claude Code has a fundamental flaw.</p>
<h2><strong>The Hidden Flaw: Context Compaction Amnesia</strong></h2>
<p>Claude Code is incredible, but it has a dirty secret. As conversations get long, it performs "context compaction" - summarizing earlier parts to make room for new information.</p>
<p>The problem? <strong>Compaction strips away the WHY behind decisions.</strong></p>
<p>It remembers the <strong>WHAT</strong> ("Use PostgreSQL") but forgets the <strong>CONSTRAINT</strong> ("team only knows SQL") and <strong>RATIONALE</strong> ("can't hire MongoDB experts").</p>
<p>Without the <strong>WHY</strong>, decisions become mere suggestions that can be overridden.</p>
<p>Here's how it plays out in real projects:</p>
<pre><code class="language-plaintext">Turn 1: "Use React not Vue — our company standardized on React last year"
Turn 15: Claude suggests Vue for a new component
Turn 30: Claude recommends a Vue-specific library
Result: Architectural inconsistency, wasted time, confused team
</code></pre>
<p><strong>This isn't just a technical issue. It's a project management nightmare.</strong></p>
<h2><strong>The Lightbulb Moment</strong></h2>
<p>The next day, I was explaining this frustration to a colleague over coffee.</p>
<p>"The problem," I said, "is that Claude remembers WHAT we decided, but not WHY we decided it."</p>
<p>He looked at me and said: "What if you could weld the WHY to the WHAT so tightly that compaction could never separate them?"</p>
<p>That was it. That was the solution.</p>
<p>What if instead of storing flat facts, we stored <strong>causal triples</strong>?</p>
<pre><code class="language-plaintext">CONSTRAINT ⛔ Company standardized on React last year
     ↓
RATIONALE 💡 Team consistency and reduced training costs
     ↓
DECISION ▸ Use React not Vue
</code></pre>
<p>These three would be welded together. Compaction couldn't drop one without dropping all. Claude would always see the WHY.</p>
<h2><strong>Building Crux: The Causal Memory Graph</strong></h2>
<p>I spent the next two weeks building <strong>Crux</strong> - a Claude Code plugin that implements this causal memory system.</p>
<h3><strong>The Architecture</strong></h3>
<p>Crux hooks directly into Claude Code's lifecycle:</p>
<img src="https://github.com/akashp1712/claude-crux/raw/main/claude-crux-sequence.png" alt="Crux Architecture Diagram" style="display:block;margin:0 auto" />

<ol>
<li><p><strong>SessionStart</strong>: Sets up the causal graph</p>
</li>
<li><p><strong>UserPromptSubmit</strong>: Extracts decisions from your requests</p>
</li>
<li><p><strong>PreCompact</strong>: The magic layer - injects co-inclusion rules</p>
</li>
<li><p><strong>Stop</strong>: Extracts decisions from Claude's responses</p>
</li>
<li><p><strong>SessionEnd</strong>: Cleans up for next session</p>
</li>
</ol>
<p>The PreCompact hook is where the magic happens. Right before Claude compacts context, Crux injects strict instructions:</p>
<blockquote>
<p>"You MUST NOT separate any DECISION from its RATIONALE and CONSTRAINT. These are co-included and must travel together."</p>
</blockquote>
<h3><strong>The Causal Triple System</strong></h3>
<p>Every decision in Crux becomes a causal triple:</p>
<pre><code class="language-plaintext">{
  "decision": {
    "id": "decision_123",
    "content": "Use PostgreSQL for user database",
    "dependsOn": ["rationale_123", "constraint_123"],
    "source": "user",
    "turn": 15
  },
  "rationale": {
    "id": "rationale_123", 
    "content": "Team knows SQL well and can't hire MongoDB experts",
    "dependsOn": ["constraint_123"],
    "source": "user",
    "turn": 15
  },
  "constraint": {
    "id": "constraint_123",
    "content": "Team only knows SQL databases",
    "dependsOn": [],
    "source": "user", 
    "turn": 15
  }
}
</code></pre>
<p>These aren't just stored - they're actively injected back into Claude's context before every compaction.</p>
<h2><strong>The Transformation: Before vs After</strong></h2>
<h3><strong>Before Crux: The Amnesia Problem</strong></h3>
<pre><code class="language-plaintext">You: "Use PostgreSQL because our team knows SQL well and we can't hire MongoDB experts"
Claude: "Got it, PostgreSQL for user database."

...40 turns of development later...

Claude: "For the user analytics feature, have you considered MongoDB? Its flexible schema would be great for user behavior tracking."
You: "But we can't hire MongoDB experts..."
Claude: "Right, but you could use a managed service or hire a consultant..."
</code></pre>
<p><strong>Result</strong>: Wasted time, architectural inconsistency, frustrated team.</p>
<h3><strong>After Crux: Co-Inclusion Guarantee</strong></h3>
<pre><code class="language-plaintext">You: "Use PostgreSQL because our team knows SQL well and we can't hire MongoDB experts"
Crux: [Stores causal triple automatically]

...40 turns of development later...

Claude: "For the user analytics feature, PostgreSQL remains the best choice given your constraint that team only knows SQL and you can't hire MongoDB experts. Here's how we can optimize the queries..."
</code></pre>
<p><strong>Result</strong>: Consistent architecture, respected constraints, productive development.</p>
<h2><strong>Real-World Impact: A Case Study</strong></h2>
<p>Let me show you how this transformed a real project.</p>
<h3><strong>The Project: E-commerce Platform</strong></h3>
<p>Our team was building a complex e-commerce platform. Here's how the conversation evolved:</p>
<h4><strong>Week 1: Foundation Decisions</strong></h4>
<pre><code class="language-plaintext">You: "We'll use Next.js for frontend because our team has React experience and we need good SEO."
Crux: [Stores: CONSTRAINT=team React experience, RATIONALE=SEO needs, DECISION=Next.js]

You: "Payment processing must use Stripe because our legal team approved it and we can't use PayPal due to compliance issues."
Crux: [Stores: CONSTRAINT=compliance issues, RATIONALE=legal approval, DECISION=Stripe]
</code></pre>
<h4><strong>Week 3: Architecture Challenge</strong></h4>
<pre><code class="language-plaintext">Claude: "For the product recommendation engine, we could use TensorFlow.js for client-side processing..."
You: "But our team only knows Python for ML..."
Claude: "Right, given your constraint that team only knows Python, let's use a Python-based recommendation service with API endpoints."
</code></pre>
<h4><strong>Week 6: Vendor Pressure</strong></h4>
<pre><code class="language-plaintext">Stakeholder: "PayPal is offering us better rates. Can we switch?"
Claude: "Based on your constraint that PayPal has compliance issues and legal team only approved Stripe, switching is not viable without legal review. However, we can negotiate better rates with Stripe or explore hybrid solutions."
</code></pre>
<p><strong>The difference was night and day.</strong></p>
<p>Without Crux: Claude might have suggested switching to PayPal, causing legal issues and wasted effort.</p>
<p>With Crux: Claude actively protected our constraints while still helping optimize within those boundaries.</p>
<h2><strong>The Technical Magic: How It Actually Works</strong></h2>
<h3><strong>Extraction Engine</strong></h3>
<p>Crux uses a sophisticated extraction engine that understands natural language:</p>
<pre><code class="language-plaintext">// Simplified example of extraction logic
function extractDecision(text) {
  const constraints = findConstraints(text); // "team only knows SQL", "legal approved"
  const decisions = findDecisions(text);    // "use PostgreSQL", "use Stripe"  
  const rationales = findRationales(text);  // "SEO needs", "compliance issues"
  
  return createCausalTriples(constraints, decisions, rationales);
}
</code></pre>
<h3><strong>The Co-Inclusion Injection</strong></h3>
<p>Before every context compaction, Crux injects:</p>
<pre><code class="language-plaintext">--- CRUX: Active Architectural Decisions ---

DECISION: Use Next.js for frontend
RATIONALE: Team has React experience and needs good SEO  
CONSTRAINT: ⛔ Team only knows React-based frameworks

DECISION: Use Stripe for payment processing
RATIONALE: Legal team approval and compliance requirements
CONSTRAINT: ⛔ Cannot use PayPal due to compliance issues

⚠️ Respect these decisions unless explicitly asked to revisit them. Do not separate decisions from their constraints and rationales.
</code></pre>
<p>This injection happens automatically, invisibly, every single time.</p>
<h3><strong>Smart Deduplication</strong></h3>
<p>Crux also prevents decision pollution:</p>
<pre><code class="language-plaintext">// Same decision, different wording
"We'll use PostgreSQL" vs "Let's go with Postgres" vs "PostgreSQL is our choice"

// Crux recognizes these as semantically identical
// and updates existing decision instead of creating duplicates
</code></pre>
<h2><strong>Zero Configuration, Maximum Impact</strong></h2>
<p>The best part about Crux? It works completely transparently.</p>
<p>No special commands. No manual configuration. No "remember this decision" prompts.</p>
<p>It just understands from normal conversation and protects your architectural choices automatically.</p>
<h3><strong>Installation</strong></h3>
<pre><code class="language-plaintext"># Add the marketplace
/plugin marketplace add akashp1712/claude-marketplace

# Install Crux
/plugin install crux@akashp1712
</code></pre>
<p>That's it. Start coding, and Crux will automatically protect your decisions.</p>
<h2><strong>This Changes Everything for Long-Term Projects</strong></h2>
<p>For anyone using Claude Code for serious development work, this is transformative:</p>
<h3><strong>Team Collaboration</strong></h3>
<p>Everyone on the team sees the same constraints and reasoning. No more "I thought we decided to use X" conversations.</p>
<h3><strong>Client Work</strong></h3>
<p>Maintain consistency across long engagements. Decisions made in month 1 are still respected in month 6.</p>
<h3><strong>Complex Architecture</strong></h3>
<p>Multiple interconnected decisions stay coherent. The payment system choice respects the legal constraints. The frontend choice respects the team skills.</p>
<h3><strong>Peace of Mind</strong></h3>
<p>No more waking up at 3 AM wondering if Claude is going to suggest something contradictory tomorrow.</p>
<h2><strong>The Future of AI Memory</strong></h2>
<p>Crux is just the beginning. As AI becomes more integrated into our development workflows, we need better memory systems:</p>
<h3><strong>Roadmap v0.2: Conflict Detection</strong></h3>
<p>When you say "Use PostgreSQL" and later "Use MongoDB", Crux will flag: "This contradicts your earlier decision about team skills."</p>
<h3><strong>Roadmap v0.3: Team Graphs</strong></h3>
<p>Git-committable decision graphs for team sharing. Everyone sees the same architectural constraints.</p>
<h3><strong>Roadmap v1.0: The Proxy</strong></h3>
<p>Protocol-level interception for exact control over context compaction.</p>
<h2><strong>Try It Yourself</strong></h2>
<p>I've open-sourced Crux under MIT license. It's production-ready, zero-dependency, and works with Claude Code immediately.</p>
<p><strong>Get started</strong>: <a href="https://github.com/akashp1712/claude-crux">https://github.com/akashp1712/claude-crux</a></p>
<p><strong>Full marketplace</strong>: <a href="https://github.com/akashp1712/claude-marketplace">https://github.com/akashp1712/claude-marketplace</a></p>
<h2><strong>Join the Movement</strong></h2>
<p>Context compaction amnesia is a problem every serious Claude Code user faces. We don't have to accept it.</p>
<p>If you've ever been frustrated by your AI forgetting important constraints, try Crux. If you're building AI tools, think about causal memory, not just flat facts.</p>
<p><strong>Share your experiences</strong>: Let me know how Crux changes your workflow.</p>
<p><strong>Contribute</strong>: The project is open for contributions - new extraction patterns, better UI, conflict detection.</p>
<hr />
<p><em><strong>Because your AI should remember WHY, not just WHAT.</strong></em></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[From O(n) to O(1): How We Fixed Our API Key Validation Performance
]]></title><description><![CDATA[When you're building a SaaS API, every millisecond counts. We recently discovered that our API key validation was a ticking time bomb—and fixed it before it exploded.
The Problem
Our API uses bearer t]]></description><link>https://blog.akashpanchal.com/from-o-n-to-o-1-how-we-fixed-our-api-key-validation-performance</link><guid isPermaLink="true">https://blog.akashpanchal.com/from-o-n-to-o-1-how-we-fixed-our-api-key-validation-performance</guid><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Wed, 25 Feb 2026 16:49:14 GMT</pubDate><content:encoded><![CDATA[<p>When you're building a SaaS API, every millisecond counts. We recently discovered that our API key validation was a ticking time bomb—and fixed it before it exploded.</p>
<h2>The Problem</h2>
<p>Our API uses bearer tokens for authentication. Every request includes an API key:</p>
<pre><code class="language-plaintext">Authorization: Bearer paymint_production_apikey_a1b2c3...
</code></pre>
<p>For security, we encrypt API keys before storing them in the database. The encryption uses AES-256-GCM, which means we can't simply query for a matching key—we have to decrypt to compare.</p>
<p>Here's what our original validation looked like:</p>
<pre><code class="language-typescript">export async function validateApiKey(apiKey: string): Promise&lt;ApiKeyValidation&gt; {
  // Get ALL active API keys
  const apiKeyRecords = await database.apiKey.findMany({
    where: { status: 'active' },
  });

  // Decrypt each one and compare
  for (const record of apiKeyRecords) {
    const decryptedKey = decrypt(record.encryptedKey);
    if (decryptedKey === apiKey) {
      return { isValid: true, organizationId: record.organizationId };
    }
  }

  return { isValid: false };
}
</code></pre>
<p>This works. But there's a problem hiding in plain sight.</p>
<h2>The Math</h2>
<p>Let's say decryption takes 5ms per key (it's actually faster, but let's be conservative).</p>
<table>
<thead>
<tr>
<th>Active API Keys</th>
<th>Time to Validate</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>50ms</td>
</tr>
<tr>
<td>100</td>
<td>500ms</td>
</tr>
<tr>
<td>1,000</td>
<td>5 seconds</td>
</tr>
<tr>
<td>10,000</td>
<td>50 seconds</td>
</tr>
</tbody></table>
<p>Every API request—fetching products, listing subscriptions, canceling a subscription—would need to wait for this validation. At 1,000 keys, we'd be adding 5 seconds of latency to every single request.</p>
<p>This is O(n) complexity. As our customer base grows, performance degrades linearly.</p>
<h2>Why Not Just Query the Encrypted Key?</h2>
<p>You might think: "Just store the encrypted key and query for it directly."</p>
<pre><code class="language-sql">SELECT * FROM api_keys WHERE encrypted_key = ?
</code></pre>
<p>This doesn't work because encryption is non-deterministic. AES-GCM uses a random initialization vector (IV) for each encryption, so encrypting the same plaintext twice produces different ciphertexts.</p>
<pre><code class="language-typescript">encrypt('my_api_key') // =&gt; 'abc123...'
encrypt('my_api_key') // =&gt; 'xyz789...' (different!)
</code></pre>
<p>This is actually a security feature—it prevents attackers from identifying duplicate keys by comparing ciphertexts.</p>
<h2>The Solution: Hash-Based Lookup</h2>
<p>The fix is elegant: store a hash of the API key alongside the encrypted version.</p>
<p>Unlike encryption, hashing is deterministic—the same input always produces the same output. And unlike encryption, we don't need to reverse it. We just need to find a match.</p>
<pre><code class="language-typescript">import crypto from 'node:crypto';

function hashApiKey(apiKey: string): string {
  return crypto.createHash('sha256').update(apiKey).digest('hex');
}
</code></pre>
<h3>New Schema</h3>
<pre><code class="language-plaintext">model ApiKey {
  id           String  @id @default(uuid())
  keyHash      String? @unique  // SHA-256 hash for O(1) lookup
  encryptedKey String           // AES-256-GCM encrypted key
  // ... other fields
}
</code></pre>
<h3>New Validation</h3>
<pre><code class="language-typescript">export async function validateApiKey(apiKey: string): Promise&lt;ApiKeyValidation&gt; {
  const keyHash = hashApiKey(apiKey);

  // O(1) lookup using unique index
  const record = await database.apiKey.findFirst({
    where: {
      keyHash: keyHash,
      status: 'active',
    },
  });

  if (!record) {
    return { isValid: false };
  }

  // Defense in depth: verify by decrypting
  const decryptedKey = decrypt(record.encryptedKey);
  if (decryptedKey !== apiKey) {
    return { isValid: false };
  }

  return {
    isValid: true,
    organizationId: record.organizationId,
  };
}
</code></pre>
<p>Now validation is O(1)—constant time regardless of how many API keys exist.</p>
<table>
<thead>
<tr>
<th>Active API Keys</th>
<th>Old Time</th>
<th>New Time</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>50ms</td>
<td>~5ms</td>
</tr>
<tr>
<td>100</td>
<td>500ms</td>
<td>~5ms</td>
</tr>
<tr>
<td>1,000</td>
<td>5s</td>
<td>~5ms</td>
</tr>
<tr>
<td>10,000</td>
<td>50s</td>
<td>~5ms</td>
</tr>
</tbody></table>
<h2>Why Keep the Encrypted Key?</h2>
<p>You might wonder: if we have the hash, why bother with encryption?</p>
<p>Two reasons:</p>
<ol>
<li><p><strong>Defense in depth</strong>: After finding a hash match, we decrypt and verify. This protects against hash collisions (astronomically unlikely with SHA-256, but defense in depth is good practice).</p>
</li>
<li><p><strong>Key rotation</strong>: If we ever need to re-encrypt keys (e.g., rotating the encryption key), we need the actual key value. The hash alone isn't reversible.</p>
</li>
</ol>
<h2>Migration Strategy</h2>
<p>We couldn't just flip a switch—existing API keys didn't have hashes. Here's how we handled the migration:</p>
<h3>1. Make the hash column nullable</h3>
<pre><code class="language-sql">ALTER TABLE "ApiKey" ADD COLUMN "keyHash" TEXT;
CREATE UNIQUE INDEX "ApiKey_keyHash_key" ON "ApiKey"("keyHash");
</code></pre>
<h3>2. Add hash on new key creation</h3>
<pre><code class="language-typescript">function generateApiKey(environment: 'sandbox' | 'production') {
  const key = `paymint_\({environment}_apikey_\){crypto.randomBytes(32).toString('hex')}`;
  const keyHash = hashApiKey(key);
  const encryptedKey = encrypt(key);

  return { key, keyHash, encryptedKey };
}
</code></pre>
<h3>3. Fallback for legacy keys</h3>
<pre><code class="language-typescript">export async function validateApiKey(apiKey: string): Promise&lt;ApiKeyValidation&gt; {
  const keyHash = hashApiKey(apiKey);

  // Try O(1) lookup first
  const record = await database.apiKey.findFirst({
    where: { keyHash, status: 'active' },
  });

  if (record) {
    // Verify and return
    const decryptedKey = decrypt(record.encryptedKey);
    if (decryptedKey === apiKey) {
      return { isValid: true, organizationId: record.organizationId };
    }
    return { isValid: false };
  }

  // Fallback: check legacy keys without hash
  return validateApiKeyLegacy(apiKey);
}

async function validateApiKeyLegacy(apiKey: string): Promise&lt;ApiKeyValidation&gt; {
  const records = await database.apiKey.findMany({
    where: { status: 'active', keyHash: null },
  });

  for (const record of records) {
    const decryptedKey = decrypt(record.encryptedKey);
    if (decryptedKey === apiKey) {
      // Auto-migrate: add hash for future O(1) lookups
      const keyHash = hashApiKey(apiKey);
      await database.apiKey.update({
        where: { id: record.id },
        data: { keyHash },
      });

      return { isValid: true, organizationId: record.organizationId };
    }
  }

  return { isValid: false };
}
</code></pre>
<p>The legacy fallback auto-migrates keys on first use. Over time, all keys get hashes, and the fallback path becomes unused.</p>
<h2>Security Considerations</h2>
<h3>Is storing a hash safe?</h3>
<p>Yes. SHA-256 is a one-way function—you can't reverse it to get the original key. An attacker with database access would see:</p>
<pre><code class="language-plaintext">keyHash: "a1b2c3d4e5f6..."
encryptedKey: "encrypted_blob..."
</code></pre>
<p>They can't use the hash to authenticate (the API expects the original key), and they can't decrypt without the encryption key (stored separately in environment variables).</p>
<h3>What about rainbow tables?</h3>
<p>API keys are high-entropy random strings (72+ characters of hex). Rainbow tables are only practical for low-entropy inputs like passwords. The search space for our keys is 16^72, which is... large.</p>
<h3>What about timing attacks?</h3>
<p>We use constant-time comparison for the final verification:</p>
<pre><code class="language-typescript">import crypto from 'node:crypto';

function secureCompare(a: string, b: string): boolean {
  return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
</code></pre>
<h2>Results</h2>
<p>After deploying this change:</p>
<ul>
<li><p><strong>P50 latency</strong>: Reduced by 40ms</p>
</li>
<li><p><strong>P99 latency</strong>: Reduced by 200ms</p>
</li>
<li><p><strong>Database load</strong>: Significantly reduced (no more full table scans)</p>
</li>
</ul>
<p>More importantly, we removed a scaling bottleneck. Our API can now handle 10x more customers without degrading performance.</p>
<h2>Key Takeaways</h2>
<ol>
<li><p><strong>Audit your auth paths</strong>: Authentication runs on every request. Even small inefficiencies compound.</p>
</li>
<li><p><strong>Encryption ≠ Hashing</strong>: Encryption is reversible and non-deterministic. Hashing is one-way and deterministic. Use the right tool.</p>
</li>
<li><p><strong>Plan for scale</strong>: Code that works at 10 customers might break at 10,000. Think about complexity classes.</p>
</li>
<li><p><strong>Migrate gracefully</strong>: Use fallbacks and auto-migration to avoid big-bang deployments.</p>
</li>
<li><p><strong>Defense in depth</strong>: Even with hash-based lookup, we still verify by decrypting. Belt and suspenders.</p>
</li>
</ol>
<hr />
<p><em>Building a SaaS? Check out</em> <a href="https://paymint.dev"><em>Paymint</em></a><em>—we handle subscription billing so you can focus on your product.</em></p>
<hr />
<p><em>Tags: performance, security, api-design, database, optimization, saas, authentication</em></p>
]]></content:encoded></item><item><title><![CDATA[From O(n) to O(1): How We Fixed Our API Key Validation Performance]]></title><description><![CDATA[When you're building a SaaS API, every millisecond counts. We recently discovered that our API key validation was a ticking time bomb—and fixed it before it exploded.
The Problem
Our API uses bearer tokens for authentication. Every request includes a...]]></description><link>https://blog.akashpanchal.com/from-on-to-o1-how-we-fixed-our-api-key-validation-performance</link><guid isPermaLink="true">https://blog.akashpanchal.com/from-on-to-o1-how-we-fixed-our-api-key-validation-performance</guid><category><![CDATA[performance]]></category><category><![CDATA[Security]]></category><category><![CDATA[API Design]]></category><category><![CDATA[Databases]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[authentication]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Sat, 24 Jan 2026 02:13:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769220695562/eb2e4394-f069-4f0a-a17c-073f65886e86.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you're building a SaaS API, every millisecond counts. We recently discovered that our API key validation was a ticking time bomb—and fixed it before it exploded.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Our API uses bearer tokens for authentication. Every request includes an API key:</p>
<pre><code class="lang-plaintext">Authorization: Bearer paymint_production_apikey_a1b2c3...
</code></pre>
<p>For security, we encrypt API keys before storing them in the database. The encryption uses AES-256-GCM, which means we can't simply query for a matching key—we have to decrypt each one to compare.</p>
<p>Here's what our original validation looked like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validateApiKey</span>(<span class="hljs-params">apiKey: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ValidationResult</span>&gt; </span>{
  <span class="hljs-comment">// Get ALL active API keys</span>
  <span class="hljs-keyword">const</span> apiKeyRecords = <span class="hljs-keyword">await</span> database.apiKey.findMany({
    where: { status: <span class="hljs-string">'active'</span> },
  });

  <span class="hljs-comment">// Decrypt each one and compare</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> record <span class="hljs-keyword">of</span> apiKeyRecords) {
    <span class="hljs-keyword">const</span> decryptedKey = decrypt(record.encryptedKey);
    <span class="hljs-keyword">if</span> (decryptedKey === apiKey) {
      <span class="hljs-keyword">return</span> { 
        isValid: <span class="hljs-literal">true</span>, 
        organizationId: record.organizationId 
      };
    }
  }

  <span class="hljs-keyword">return</span> { isValid: <span class="hljs-literal">false</span> };
}
</code></pre>
<p>This works. But there's a problem hiding in plain sight.</p>
<h3 id="heading-the-request-flow-problem">The Request Flow Problem</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769219988024/d8d66c57-4a12-44c6-a144-4503f2ba14d7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-math">The Math</h2>
<p>Let's say decryption takes 5ms per key (it's actually faster, but let's be conservative).</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Active API Keys</strong></td><td><strong>Time to Validate</strong></td><td><strong>Performance Impact</strong></td></tr>
</thead>
<tbody>
<tr>
<td>10</td><td>50ms</td><td>Noticeable</td></tr>
<tr>
<td>100</td><td>500ms</td><td>Unacceptable</td></tr>
<tr>
<td>1,000</td><td>5 seconds</td><td>Business-breaking</td></tr>
<tr>
<td>10,000</td><td>50 seconds</td><td>Timeout territory</td></tr>
</tbody>
</table>
</div><p>Every API request—fetching products, listing subscriptions, canceling a subscription—would need to wait for this validation. At 1,000 keys, we'd be adding 5 seconds of latency to <strong>every single request</strong>.</p>
<p>This is <strong>O(n) complexity</strong>. As our customer base grows, performance degrades linearly. We needed O(1).</p>
<h2 id="heading-why-not-just-query-the-encrypted-key">Why Not Just Query the Encrypted Key?</h2>
<p>You might think: "<strong>Just store the encrypted key and query for it directly.</strong>"</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> api_keys <span class="hljs-keyword">WHERE</span> encrypted_key = ?
</code></pre>
<p>This doesn't work because encryption is <strong>non-deterministic</strong>. AES-GCM uses a random initialization vector (IV) for each encryption, so encrypting the same plaintext twice produces different ciphertexts.</p>
<pre><code class="lang-typescript">encrypt(<span class="hljs-string">'my_api_key'</span>) <span class="hljs-comment">// =&gt; 'abc123...'</span>
encrypt(<span class="hljs-string">'my_api_key'</span>) <span class="hljs-comment">// =&gt; 'xyz789...' (different!)</span>
</code></pre>
<p>This is actually a <strong>security feature</strong>—it prevents attackers from identifying duplicate keys by comparing ciphertexts. But it means we can't use encrypted values for database lookups.</p>
<h2 id="heading-the-solution-hash-based-lookup">The Solution: Hash-Based Lookup</h2>
<p>The fix is elegant: store a <strong>hash</strong> of the API key alongside the encrypted version.</p>
<p>Unlike encryption, hashing is <strong>deterministic</strong>—the same input always produces the same output. And unlike encryption, we don't need to reverse it. We just need to find a match.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">'node:crypto'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashApiKey</span>(<span class="hljs-params">apiKey: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">string</span> </span>{
  <span class="hljs-keyword">return</span> crypto.createHash(<span class="hljs-string">'sha256'</span>).update(apiKey).digest(<span class="hljs-string">'hex'</span>);
}
</code></pre>
<h3 id="heading-comparing-encryption-vs-hashing">Comparing Encryption vs Hashing</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769220166983/513cac5e-dc37-49bf-abc8-6b3d7f81b4ae.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769220225796/c1f8de1f-d9ed-4132-a8a4-c73f152b64e0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-new-schema">New Schema</h3>
<p>prisma</p>
<pre><code class="lang-typescript">model ApiKey {
  id           <span class="hljs-built_in">String</span>   <span class="hljs-meta">@id</span> <span class="hljs-meta">@default</span>(uuid())
  keyHash      <span class="hljs-built_in">String</span>?  <span class="hljs-meta">@unique</span> <span class="hljs-comment">// SHA-256 hash for O(1) lookup</span>
  encryptedKey <span class="hljs-built_in">String</span>            <span class="hljs-comment">// AES-256-GCM encrypted key</span>
  status       <span class="hljs-built_in">String</span>
  organizationId <span class="hljs-built_in">String</span>
  <span class="hljs-comment">// ... other fields</span>
}
</code></pre>
<h3 id="heading-new-validation-logic">New Validation Logic</h3>
<p>typescript</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validateApiKey</span>(<span class="hljs-params">apiKey: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">ValidationResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> keyHash = hashApiKey(apiKey);

  <span class="hljs-comment">// O(1) lookup using unique index</span>
  <span class="hljs-keyword">const</span> record = <span class="hljs-keyword">await</span> database.apiKey.findFirst({
    where: { 
      keyHash: keyHash,
      status: <span class="hljs-string">'active'</span>,
    },
  });

  <span class="hljs-keyword">if</span> (!record) {
    <span class="hljs-keyword">return</span> { isValid: <span class="hljs-literal">false</span> };
  }

  <span class="hljs-comment">// Defense in depth: verify by decrypting</span>
  <span class="hljs-keyword">const</span> decryptedKey = decrypt(record.encryptedKey);
  <span class="hljs-keyword">if</span> (decryptedKey !== apiKey) {
    <span class="hljs-keyword">return</span> { isValid: <span class="hljs-literal">false</span> };
  }

  <span class="hljs-keyword">return</span> { 
    isValid: <span class="hljs-literal">true</span>, 
    organizationId: record.organizationId,
  };
}
</code></pre>
<p>Now validation is <strong>O(1)</strong>—constant time regardless of how many API keys exist.</p>
<h3 id="heading-the-new-request-flow-optimized-api-key-validation-flow">The New Request Flow: Optimized API Key Validation Flow</h3>
<h3 id="heading-performance-comparison">Performance Comparison</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Active API Keys</strong></td><td><strong>Old Time (O(n))</strong></td><td><strong>New Time (O(1))</strong></td><td><strong>Improvement</strong></td></tr>
</thead>
<tbody>
<tr>
<td>10</td><td>50ms</td><td>~5ms</td><td><strong>10x faster</strong></td></tr>
<tr>
<td>100</td><td>500ms</td><td>~5ms</td><td><strong>100x faster</strong></td></tr>
<tr>
<td>1,000</td><td>5s</td><td>~5ms</td><td><strong>1000x faster</strong></td></tr>
<tr>
<td>10,000</td><td>50s</td><td>~5ms</td><td><strong>10000x faster</strong></td></tr>
</tbody>
</table>
</div><h2 id="heading-why-keep-the-encrypted-key">Why Keep the Encrypted Key?</h2>
<p>You might wonder: <strong>if we have the hash, why bother with encryption?</strong></p>
<p><strong>Two critical reasons:</strong></p>
<h3 id="heading-1-defense-in-depth">1. Defense in Depth</h3>
<p>After finding a hash match, we decrypt and verify. This protects against hash collisions (astronomically unlikely with SHA-256, but defense in depth is good practice). If an attacker somehow found a collision, they still wouldn't get through.</p>
<h3 id="heading-2-key-rotation-amp-recovery">2. Key Rotation &amp; Recovery</h3>
<p>If we ever need to:</p>
<ul>
<li><p>Re-encrypt keys (e.g., rotating the encryption key)</p>
</li>
<li><p>Migrate to a different encryption algorithm</p>
</li>
<li><p>Support key export features</p>
</li>
</ul>
<p>We need the actual key value. The hash alone isn't reversible, so we'd lose the original keys forever.</p>
<h3 id="heading-what-about-rainbow-tables">What About Rainbow Tables?</h3>
<p>API keys are <strong>high-entropy random strings</strong> (72+ characters of hex from <code>crypto.randomBytes(32)</code>). Rainbow tables are only practical for low-entropy inputs like passwords or common phrases.</p>
<p>The search space for our keys is <strong>16^72</strong> ≈ <strong>10^86</strong> possible values. For comparison:</p>
<ul>
<li><p>Number of atoms in the universe: ~10^80</p>
</li>
<li><p>SHA-256 output space: 2^256 ≈ 10^77</p>
</li>
</ul>
<p>Rainbow tables aren't feasible here.</p>
<h3 id="heading-what-about-timing-attacks">What About Timing Attacks?</h3>
<p>We use constant-time comparison for the final verification to prevent timing attacks:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">'node:crypto'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">secureCompare</span>(<span class="hljs-params">a: <span class="hljs-built_in">string</span>, b: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">boolean</span> </span>{
  <span class="hljs-keyword">if</span> (a.length !== b.length) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
  }
  <span class="hljs-keyword">return</span> crypto.timingSafeEqual(
    Buffer.from(a), 
    Buffer.from(b)
  );
}

<span class="hljs-comment">// Use in validation</span>
<span class="hljs-keyword">if</span> (secureCompare(decryptedKey, apiKey)) {
  <span class="hljs-keyword">return</span> { isValid: <span class="hljs-literal">true</span>, organizationId: record.organizationId };
}
</code></pre>
<p>This prevents attackers from using response time variations to guess key characters.</p>
<h3 id="heading-security-layers-summary">Security Layers Summary</h3>
<p>Defense-in-Depth Security Layers</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769220493324/6ac792f5-cc49-427a-b3c9-2134b1d45eb2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-results">Results</h2>
<p>After deploying this change, we saw immediate and dramatic improvements:</p>
<ul>
<li><p><strong>P50 latency</strong>: Reduced by 40ms (from 45ms to 5ms)</p>
</li>
<li><p><strong>P99 latency</strong>: Reduced by 200ms (from 205ms to 5ms)</p>
</li>
<li><p><strong>Database load</strong>: Significantly reduced (no more full table scans)</p>
</li>
<li><p><strong>Scalability</strong>: Removed the O(n) bottleneck entirely</p>
</li>
</ul>
<p>More importantly, we <strong>removed a scaling time bomb</strong>. Our API can now handle 10x, 100x, or even 10,000x more customers without degrading authentication performance.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<h3 id="heading-1-audit-your-authentication-paths">1. <strong>Audit Your Authentication Paths</strong></h3>
<p>Authentication runs on <strong>every single request</strong>. Even small inefficiencies compound dramatically. A 50ms slowdown might seem negligible, but multiply that by millions of requests and you've got a serious problem.</p>
<h3 id="heading-2-encryption-hashing">2. <strong>Encryption ≠ Hashing</strong></h3>
<ul>
<li><p><strong>Encryption</strong>: Reversible, non-deterministic, requires a secret key</p>
</li>
<li><p><strong>Hashing</strong>: One-way, deterministic, no secret needed</p>
</li>
</ul>
<p>Use encryption when you need to retrieve the original value. Use hashing when you only need to verify a match. For API keys, we need both—hash for lookup, encryption for storage.</p>
<h3 id="heading-3-plan-for-scale-from-day-one">3. <strong>Plan for Scale from Day One</strong></h3>
<p>Code that works perfectly at 10 customers might completely break at 10,000. Always think about algorithmic complexity:</p>
<ul>
<li><p><strong>O(1)</strong>: Constant time - scales infinitely</p>
</li>
<li><p><strong>O(log n)</strong>: Logarithmic - scales very well</p>
</li>
<li><p><strong>O(n)</strong>: Linear - degrades as you grow</p>
</li>
<li><p><strong>O(n²)</strong>: Quadratic - disaster waiting to happen</p>
</li>
</ul>
<h3 id="heading-4-migrate-gracefully">4. <strong>Migrate Gracefully</strong></h3>
<p>Use fallbacks and auto-migration to avoid big-bang deployments. Our approach:</p>
<ul>
<li><p>Made changes backward-compatible</p>
</li>
<li><p>Auto-migrated on first use</p>
</li>
<li><p>Monitored migration progress</p>
</li>
<li><p>Removed legacy code only after full migration</p>
</li>
</ul>
<p>Zero downtime, zero broken API keys.</p>
<h3 id="heading-5-defense-in-depth-works">5. <strong>Defense in Depth Works</strong></h3>
<p>Even with hash-based lookup, we still verify by decrypting. This belt-and-suspenders approach:</p>
<ul>
<li><p>Protects against hash collisions</p>
</li>
<li><p>Enables future key rotation</p>
</li>
<li><p>Provides an extra security layer</p>
</li>
<li><p>Costs only one additional decryption (~5ms)</p>
</li>
</ul>
<p>The tiny performance cost is worth the security benefits.</p>
<h3 id="heading-6-database-indexes-are-your-friend">6. <strong>Database Indexes Are Your Friend</strong></h3>
<p>The <code>@unique</code> index on <code>keyHash</code> is what makes the O(1) lookup possible. Without it, we'd still be doing table scans. Always index your lookup fields.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>This optimization transformed our API from a scaling liability into a performant, production-ready system. By understanding the difference between encryption and hashing, and applying the right tool for each job, we turned a potential disaster into a success story.</p>
<p>The next time you're implementing authentication, remember: <strong>how</strong> you store credentials matters just as much as <strong>that</strong> you store them securely.</p>
<hr />
<p><em>Building a SaaS? Check out</em> <a target="_blank" href="https://paymint.dev"><em>Paymint</em></a><em>—we handle subscription billing so you can focus on your product.</em></p>
<p><em>Have questions about this implementation? Found this helpful? Let me know in the comments below!</em></p>
<hr />
<p><strong>Tags:</strong> #performance #security #api-design #database #optimization #saas #authentication #scaling #node-js #postgresql</p>
]]></content:encoded></item><item><title><![CDATA[From 4.5s to 90ms: Solving Webhook Timeouts with Vercel Workflow]]></title><description><![CDATA[When building integrations with third-party services like Paddle or Stripe, webhook handlers face a critical, often overlooked challenge: Response Time Constraints.
Most webhook providers enforce strict timeout limits (typically 5–10 seconds). If you...]]></description><link>https://blog.akashpanchal.com/fixing-webhook-timeouts-vercel-workflow</link><guid isPermaLink="true">https://blog.akashpanchal.com/fixing-webhook-timeouts-vercel-workflow</guid><category><![CDATA[Next.js]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[payment gateway]]></category><category><![CDATA[Vercel]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Sun, 14 Dec 2025 04:47:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765687544816/ee7f577d-8281-493f-9eba-8209d4bf349b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building integrations with third-party services like Paddle or Stripe, webhook handlers face a critical, often overlooked challenge: <strong>Response Time Constraints</strong>.</p>
<p>Most webhook providers enforce strict timeout limits (typically 5–10 seconds). If you don't respond with a <code>200 OK</code> within that window, the provider assumes you failed. They will retry the webhook, potentially leading to duplicate processing, or worse—give up entirely.</p>
<h2 id="heading-the-4500ms-nightmare">The 4500ms Nightmare</h2>
<p>Recently, our Paddle webhook integration started behaving dangerously. We were experiencing response times of approximately <strong>~4500ms</strong>.</p>
<p>With Paddle’s timeout limit set to <strong>5000ms</strong>, we were living on the edge.</p>
<h3 id="heading-why-was-it-so-slow">Why was it so slow?</h3>
<p>Our architecture was synchronous. When a webhook hit our server, we tried to do everything at once:</p>
<ol>
<li><p><strong>Log the entry:</strong> Write a <code>received</code> status to the database.</p>
</li>
<li><p><strong>Process the logic:</strong> Run complex business logic (provisioning licenses, sending emails).</p>
</li>
<li><p><strong>Update the log:</strong> Write a <code>processed</code> status to the database.</p>
</li>
</ol>
<h3 id="heading-the-risks">The Risks</h3>
<p>While this worked in local dev, in production it created three critical issues:</p>
<ul>
<li><p><strong>Timeout Risk:</strong> A slight network blip or database latency would push us over 5000ms, triggering retries.</p>
</li>
<li><p><strong>Data Integrity:</strong> If the handler failed on step 3, the provider would retry, and we might provision the license twice (Step 2).</p>
</li>
<li><p><strong>Scalability:</strong> Synchronous processing locks up server resources, making us vulnerable to burst traffic.</p>
</li>
</ul>
<h2 id="heading-the-solution-asynchronous-durable-workflows">The Solution: Asynchronous Durable Workflows</h2>
<p>We needed to decouple the <strong>acknowledgment</strong> of the event from the <strong>processing</strong> of the event.</p>
<p>We implemented the <strong>Vercel Workflow Development Kit</strong> to transform our synchronous handler into an asynchronous, durable workflow.</p>
<p><strong>The Result?</strong> We reduced our response time from <strong>~4500ms to ~90ms</strong> — a <strong>98% improvement</strong>.</p>
<p>Here is how we architected it using Next.js.</p>
<h3 id="heading-the-architecture-change">The Architecture Change</h3>
<p>Instead of doing the work <em>during</em> the request, we simply validate the request, queue the work, and respond immediately.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765686576482/b1343c5f-450f-4c84-90a4-b6766518fd91.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-implementation-guide">Implementation Guide</h2>
<p>Here is the step-by-step implementation to move from synchronous code to Vercel Workflow.</p>
<h3 id="heading-1-configure-nextjs">1. Configure Next.js</h3>
<p>First, wrap your Next.js config to enable workflow directives.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// next.config.ts</span>
<span class="hljs-keyword">import</span> { withWorkflow } <span class="hljs-keyword">from</span> <span class="hljs-string">'workflow/next'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>;

<span class="hljs-keyword">const</span> nextConfig: NextConfig = {
  <span class="hljs-comment">// ... your Next.js configuration</span>
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> withWorkflow(nextConfig);
</code></pre>
<h3 id="heading-2-update-middleware">2. Update Middleware</h3>
<p>The SDK creates internal endpoints at <code>/.well-known/workflow/*</code>. You must exclude these from your middleware authentication/redirect logic so the workflow engine can communicate with your app.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// middleware.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [
    <span class="hljs-comment">// Exclude .well-known/workflow/ from middleware</span>
    <span class="hljs-string">'/((?!_next/static|_next/image|favicon.ico|.well-known/workflow/).*)'</span>,
  ],
};
</code></pre>
<h3 id="heading-3-the-new-route-handler-the-90ms-fix">3. The New Route Handler (The 90ms Fix)</h3>
<p>This is the most important change. Notice how we do <strong>not</strong> await the business logic. We only await the <code>start()</code> command, which simply queues the job.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/webhooks/paddle/[webhookId]/route.ts</span>
<span class="hljs-keyword">import</span> { start } <span class="hljs-keyword">from</span> <span class="hljs-string">'workflow/api'</span>;
<span class="hljs-keyword">import</span> { workflowPaddleWebhook } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/workflows/webhooks/paddle'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> POST = <span class="hljs-keyword">async</span> (
  request: Request,
  { params }: { params: <span class="hljs-built_in">Promise</span>&lt;{ webhookId: <span class="hljs-built_in">string</span> }&gt; }
): <span class="hljs-built_in">Promise</span>&lt;Response&gt; =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { webhookId } = <span class="hljs-keyword">await</span> params;

    <span class="hljs-comment">// 1. Verify signature (Security is still synchronous!)</span>
    <span class="hljs-keyword">const</span> isValid = <span class="hljs-keyword">await</span> verifyWebhookSignature(request, webhookId);
    <span class="hljs-keyword">if</span> (!isValid) <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'Invalid signature'</span>, { status: <span class="hljs-number">401</span> });

    <span class="hljs-keyword">const</span> eventData = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-comment">// 2. Start workflow asynchronously - returns immediately!</span>
    <span class="hljs-keyword">await</span> start(workflowPaddleWebhook, [
      organizationId,
      <span class="hljs-string">'production'</span>,
      eventData,
    ]);

    <span class="hljs-comment">// 3. Respond immediately</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'Webhook processed'</span>, { status: <span class="hljs-number">200</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error starting webhook workflow'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'Internal server error'</span>, { status: <span class="hljs-number">500</span> });
  }
};
</code></pre>
<h3 id="heading-4-defining-the-workflow">4. Defining the Workflow</h3>
<p>The workflow file acts as the orchestrator. The <code>'use workflow'</code> directive enables automatic retries and state persistence.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// workflows/webhooks/paddle/index.ts</span>
<span class="hljs-keyword">import</span> {
  stepCreateWebhookLog,
  stepHandlePaddleWebhook,
  stepUpdateWebhookLog,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'./steps'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> workflowPaddleWebhook = <span class="hljs-keyword">async</span> (
  organizationId: <span class="hljs-built_in">string</span>,
  environment: <span class="hljs-string">'sandbox'</span> | <span class="hljs-string">'production'</span>,
  eventData: PaddleWebhookPayload
) =&gt; {
  <span class="hljs-string">'use workflow'</span>; <span class="hljs-comment">// &lt;--- The magic directive</span>

  <span class="hljs-comment">// Step 1: Log reception</span>
  <span class="hljs-keyword">const</span> webhookLog = <span class="hljs-keyword">await</span> stepCreateWebhookLog(organizationId, eventData);

  <span class="hljs-comment">// Step 2: Heavy lifting</span>
  <span class="hljs-keyword">await</span> stepHandlePaddleWebhook(organizationId, environment, eventData);

  <span class="hljs-comment">// Step 3: Log completion</span>
  <span class="hljs-keyword">await</span> stepUpdateWebhookLog(webhookLog.id);

  <span class="hljs-keyword">return</span> { success: <span class="hljs-literal">true</span> };
};
</code></pre>
<h3 id="heading-5-defining-the-steps">5. Defining the Steps</h3>
<p>Each step is an isolated unit of work. If Step 2 fails (e.g., the database is temporarily down), the engine will retry <em>only</em> Step 2. Step 1 remains completed.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// workflows/webhooks/paddle/steps.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stepCreateWebhookLog = <span class="hljs-keyword">async</span> (orgId: <span class="hljs-built_in">string</span>, data: <span class="hljs-built_in">any</span>) =&gt; {
  <span class="hljs-string">'use step'</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> database.webhookLog.create({ <span class="hljs-comment">/* ... */</span> });
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> stepHandlePaddleWebhook = <span class="hljs-keyword">async</span> (orgId: <span class="hljs-built_in">string</span>, env: <span class="hljs-built_in">string</span>, data: <span class="hljs-built_in">any</span>) =&gt; {
  <span class="hljs-string">'use step'</span>;

  <span class="hljs-comment">// Dispatch based on event type</span>
  <span class="hljs-keyword">switch</span> (data.event_type) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'product.created'</span>:
      <span class="hljs-keyword">await</span> handleProductCreated(data.data, orgId);
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'subscription.created'</span>:
      <span class="hljs-keyword">await</span> handleSubscriptionCreated(data.data, orgId);
      <span class="hljs-keyword">break</span>;
  }
};
</code></pre>
<h2 id="heading-the-gotchas-handling-idempotency">The "Gotchas": Handling Idempotency</h2>
<p>When you switch to an async retry system, <strong>idempotency is mandatory</strong>.</p>
<p>Because the workflow engine might retry a failed step, your code must be able to run twice without breaking things (e.g., charging a customer twice).</p>
<p>Always check if the work has already been done before processing:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handleSubscriptionCreated = <span class="hljs-keyword">async</span> (data: <span class="hljs-built_in">any</span>, orgId: <span class="hljs-built_in">string</span>) =&gt; {
  <span class="hljs-comment">// Check if we already handled this Event ID</span>
  <span class="hljs-keyword">const</span> existingEvent = <span class="hljs-keyword">await</span> database.webhookEvent.findUnique({
    where: { eventId: data.id },
  });

  <span class="hljs-keyword">if</span> (existingEvent) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Event already processed, skipping.'</span>);
    <span class="hljs-keyword">return</span>; 
  }

  <span class="hljs-comment">// If not, proceed...</span>
  <span class="hljs-keyword">await</span> database.subscription.create({ <span class="hljs-comment">/* ... */</span> });
};
</code></pre>
<h2 id="heading-performance-comparison">Performance Comparison</h2>
<p>The impact on our system stability was immediate.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765686917236/e2af7de6-6ae4-4d14-adb8-d5a77aa59190.png" alt class="image--center mx-auto" /></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Metric</strong></td><td><strong>Before (Synchronous)</strong></td><td><strong>After (Workflow)</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Response Time</strong></td><td>~4500ms</td><td><strong>~90ms</strong></td></tr>
<tr>
<td><strong>Timeout Buffer</strong></td><td>&lt; 10%</td><td><strong>\&gt; 99%</strong></td></tr>
<tr>
<td><strong>Retry Strategy</strong></td><td>None (Fail = Data Loss)</td><td><strong>Automatic &amp; Durable</strong></td></tr>
<tr>
<td><strong>Scalability</strong></td><td>Low (Blocking)</td><td><strong>High (Queued)</strong></td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>Migrating to Vercel Workflow DevKit didn't just speed up our response times; it fundamentally changed how we handle reliability. We no longer fear the 5-second timeout limit, and we have granular visibility into exactly which step of a webhook failed.</p>
<p>If you are dealing with critical webhooks (Payments, CI/CD, Email), move your logic out of the route handler. Your future self will thank you.</p>
<hr />
<h3 id="heading-links-amp-references">Links &amp; References</h3>
<ul>
<li><p><a target="_blank" href="https://useworkflow.dev/docs/getting-started/next">Vercel Workflow DevKit Docs</a></p>
</li>
<li><p><a target="_blank" href="https://developer.paddle.com/webhooks/overview">Paddle Webhook Guidelines</a></p>
</li>
</ul>
<hr />
<p><strong>Have you struggled with webhook timeouts in Next.js? Let me know how you solved it in the comments below!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Configuring HTTP Proxy for gRPC in C# Without Environment Variables]]></title><description><![CDATA[The Challenge
You have a gRPC client in C# using Grpc.Core that needs to route traffic through an HTTP proxy. Sounds simple, right?
Not quite.
If you've searched for solutions, you've probably found:

Set http_proxy environment variable ✅ Works, but ...]]></description><link>https://blog.akashpanchal.com/configuring-http-proxy-for-grpc-in-c-without-environment-variables</link><guid isPermaLink="true">https://blog.akashpanchal.com/configuring-http-proxy-for-grpc-in-c-without-environment-variables</guid><category><![CDATA[proxy]]></category><category><![CDATA[gRPC]]></category><category><![CDATA[C#]]></category><category><![CDATA[mTLS]]></category><dc:creator><![CDATA[Akash Panchal]]></dc:creator><pubDate>Sat, 13 Dec 2025 13:34:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766234345214/2e1db822-fa45-4896-a518-e5510cd636f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-challenge"><strong>The Challenge</strong></h2>
<p>You have a gRPC client in C# using <code>Grpc.Core</code> that needs to route traffic through an HTTP proxy. Sounds simple, right?</p>
<p><strong>Not quite.</strong></p>
<p>If you've searched for solutions, you've probably found:</p>
<ul>
<li><p>Set <code>http_proxy</code> environment variable ✅ Works, but affects ALL HTTP traffic</p>
</li>
<li><p>Use <code>Grpc.Net.Client</code> with <code>HttpClientHandler.Proxy</code> ✅ Clean, but requires library migration</p>
</li>
<li><p>Set <code>grpc.http_proxy</code> channel option ❌ Doesn't work in Grpc.Core</p>
</li>
</ul>
<p>I needed <strong>per-channel proxy configuration</strong> without affecting other traffic and without migrating libraries. So I dove into the gRPC C-core source code to understand how <code>http_proxy</code> actually works.</p>
<p>My major limitation was that my monolith library is using grpc.core v1.10.0 and had no options such as grpc_proxy</p>
<h3 id="heading-source-code-references-grpc-v1100"><strong>Source Code References (gRPC v1.10.0)</strong></h3>
<p>If you want to explore the internals yourself, here are the key files:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>File</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/filters/client_channel/http_proxy.cc"><code>http_proxy.cc</code></a></td><td>Reads <code>http_proxy</code> env var, sets channel args</td></tr>
<tr>
<td><a target="_blank" href="https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/filters/client_channel/http_connect_handshaker.cc"><code>http_connect_handshaker.cc</code></a></td><td>Sends HTTP CONNECT request to proxy</td></tr>
<tr>
<td><a target="_blank" href="https://github.com/grpc/grpc/blob/v1.10.0/src/core/ext/transport/chttp2/client/secure/secure_channel_create.cc"><code>secure_channel_create.cc</code></a></td><td>SSL/TLS target name override handling</td></tr>
<tr>
<td><a target="_blank" href="https://github.com/grpc/grpc/blob/v1.10.0/src/core/lib/surface/channel.cc"><code>channel.cc</code></a></td><td>Default authority header logic</td></tr>
<tr>
<td><a target="_blank" href="https://github.com/grpc/grpc/blob/v1.10.0/src/csharp/Grpc.Core/ChannelOptions.cs"><code>ChannelOptions.cs</code></a></td><td>C# channel option constants</td></tr>
</tbody>
</table>
</div><h2 id="heading-what-i-discovered"><strong>What I Discovered</strong></h2>
<p>When gRPC honors the <code>http_proxy</code> environment variable, it doesn't do anything magical. It simply:</p>
<ol>
<li><p>Parses the proxy URL</p>
</li>
<li><p>Sets internal channel arguments</p>
</li>
<li><p>Uses these arguments during connection</p>
</li>
</ol>
<p>The key insight: <strong>these channel arguments are accessible via</strong> <code>ChannelOption</code> in C#!</p>
<h2 id="heading-the-solution"><strong>The Solution</strong></h2>
<h3 id="heading-understanding-http-connect-tunneling"><strong>Understanding HTTP CONNECT Tunneling</strong></h3>
<p>HTTP proxies use the CONNECT method to create TCP tunnels:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765631349932/d6a93113-8002-4b9b-801e-62b41358718a.png" alt class="image--center mx-auto" /></p>
<p>Once the tunnel is established, TLS handshake and gRPC communication flow through transparently.</p>
<h3 id="heading-the-three-magic-channel-options"><strong>The Three Magic Channel Options</strong></h3>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> Channel <span class="hljs-title">CreateChannel</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> targetEndpoint, SslCredentials credentials, <span class="hljs-keyword">string</span> proxyEndpoint</span>)</span>
{
    <span class="hljs-keyword">string</span> proxy = proxyEndpoint?.Trim();

    <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">string</span>.IsNullOrEmpty(proxy))
    {
        <span class="hljs-comment">// Extract hostname without port for SSL validation</span>
        <span class="hljs-keyword">string</span> targetHost = targetEndpoint.Contains(<span class="hljs-string">":"</span>) 
            ? targetEndpoint.Substring(<span class="hljs-number">0</span>, targetEndpoint.LastIndexOf(<span class="hljs-string">':'</span>)) 
            : targetEndpoint;

        <span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span>[]
        {
            <span class="hljs-comment">// 1. HTTP CONNECT tunnel target (host:port)</span>
            <span class="hljs-keyword">new</span> ChannelOption(<span class="hljs-string">"grpc.http_connect_server"</span>, targetEndpoint),

            <span class="hljs-comment">// 2. SSL SNI + certificate validation (hostname only)</span>
            <span class="hljs-keyword">new</span> ChannelOption(ChannelOptions.SslTargetNameOverride, targetHost),

            <span class="hljs-comment">// 3. HTTP/2 :authority header (host:port)</span>
            <span class="hljs-keyword">new</span> ChannelOption(ChannelOptions.DefaultAuthority, targetEndpoint)
        };

        <span class="hljs-comment">// Channel connects to PROXY, tunnels to TARGET</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Channel(proxy, credentials, options);
    }

    <span class="hljs-comment">// Direct connection</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Channel(targetEndpoint, credentials);
}
</code></pre>
<h3 id="heading-what-each-option-does"><strong>What Each Option Does</strong></h3>
<h4 id="heading-1-grpchttpconnectserver"><strong>1.</strong> <code>grpc.http_connect_server</code></h4>
<p><strong>Purpose:</strong> Tells gRPC where to tunnel</p>
<p><strong>What happens:</strong> When gRPC connects to the channel target (the proxy), it sends:</p>
<pre><code class="lang-bash">CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443
</code></pre>
<p><strong>Format:</strong> <code>host:port</code> (port is required!)</p>
<hr />
<h4 id="heading-2-ssltargetnameoverride"><strong>2.</strong> <code>SslTargetNameOverride</code></h4>
<p><strong>Purpose:</strong> SSL/TLS hostname for SNI and certificate validation</p>
<p><strong>The problem:</strong> Without this, gRPC would:</p>
<ul>
<li><p>Send SNI for the proxy hostname</p>
</li>
<li><p>Validate the certificate against the proxy hostname</p>
</li>
<li><p>Both are WRONG — we need the actual target's certificate!</p>
</li>
</ul>
<p><strong>What happens:</strong></p>
<ul>
<li><p>TLS ClientHello contains correct SNI: <code>api.example.com</code></p>
</li>
<li><p>Certificate validation checks against: <code>api.example.com</code></p>
</li>
</ul>
<p><strong>Format:</strong> <code>hostname</code> (NO port — SSL certificates don't include ports)</p>
<hr />
<h4 id="heading-3-defaultauthority"><strong>3.</strong> <code>DefaultAuthority</code></h4>
<p><strong>Purpose:</strong> Sets the HTTP/2 <code>:authority</code> pseudo-header</p>
<p><strong>The problem:</strong> gRPC servers use <code>:authority</code> for routing. Without this override, it would be set to the proxy address.</p>
<p><strong>What happens:</strong></p>
<pre><code class="lang-plaintext">:method: POST
:scheme: https
:authority: api.example.com:443  ← Correct!
:path: /mypackage.MyService/MyMethod
</code></pre>
<p><strong>Format:</strong> <code>host:port</code> (port often required for server routing)</p>
<hr />
<h2 id="heading-complete-sequence-diagram"><strong>Complete Sequence Diagram</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765631407611/72b22b30-f6a8-4420-8c1a-8eec99b81b74.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-not-just-use-environment-variables"><strong>Why Not Just Use Environment Variables?</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Approach</td><td>Scope</td><td>Risk</td></tr>
</thead>
<tbody>
<tr>
<td><code>http_proxy</code> env var</td><td>Global (all HTTP)</td><td>May break other services</td></tr>
<tr>
<td>Channel Options</td><td>Per-channel</td><td>Isolated, controlled</td></tr>
</tbody>
</table>
</div><p>Environment variables are global. If your application makes HTTP calls to multiple services, and only ONE needs proxying, you can't use environment variables safely.</p>
<p>Channel options give you <strong>surgical precision</strong>.</p>
<p>By understanding how gRPC handles proxies internally, we can configure per-channel proxy support without environment variables, keeping our other HTTP traffic unaffected.<strong>Configuring HTTP Proxy for gRPC in C# Without Environment Variables</strong></p>
]]></content:encoded></item></channel></rss>