<?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: Siddharth Pandey</title>
    <description>The latest articles on DEV Community by Siddharth Pandey (@siddharth_pandey_27).</description>
    <link>https://dev.to/siddharth_pandey_27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1878784%2Fc5935d13-014b-42fe-a1ab-a9374ff32cbb.jpg</url>
      <title>DEV Community: Siddharth Pandey</title>
      <link>https://dev.to/siddharth_pandey_27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siddharth_pandey_27"/>
    <language>en</language>
    <item>
      <title>The Right Way to Start Claude Code on an AWS Project</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Tue, 14 Jul 2026 09:40:36 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/the-right-way-to-start-claude-code-on-an-aws-project-ipf</link>
      <guid>https://dev.to/siddharth_pandey_27/the-right-way-to-start-claude-code-on-an-aws-project-ipf</guid>
      <description>&lt;p&gt;You know the drill for adding an MCP server to a project: dig the exact command string out of the docs, hand-write a &lt;code&gt;.mcp.json&lt;/code&gt; with an absolute path you'll typo once, restart the editor, and discover no tools showed up because the server expected a config file you haven't created yet. Plenty of MCP servers lose their would-be users somewhere inside that loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; collapses the whole loop into one command. It's an open-source tool (&lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;) that statically analyzes your codebase, AWS infrastructure, and database schemas, then exposes that context to AI coding assistants over MCP — so Claude Code knows your actual partition keys, GSIs, and indexes instead of guessing from source files. This post is about the part that usually kills tools like this before they deliver any value: setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 1: One command, four steps
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; infrawise   &lt;span class="c"&gt;# or skip install and use npx&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
infrawise start &lt;span class="nt"&gt;--claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start&lt;/code&gt; does four things, in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Probes your environment.&lt;/strong&gt; If there's no &lt;code&gt;infrawise.yaml&lt;/code&gt; in the project, it generates one. It reads &lt;code&gt;AWS_PROFILE&lt;/code&gt; if set; otherwise it looks at your configured AWS profiles — one profile means zero questions, several means one prompt asking which to use. That's the entire interview. (If you want the full guided wizard instead, &lt;code&gt;infrawise start --interactive&lt;/code&gt; runs it.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Runs the analysis.&lt;/strong&gt; It scans your AWS services, database schemas, and codebase, builds a graph of services, tables, indexes, and query patterns, and runs rule-based analyzers over it. No LLM is involved in this step — extraction and analysis are deterministic, so the same infrastructure always produces the same graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Writes &lt;code&gt;.mcp.json&lt;/code&gt; to your project root.&lt;/strong&gt; This is the file you'd otherwise write by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"infrawise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"infrawise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/infrawise.yaml"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Opens Claude Code.&lt;/strong&gt; Claude Code reads &lt;code&gt;.mcp.json&lt;/code&gt; automatically and starts the session with all 21 infrawise tools available — schema lookups, per-function analysis, GSI suggestions, queue and topic details, the lot.&lt;/p&gt;

&lt;p&gt;If the &lt;code&gt;claude&lt;/code&gt; CLI isn't installed, &lt;code&gt;start&lt;/code&gt; tells you exactly that and where to get it, instead of failing silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 2: Day two — you never type &lt;code&gt;infrawise&lt;/code&gt; again
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than the first run. &lt;code&gt;.mcp.json&lt;/code&gt; doesn't point at a long-running server you have to remember to start. It tells the editor how to spawn one: every time you launch Claude Code in that project, the editor itself runs &lt;code&gt;infrawise serve --stdio&lt;/code&gt; as a child process. There is no port to keep free, no background daemon to babysit, no "is the server running?" debugging session.&lt;/p&gt;

&lt;p&gt;So the second-day workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No infrawise command anywhere.&lt;/p&gt;

&lt;p&gt;Two mechanisms keep the context from going stale underneath you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 24-hour analysis cache.&lt;/strong&gt; The analysis from &lt;code&gt;start&lt;/code&gt; is cached. When the editor spawns the server and the cache is fresh, the session begins instantly with the existing graph. Once the cache is older than 24 hours, the next session start refreshes it. The freshness is also visible to the assistant itself: &lt;code&gt;get_infra_overview&lt;/code&gt; returns an &lt;code&gt;analyzedAt&lt;/code&gt; timestamp, an &lt;code&gt;ageSeconds&lt;/code&gt; value, and a &lt;code&gt;stale&lt;/code&gt; flag, so Claude can tell you — or decide on its own — when the picture it has is old. (This came out of a reader question on an earlier post about deterministic analysis; it shipped as a proper feature.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A file watcher inside the session.&lt;/strong&gt; While the server is running, it watches the repository for file changes and re-runs code analysis on save. Write a new function that calls &lt;code&gt;DynamoDB.scan()&lt;/code&gt; mid-session, and the code graph reflects it — you don't need to restart anything for the assistant to see what you just wrote.&lt;/p&gt;

&lt;p&gt;Infrastructure changes are the one thing that won't propagate automatically mid-session — if you just added a table or changed a GSI in AWS, run &lt;code&gt;infrawise analyze&lt;/code&gt; to force a full re-scan rather than waiting out the cache. And if the config itself has drifted from reality (new AWS account, different profile), &lt;code&gt;infrawise start --rediscover&lt;/code&gt; deletes &lt;code&gt;infrawise.yaml&lt;/code&gt; and the &lt;code&gt;.infrawise/&lt;/code&gt; cache directory and rebuilds both from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 3: Not a Claude Code shop? Same command, different flag
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;--claude&lt;/code&gt; flag is one of three editor targets, and none of them changes what gets analyzed — only where the MCP config lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;infrawise start --cursor&lt;/code&gt; writes &lt;code&gt;.cursor/mcp.json&lt;/code&gt; and opens Cursor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infrawise start --vscode&lt;/code&gt; writes into &lt;code&gt;.vscode/mcp.json&lt;/code&gt; and opens VS Code. This one &lt;strong&gt;merges&lt;/strong&gt; rather than overwrites: if you already have other MCP servers registered in that file, they survive. Infrawise adds its own entry alongside them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infrawise start&lt;/code&gt; with no flag writes &lt;code&gt;.mcp.json&lt;/code&gt;, prints the launch command for each editor, and exits — for any other MCP-capable editor, point it at &lt;code&gt;infrawise serve --stdio --config /path/to/infrawise.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for the teammates who don't use an AI editor at all, the same analysis runs as a CI gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infrawise check &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; runs a fresh analysis and exits non-zero when any finding reaches the threshold severity (&lt;code&gt;high&lt;/code&gt; is the default; &lt;code&gt;medium&lt;/code&gt; and &lt;code&gt;low&lt;/code&gt; tighten it). A full-table scan on a production DynamoDB table fails the build whether or not anyone on the team has ever opened Claude Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 4: What the assistant actually does with it
&lt;/h2&gt;

&lt;p&gt;The payoff for the setup being this short is that the context is there before you need it. Without it, an AI assistant reading only your source files will happily suggest a &lt;code&gt;.scan()&lt;/code&gt; on a table with 50 million rows, or recommend adding a GSI you already have. With the MCP tools connected, it can call &lt;code&gt;get_table_schema&lt;/code&gt; for the exact columns and keys, &lt;code&gt;analyze_function&lt;/code&gt; for a Lambda's real trigger event shape, or &lt;code&gt;suggest_gsi&lt;/code&gt; for a ready-to-use index definition matched to your table's billing mode — before writing the query, not after you've reviewed it.&lt;/p&gt;

&lt;p&gt;I've written about those analysis capabilities in earlier posts; the point of this one is narrower. A context tool only works if it's actually connected, and "actually connected" has to cost less than the problem it solves. Pasting a schema into a prompt costs thirty seconds, every session, forever. &lt;code&gt;infrawise start --claude&lt;/code&gt; costs one command, once.&lt;/p&gt;

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

&lt;p&gt;The gap between "this MCP server exists" and "this MCP server is running in my editor" is where most infrastructure context tools die. Infrawise's answer is to make the editor own the server lifecycle: one &lt;code&gt;start&lt;/code&gt; command probes, analyzes, writes the editor config, and launches; from then on the editor spawns &lt;code&gt;infrawise serve --stdio&lt;/code&gt; itself, a 24-hour cache keeps session starts instant, and a file watcher keeps the code graph current while you work.&lt;/p&gt;

&lt;p&gt;Try it on a real project — the first &lt;code&gt;start&lt;/code&gt; on an actual AWS account is where it gets interesting: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;infrawise start --claude&lt;/code&gt; is the entire setup: probe environment, generate &lt;code&gt;infrawise.yaml&lt;/code&gt;, analyze, write &lt;code&gt;.mcp.json&lt;/code&gt;, open Claude Code with 21 MCP tools.&lt;/li&gt;
&lt;li&gt;After the first run, just launch your editor — it spawns &lt;code&gt;infrawise serve --stdio&lt;/code&gt; from &lt;code&gt;.mcp.json&lt;/code&gt; on its own. No daemon, no port, no second command.&lt;/li&gt;
&lt;li&gt;Analysis is cached for 24 hours and refreshed at session start when stale; the assistant can check freshness itself via &lt;code&gt;get_infra_overview&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;File changes are picked up mid-session by a watcher; infrastructure changes need &lt;code&gt;infrawise analyze&lt;/code&gt;, and &lt;code&gt;--rediscover&lt;/code&gt; rebuilds config from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--cursor&lt;/code&gt; and &lt;code&gt;--vscode&lt;/code&gt; target other editors (the VS Code writer merges with existing MCP servers), and &lt;code&gt;infrawise check --fail-on high&lt;/code&gt; gates CI with the same analysis.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>mcp</category>
      <category>aws</category>
    </item>
    <item>
      <title>Why SNS Silently Drops Your Messages and How to Catch It Before You Ship</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sat, 11 Jul 2026 08:53:46 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/why-sns-silently-drops-your-messages-and-how-to-catch-it-before-you-ship-1fcm</link>
      <guid>https://dev.to/siddharth_pandey_27/why-sns-silently-drops-your-messages-and-how-to-catch-it-before-you-ship-1fcm</guid>
      <description>&lt;p&gt;Your checkout service publishes an &lt;code&gt;OrderRefunded&lt;/code&gt; event to an SNS topic. The publish call returns a &lt;code&gt;MessageId&lt;/code&gt;. No exception, no retry, nothing in the dead-letter queue. Three days later a customer emails asking where their refund is, and you discover the refund Lambda was never invoked.&lt;/p&gt;

&lt;p&gt;The message wasn't lost. It was filtered. One of the topic's subscriptions has a filter policy that requires an &lt;code&gt;eventType&lt;/code&gt; message attribute, and your publish call didn't include it. SNS did exactly what it was configured to do: it evaluated the filter, found no match, and skipped delivery for that subscription. From the publisher's side, everything looks like success.&lt;/p&gt;

&lt;p&gt;This is one of the nastiest failure modes in event-driven AWS architectures, because every tool you'd normally reach for reports green. This post walks through why it happens, why neither your code review nor your AI coding assistant can catch it from source code alone, and how to make the contract visible at coding time instead of incident time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract nobody wrote down
&lt;/h2&gt;

&lt;p&gt;SNS filter policies live on the &lt;em&gt;subscription&lt;/em&gt;, not the topic. A subscriber says "only deliver messages where the &lt;code&gt;eventType&lt;/code&gt; attribute is one of these values":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order.refunded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order.cancelled"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that subscription to ever receive anything, the publisher must include the attribute:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TopicArn&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;ORDER_EVENTS_TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;MessageAttributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;StringValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.refunded&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="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;Omit &lt;code&gt;MessageAttributes&lt;/code&gt; and the publish still succeeds — SNS accepts the message and returns a &lt;code&gt;MessageId&lt;/code&gt; regardless of whether any subscription matches. The message simply isn't delivered to the filtered subscription.&lt;/p&gt;

&lt;p&gt;Three things make this failure quiet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No error surface.&lt;/strong&gt; A filter mismatch is not a delivery failure. The subscription's redrive policy never kicks in, so the message never lands in a DLQ. There is nothing to retry and nothing to alert on by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The only trace is a metric.&lt;/strong&gt; SNS increments a &lt;code&gt;NumberOfNotificationsFilteredOut&lt;/code&gt; CloudWatch metric, but unless you already suspect filtering, nobody looks at it — and by the time you do, you're debugging in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests pass.&lt;/strong&gt; Unit tests mock the SNS client and assert that &lt;code&gt;PublishCommand&lt;/code&gt; was called. The mock has no filter policy. The gap between your code and the subscription's configuration is exactly the part the test can't see.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a second variant of the same trap: filter policies can also match against the message &lt;em&gt;body&lt;/em&gt; (&lt;code&gt;FilterPolicyScope: MessageBody&lt;/code&gt;) instead of message attributes. Then the required keys must appear in your JSON payload itself. Same silence, different location.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your AI assistant writes the broken version
&lt;/h2&gt;

&lt;p&gt;Ask an AI coding assistant to "publish an order refunded event to the order-events topic" and it will produce a perfectly reasonable &lt;code&gt;PublishCommand&lt;/code&gt; call — topic ARN from an environment variable, &lt;code&gt;JSON.stringify&lt;/code&gt; on the payload, maybe a comment. What it will almost never produce is the &lt;code&gt;MessageAttributes&lt;/code&gt; block with exactly the keys your subscriptions filter on.&lt;/p&gt;

&lt;p&gt;It can't. The filter policy is not in your repository. It's a JSON document attached to a subscription in your AWS account, set by whoever wired up the consumer — possibly you, six months ago. The assistant reads your source files, sees other publish calls (which may themselves be missing attributes), and generates code that matches the pattern it found. If the pattern is wrong, the wrongness propagates.&lt;/p&gt;

&lt;p&gt;The manual fix is miserable: open the AWS console, navigate to the topic, open each subscription one by one, read each filter policy, and paste the required attribute names into your prompt. Every session. For every topic. This is the copy-paste loop that breaks flow — and if you skip it once, you ship the silent drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling the contract into the coding session
&lt;/h2&gt;

&lt;p&gt;This is the problem &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;infrawise&lt;/a&gt; is built for: it extracts your live infrastructure into a graph and serves it to your AI assistant over MCP, so the assistant queries facts instead of guessing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
npx infrawise start &lt;span class="nt"&gt;--claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That analyzes your AWS account and codebase, writes &lt;code&gt;.mcp.json&lt;/code&gt;, and opens Claude Code with 21 MCP tools connected. For SNS specifically, the extraction is read-only and direct: infrawise lists your topics, then for every confirmed subscription fetches its attributes, parses the &lt;code&gt;FilterPolicy&lt;/code&gt; JSON, and records which attribute keys the policy requires and whether the policy scope is &lt;code&gt;MessageAttributes&lt;/code&gt; or &lt;code&gt;MessageBody&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is exposed through a tool called &lt;code&gt;get_topic_details&lt;/code&gt;. For each topic it returns the subscription count, encryption status, and a &lt;code&gt;filterPolicies&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"encrypted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filterPolicies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:...:order-events:a1b2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requiredAttributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MessageAttributes"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the workflow changes. When you ask your assistant to write a publish call, it calls &lt;code&gt;get_topic_details&lt;/code&gt; first, sees that a subscription on &lt;code&gt;order-events&lt;/code&gt; filters on &lt;code&gt;eventType&lt;/code&gt;, and writes the &lt;code&gt;MessageAttributes&lt;/code&gt; block into the code on the first attempt. The contract that used to live invisibly in a subscription's configuration is now part of the context every generated publish call is checked against.&lt;/p&gt;

&lt;p&gt;The tool description itself tells the assistant when to use it — before writing any SNS publish code — so you don't have to remember to prompt for it. And because infrawise also scans your application code (an AST pass that recognizes &lt;code&gt;PublishCommand&lt;/code&gt;, &lt;code&gt;PublishBatchCommand&lt;/code&gt;, and SDK &lt;code&gt;publish&lt;/code&gt; calls and resolves their target topic ARNs), the graph knows which of your functions publish to which topics. Reviewing an existing publisher works the same way: the assistant can cross-reference what the function sends against what the topic's subscriptions require.&lt;/p&gt;

&lt;p&gt;A few boundaries worth stating, because tools that read your AWS account should be explicit about them. Infrawise is read-only — the SNS extraction uses &lt;code&gt;GetTopicAttributes&lt;/code&gt;, &lt;code&gt;ListSubscriptionsByTopic&lt;/code&gt;, and &lt;code&gt;GetSubscriptionAttributes&lt;/code&gt;, nothing that writes. It never reads message contents, secret values, or parameter values. And the analysis is deterministic: AST parsing and API introspection, no LLM deciding what your infrastructure looks like. The LLM is only a consumer of the extracted context.&lt;/p&gt;

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

&lt;p&gt;Silent message drops are a configuration-versus-code mismatch, and those never show up in the layer you're staring at. The filter policy is correct. The publish code is correct. Only the combination is broken, and the combination exists nowhere in your repository — until you put it there.&lt;/p&gt;

&lt;p&gt;You can do that manually every session by reading subscriptions in the console, or you can have it extracted once and served to your assistant automatically. If you're on the second option: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt; — &lt;code&gt;npx infrawise start --claude&lt;/code&gt; and ask your assistant what the &lt;code&gt;order-events&lt;/code&gt; topic requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An SNS publish that misses a subscription's filter policy succeeds silently: &lt;code&gt;MessageId&lt;/code&gt; returned, no DLQ entry, no error. The redrive policy only covers delivery failures, and a filter mismatch isn't one.&lt;/li&gt;
&lt;li&gt;The publisher's contract lives on the subscription, in AWS, not in your code — so tests with mocked SNS clients and AI assistants reading source files both miss it.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;FilterPolicyScope&lt;/code&gt;: policies can require message &lt;em&gt;attributes&lt;/em&gt; or keys in the message &lt;em&gt;body&lt;/em&gt;. The fix is different for each.&lt;/li&gt;
&lt;li&gt;Before writing any publish call, enumerate the target topic's filter policies and include every required attribute. With infrawise, &lt;code&gt;get_topic_details&lt;/code&gt; gives your AI assistant that list automatically.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;NumberOfNotificationsFilteredOut&lt;/code&gt; CloudWatch metric is your post-hoc signal, but the goal is to never need it — catch the missing attribute at coding time.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Stop Sending Your AI Assistant 40 Tables When It Only Needs 3</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Tue, 07 Jul 2026 08:35:15 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/stop-sending-your-ai-assistant-40-tables-when-it-only-needs-3-19b7</link>
      <guid>https://dev.to/siddharth_pandey_27/stop-sending-your-ai-assistant-40-tables-when-it-only-needs-3-19b7</guid>
      <description>&lt;p&gt;Say your service has 40 tables. You ask Claude Code to fix a bug in checkout — a function that touches exactly three of them: &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;payments&lt;/code&gt;, &lt;code&gt;inventory_reservations&lt;/code&gt;. If your MCP server hands the model your whole schema graph on every call just to answer that, you've spent a few thousand tokens of context on 37 tables nobody asked about, before the model has written a single line of code.&lt;/p&gt;

&lt;p&gt;Multiply that by every tool call, every session, every developer on the team, and "just give it the schema" turns into a real line item — slower responses, a noisier context window, and a model more likely to get distracted by a &lt;code&gt;campaigns&lt;/code&gt; table that has nothing to do with the bug you're fixing.&lt;/p&gt;

&lt;p&gt;Infrawise's MCP tools are built around a specific answer to this: never send more schema than the task needs, and give the agent an explicit way to ask for exactly what it's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bad defaults
&lt;/h2&gt;

&lt;p&gt;Without something like this, an AI coding assistant reading your codebase has two options, and both are wrong in a different direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option one: no schema at all.&lt;/strong&gt; The model reads your source files, sees a &lt;code&gt;DocumentClient.scan()&lt;/code&gt; call, and has no way to know that table has 50 million rows and a GSI it isn't using. It writes code that compiles and looks reasonable and is wrong the moment it touches production data — because "wrong" here isn't a syntax problem, it's a missing fact about your infrastructure that isn't in any file it can read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option two: dump everything, every time.&lt;/strong&gt; Paste the full schema — every table, every column, every foreign key, every DynamoDB GSI — into the prompt so the model definitely has what it needs. This works, in the sense that the model now has the fact it's missing. It also means every single request pays for the full graph whether the task touches one table or fifteen. Context windows aren't free, and neither is the model's attention: the more irrelevant schema it has to read past, the more likely it latches onto the wrong table or a stale column name from a service you don't even own.&lt;/p&gt;

&lt;p&gt;Infrawise's MCP server is designed around a third option: give the agent a cheap way to see what exists, then let it ask for detail only on the things it's actually going to touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the lookup actually works
&lt;/h2&gt;

&lt;p&gt;The server exposes 21 tools, but three of them define the whole pattern.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get_infra_overview&lt;/code&gt; is the entry point. It returns a compact snapshot — every table and collection by name and database type, queue and topic names, secret names, Lambda names, high-severity findings — no columns, no foreign keys, no indexes. It's meant to answer "what exists here" in a few hundred tokens, not "give me everything about it."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get_table_schema&lt;/code&gt; is where the actual detail lives, and it's scoped on purpose: it takes a list of 1 to 20 table or collection names and returns, per name, the columns with data types and nullability, primary keys, foreign keys (so an agent building a join knows the path without guessing), indexes, DynamoDB partition/sort keys, or a MongoDB estimated document count. Row data is never included — this is schema, not a data dump. Names are matched case-insensitively and by suffix, so asking for &lt;code&gt;orders&lt;/code&gt; matches &lt;code&gt;public.orders&lt;/code&gt; without the agent needing to know your schema prefix in advance. Ask for a table that doesn't exist and instead of a bare failure, you get up to five closest name matches back — useful when the agent guessed &lt;code&gt;order&lt;/code&gt; instead of &lt;code&gt;orders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get_graph_summary&lt;/code&gt; is still there, and it still returns everything — every node, every edge, every finding, no filtering. It's the tool description that makes the intent explicit: it exists as the tool to reach for when you genuinely need the full picture across services, not the one an agent should reach for to answer "what does the orders table look like." The default path is &lt;code&gt;get_infra_overview&lt;/code&gt; for orientation, &lt;code&gt;get_table_schema&lt;/code&gt; for the two or three tables actually in scope, and &lt;code&gt;get_graph_summary&lt;/code&gt; only when the task is broad enough to need it — reviewing an entire service, tracing relationships across five different tables and functions at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like on a real task
&lt;/h2&gt;

&lt;p&gt;Take the checkout bug from the top. An agent working through it calls &lt;code&gt;get_infra_overview&lt;/code&gt; once and learns there are 40 tables across Postgres and DynamoDB, plus a queue and a couple of Lambdas — cheap, one call, no column data yet. It's now looking at &lt;code&gt;orders.ts&lt;/code&gt;, sees a query joining on &lt;code&gt;payment_id&lt;/code&gt;, and calls &lt;code&gt;get_table_schema&lt;/code&gt; with &lt;code&gt;["orders", "payments"]&lt;/code&gt;. Back comes exactly two schemas: column types, the foreign key from &lt;code&gt;orders.payment_id&lt;/code&gt; to &lt;code&gt;payments.id&lt;/code&gt;, and the indexes on both tables. That's the entire fetched context for the task — two tables, not forty.&lt;/p&gt;

&lt;p&gt;If the bug turns out to touch &lt;code&gt;inventory_reservations&lt;/code&gt; too, the agent just adds it to the next &lt;code&gt;get_table_schema&lt;/code&gt; call. It never had to have asked for it upfront, and it never had to eat the cost of the other 37 tables it was never going to look at.&lt;/p&gt;

&lt;p&gt;Compare that to the alternative most teams reach for without a tool like this: pasting the schema export once, keeping it in context, and hoping it doesn't drift as the schema changes. Infrawise's tables come from a fresh analysis (&lt;code&gt;get_infra_overview&lt;/code&gt; even reports a &lt;code&gt;freshness&lt;/code&gt; field with an age and a &lt;code&gt;stale&lt;/code&gt; flag once the cached analysis passes 24 hours), so the agent knows when to ask for a re-run instead of working off of something that quietly went out of date three deploys ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond the token count
&lt;/h2&gt;

&lt;p&gt;The token savings are the visible part, but the more important effect is on accuracy. A model given forty tables' worth of columns has to do implicit filtering — figure out which of them are relevant, hold the rest as noise. Give it three tables that are actually in scope, and there's no filtering step: everything in context is something it's going to use. That's a smaller, sharper problem than "here's a schema, find what you need," and models are measurably better at smaller, sharper problems.&lt;/p&gt;

&lt;p&gt;It also composes with what AGENTS.md calls out directly for large-database use cases — a text-to-SQL or query-writing agent should call &lt;code&gt;get_infra_overview&lt;/code&gt; once per session for the table inventory, then &lt;code&gt;get_table_schema&lt;/code&gt; only for the tables the current query touches, and treat &lt;code&gt;get_graph_summary&lt;/code&gt; as the tool of last resort, not the default. That's the same pattern the checkout example walks through, just named as the recommended path for exactly the case where dumping everything hurts most — a database with hundreds of tables where "just paste the schema" was never realistic in the first place.&lt;/p&gt;

&lt;p&gt;None of this requires the developer to think about it. You don't decide when to call &lt;code&gt;get_table_schema&lt;/code&gt; versus &lt;code&gt;get_graph_summary&lt;/code&gt; — the agent does, because the tool descriptions say when to reach for each one. The developer experience is just: ask Claude Code to fix the bug, and it already knows which two tables matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two bad defaults exist for schema context: give an AI assistant nothing (it guesses and gets it wrong) or give it everything (it pays for and gets distracted by tables it will never touch).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_infra_overview&lt;/code&gt; is a compact, column-free snapshot meant for orientation — names and types, not detail.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_table_schema&lt;/code&gt; fetches full column, key, and index detail for up to 20 named tables at a time, matched case-insensitively by short name, with fuzzy suggestions on a miss.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_graph_summary&lt;/code&gt; still returns the full graph — it's the explicit escape hatch for cross-service work, not the tool an agent should reach for to answer a question about two tables.&lt;/li&gt;
&lt;li&gt;The pattern scales down naturally to large databases: fetch the inventory once per session, then pull schemas only for the tables the current task actually touches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Pasting Your Database Schema Into Every AI Prompt</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:04:42 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/stop-pasting-your-database-schema-into-every-ai-prompt-5bk0</link>
      <guid>https://dev.to/siddharth_pandey_27/stop-pasting-your-database-schema-into-every-ai-prompt-5bk0</guid>
      <description>&lt;p&gt;Your team builds an internal agent that answers questions like "revenue by product category last quarter." It generates SQL against a Postgres database with 240 tables. The first version does the obvious thing: dump the entire schema into the system prompt. It mostly works, and it is quietly terrible — every single question pays the token cost of 240 table definitions, the agent confuses &lt;code&gt;users.name&lt;/code&gt; with the actual column &lt;code&gt;full_name&lt;/code&gt; because the real signal is buried in noise, and the schema snapshot in the prompt drifts out of date the moment someone runs a migration.&lt;/p&gt;

&lt;p&gt;The fix is not a bigger context window. It is giving the agent the same thing a human analyst has: a way to &lt;em&gt;look up&lt;/em&gt; the tables it needs, when it needs them.&lt;/p&gt;

&lt;p&gt;This is a use case I did not originally design &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; for — it started as infrastructure context for AI coding assistants — but as of v0.15.0 it handles this pattern end to end, for SQL and NoSQL databases. (&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema dump is the problem, not the model
&lt;/h2&gt;

&lt;p&gt;When a query-writing agent gets the whole schema up front, three things go wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token cost scales with your database, not your question.&lt;/strong&gt; A 240-table schema rendered as DDL is tens of thousands of tokens. You pay that on every request, forever, even when the question touches two tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Irrelevant schema actively hurts accuracy.&lt;/strong&gt; Models pick columns from tables that merely look plausible. The more near-duplicate tables in context (&lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;orders_archive&lt;/code&gt;, &lt;code&gt;orders_v2&lt;/code&gt;), the more confidently wrong the generated SQL gets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dump goes stale.&lt;/strong&gt; Someone pastes the schema into the prompt template in March. In May a column is renamed. The agent keeps generating SQL against the March schema, and the failures look like model errors instead of what they are: stale context.&lt;/p&gt;

&lt;p&gt;The alternative is progressive disclosure — a small inventory up front, full detail on demand. Infrawise exposes exactly that over &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;, the protocol most agent SDKs can already speak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory first, schema on demand
&lt;/h2&gt;

&lt;p&gt;Infrawise runs a read-only analysis of your infrastructure — Postgres, MySQL, and MongoDB via schema introspection, DynamoDB via the AWS API — and serves the result through an MCP server. It never reads row data, only metadata.&lt;/p&gt;

&lt;p&gt;The agent flow is two calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call 1, once per session: &lt;code&gt;get_infra_overview&lt;/code&gt;.&lt;/strong&gt; This returns a compact inventory — every table name with its database type, plus counts and high-severity findings. For a 240-table database this is a fraction of the tokens of a full dump, because it is names, not definitions. The response also carries a &lt;code&gt;freshness&lt;/code&gt; object with &lt;code&gt;analyzedAt&lt;/code&gt;, &lt;code&gt;ageSeconds&lt;/code&gt;, and a &lt;code&gt;stale&lt;/code&gt; flag that flips after 24 hours, so the agent knows whether the analysis is current or should be refreshed with &lt;code&gt;infrawise analyze&lt;/code&gt;. That freshness contract means you can cache the inventory for the session and trust the server to tell you when it has gone stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call 2, per question: &lt;code&gt;get_table_schema&lt;/code&gt;.&lt;/strong&gt; When the agent decides the question needs &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;payments&lt;/code&gt;, it asks for exactly those (up to 20 names per call):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_table_schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Names are matched case-insensitively, and short names work — &lt;code&gt;orders&lt;/code&gt; matches &lt;code&gt;public.orders&lt;/code&gt;, so the agent does not need to know your schema-qualification convention. The response is the full picture for just those tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Row data is never included."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public.orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"databaseType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"character varying"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dataType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"numeric"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"primaryKeys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"indexes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders_pkey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Column names, data types, nullability, primary keys, indexes. If the agent asks for a table that does not exist — a hallucinated name, a typo — it gets &lt;code&gt;found: false&lt;/code&gt;, and when near matches exist, up to five suggestions from the real inventory. That turns a silent wrong-table failure into a correctable one.&lt;/p&gt;

&lt;p&gt;The prompt for any given question now contains the inventory plus two or three real schemas instead of 240 speculative ones. Costs drop, and the model stops choosing columns from tables that were never relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Foreign keys are the context your agent actually needed
&lt;/h2&gt;

&lt;p&gt;Column lists get you valid syntax. Join paths get you correct queries — and join paths are exactly what schema dumps usually omit, because people paste &lt;code&gt;\d&lt;/code&gt; output or ORM models without constraint details.&lt;/p&gt;

&lt;p&gt;Infrawise extracts foreign keys directly from &lt;code&gt;information_schema&lt;/code&gt; (Postgres and MySQL) during analysis, and &lt;code&gt;get_table_schema&lt;/code&gt; returns them per table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"foreignKeys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"referencesTable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public.orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"referencesColumn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent fetches &lt;code&gt;payments&lt;/code&gt; and sees &lt;code&gt;order_id → public.orders.id&lt;/code&gt;, the join is no longer a guess. Chains compose the same way: fetch the tables the question mentions, follow the &lt;code&gt;referencesTable&lt;/code&gt; values one hop out, and the agent has the join graph for the query without ever seeing the other 230 tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same flow works for NoSQL
&lt;/h2&gt;

&lt;p&gt;The tool is polymorphic across database types, because "what does my agent need to know before writing this query" differs by engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; — the response carries &lt;code&gt;partitionKey&lt;/code&gt; and &lt;code&gt;sortKey&lt;/code&gt;, which decide whether the access pattern is a cheap &lt;code&gt;Query&lt;/code&gt; or an expensive &lt;code&gt;Scan&lt;/code&gt;. Index names cover the GSIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB&lt;/strong&gt; — collections return their indexes and an estimated document count, so the agent can tell a 100-row lookup table from a 50M-document collection before choosing a filter strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One inventory call, one schema tool, four database engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into an agent
&lt;/h2&gt;

&lt;p&gt;Setup is one command in the project that has your &lt;code&gt;infrawise.yaml&lt;/code&gt; (or lets &lt;code&gt;start&lt;/code&gt; generate one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx infrawise start    &lt;span class="c"&gt;# analyze + write .mcp.json for stdio-based editors/agents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For agents that speak HTTP instead of stdio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infrawise serve        &lt;span class="c"&gt;# MCP server at http://localhost:3000/mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any MCP-capable agent framework can then list the tools and call them. The system prompt shrinks to a policy instead of a schema:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Call &lt;code&gt;get_infra_overview&lt;/code&gt; once to learn what tables exist. Before writing a query, call &lt;code&gt;get_table_schema&lt;/code&gt; with only the tables the question needs. Use the returned foreign keys for joins. Never guess column names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two cautions from building this. First, the server has no authentication — it is designed to run locally or inside a private network, next to the agent. Keep it off the public internet; it exposes schema metadata (never data, never secrets, but your table names are your business). Second, the analysis is a snapshot: honor the &lt;code&gt;stale&lt;/code&gt; flag rather than assuming the graph is live.&lt;/p&gt;

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

&lt;p&gt;"Text-to-SQL accuracy" problems are very often context problems wearing a trench coat. An agent that receives 240 table definitions is being asked to find a needle in a haystack it paid for by the token. An agent that can look up exactly the three tables a question touches — with types, primary keys, and foreign-key join paths — is doing what a competent analyst does with a schema browser open.&lt;/p&gt;

&lt;p&gt;Infrawise was built to stop AI coding assistants from guessing about infrastructure. It turns out the same deterministic extraction, served over MCP, is a schema context provider for any query-writing agent. That flow shipped in v0.15.0: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full-schema prompts scale cost with database size, not question size — and the irrelevant 95% of the schema makes column hallucination worse, not better.&lt;/li&gt;
&lt;li&gt;Use progressive disclosure: &lt;code&gt;get_infra_overview&lt;/code&gt; once per session for the table inventory, &lt;code&gt;get_table_schema&lt;/code&gt; for only the tables each question needs.&lt;/li&gt;
&lt;li&gt;Foreign keys are the highest-value schema context for SQL generation — they turn joins from guesses into lookups. Infrawise extracts them from &lt;code&gt;information_schema&lt;/code&gt; automatically.&lt;/li&gt;
&lt;li&gt;Unknown table names return suggestions instead of silent failure, catching hallucinated tables before they become broken SQL.&lt;/li&gt;
&lt;li&gt;Respect the built-in freshness contract: the overview reports analysis age and flips a &lt;code&gt;stale&lt;/code&gt; flag at 24 hours, so agents know when to trigger a re-analysis instead of trusting old schema.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>ai</category>
      <category>sql</category>
    </item>
    <item>
      <title>Your SQS Queue Is Redelivering Messages Your Lambda Is Still Processing</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sun, 05 Jul 2026 09:41:35 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-sqs-queue-is-redelivering-messages-your-lambda-is-still-processing-40k2</link>
      <guid>https://dev.to/siddharth_pandey_27/your-sqs-queue-is-redelivering-messages-your-lambda-is-still-processing-40k2</guid>
      <description>&lt;p&gt;Your order-processing Lambda starts sending duplicate confirmation emails. Not always — maybe one order in twenty. CloudWatch shows more invocations than messages published. The function code hasn't changed in weeks. What changed is that someone added a fraud check that pushed processing time from 25 seconds to around 45, and your SQS queue is still running the default 30-second visibility timeout.&lt;/p&gt;

&lt;p&gt;That combination is the whole bug. When a Lambda pulls a message from SQS, the message isn't deleted — it's hidden for the duration of the visibility timeout. If the function is still working when that window closes, SQS assumes the consumer died and hands the same message to another invocation. Now two Lambdas are processing the same order, both will "succeed," and both will send the email. Nothing errors. Nothing retries. There is no log line that says "this message was delivered twice because your timeouts are misconfigured."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; (&lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;) flags this exact mismatch as a high-severity finding before it costs you an afternoon of staring at idempotency-free handler code. This post walks through why the bug is so hard to see, how the detection works, and how to keep an AI assistant from reintroducing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you never catch this one yourself
&lt;/h2&gt;

&lt;p&gt;Three things make this misconfiguration nearly invisible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It passes every test.&lt;/strong&gt; In local tests and staging, your handler processes a synthetic message in two seconds. The 30-second visibility window never comes close to expiring. The bug only exists under production conditions — real payload sizes, real downstream latency, cold starts stacking on top of slow dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The defaults set the trap.&lt;/strong&gt; SQS queues default to a 30-second visibility timeout. Lambda functions routinely get their timeout bumped to 60, 120, or 900 seconds as they grow. Nobody bumps the queue at the same time, because the two settings live in different consoles, different IaC resources, and usually different pull requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure signature points elsewhere.&lt;/strong&gt; Duplicate processing looks like an application bug. You'll audit your handler for accidental double-sends, check whether the producer published twice, and read DynamoDB conditional-write docs before anyone thinks to compare two timeout values across two services.&lt;/p&gt;

&lt;p&gt;The fix is one line of IaC. Finding out you need it is the expensive part.&lt;/p&gt;

&lt;h2&gt;
  
  
  How infrawise detects the mismatch
&lt;/h2&gt;

&lt;p&gt;Infrawise builds a graph of your actual AWS account and runs rule-based analyzers over it — no LLM involved in the analysis, so a finding either fires or it doesn't (&lt;a href="https://dev.to/siddharth_pandey_27/why-infrawise-uses-deterministic-analysis-instead-of-an-llm-15fk"&gt;deterministic by design&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For this check, two extractions matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue attributes.&lt;/strong&gt; For every queue returned by &lt;code&gt;ListQueues&lt;/code&gt;, infrawise calls &lt;code&gt;GetQueueAttributes&lt;/code&gt; and records &lt;code&gt;VisibilityTimeout&lt;/code&gt; alongside the redrive policy, encryption status, and approximate message counts. A queue node in the graph carries its &lt;code&gt;visibilityTimeoutSec&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event source mappings.&lt;/strong&gt; Infrawise paginates through &lt;code&gt;ListEventSourceMappings&lt;/code&gt; and attaches each mapping to its Lambda. Every SQS-type mapping becomes a &lt;code&gt;triggers&lt;/code&gt; edge in the graph: &lt;code&gt;queue:aws:order-events → lambda:aws:process-order&lt;/code&gt;. Disabled mappings are skipped — a queue that used to feed a Lambda doesn't generate noise.&lt;/p&gt;

&lt;p&gt;Then the &lt;code&gt;VisibilityTimeoutMismatchAnalyzer&lt;/code&gt; walks the graph: for each queue that actually triggers a Lambda, it compares the queue's visibility timeout against that function's configured timeout. If the visibility timeout is smaller, you get a high-severity finding.&lt;/p&gt;

&lt;p&gt;Two details keep this precise rather than noisy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Only wired-up queues are checked.&lt;/strong&gt; A queue with no active event source mapping is ignored. The analyzer isn't pattern-matching on names or guessing at architecture — it follows the same edge SQS itself uses to deliver messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The comparison uses real values from both sides.&lt;/strong&gt; The Lambda timeout comes from the function configuration; the visibility timeout comes from the live queue attributes. If your Terraform says one thing and someone changed the queue in the console, infrawise reports what's actually deployed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running &lt;code&gt;infrawise analyze&lt;/code&gt; against the scenario above prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1.  HIGH   Queue "order-events" visibility timeout (30s) is less than Lambda "process-order" timeout (120s)
       If the Lambda takes longer than the visibility timeout, SQS will re-deliver the message to another consumer while the original invocation is still running, causing duplicate processing.
       → Set the visibility timeout for "order-events" to at least 720s (6× the Lambda timeout of 120s), per AWS best practice.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 6× multiplier follows AWS's own guidance for Lambda event source mappings: the extra headroom covers batch processing and retries within the polling window, not just a single invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — and keeping it fixed when AI writes your infra
&lt;/h2&gt;

&lt;p&gt;The immediate fix is mechanical. In CDK:&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;orderEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrderEvents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 6× the consumer's 120s timeout&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The longer-term problem is that this class of bug gets reintroduced constantly — and increasingly by AI assistants. Ask an assistant to "add an SQS queue that triggers the order processor" and it will happily emit a queue with default settings wired to a 120-second function. The code is syntactically perfect. It deploys. It has the bug from paragraph one built in.&lt;/p&gt;

&lt;p&gt;This is where infrawise's MCP server changes the workflow. Run once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infrawise start &lt;span class="nt"&gt;--claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It scans your account, writes &lt;code&gt;.mcp.json&lt;/code&gt; to the project root, and opens Claude Code with the analysis available as tools. From then on the assistant can call &lt;code&gt;get_queue_details&lt;/code&gt;, which returns every queue with its &lt;code&gt;visibilityTimeoutSec&lt;/code&gt;, DLQ presence, encryption, FIFO flag, and message counts — plus any findings attached to that queue. The tool description explicitly tells the model to verify visibility timeout against Lambda timeout when reviewing messaging architecture, so an assistant asked to touch queue infrastructure checks the real numbers instead of emitting defaults.&lt;/p&gt;

&lt;p&gt;Concretely, the before/after looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; "add a queue for order events" → assistant generates &lt;code&gt;new sqs.Queue(...)&lt;/code&gt; with no visibility timeout → default 30s ships → duplicates appear weeks later when processing slows down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After:&lt;/strong&gt; the assistant calls &lt;code&gt;get_queue_details&lt;/code&gt; and &lt;code&gt;get_lambda_overview&lt;/code&gt;, sees the consumer's 120s timeout, and generates the queue with &lt;code&gt;visibilityTimeout: Duration.seconds(720)&lt;/code&gt; — citing the existing high-severity finding if one is already live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two adjacent findings tend to show up in the same report, and they compound. If the mismatched queue also has no dead-letter queue — another high-severity check — your duplicate-processing problem coexists with silent message loss after retries are exhausted. And note the fix's flip side: a properly long visibility timeout means a genuinely failed message stays hidden for that full window before retry, which is exactly why the DLQ check matters alongside this one. Duplicate delivery can still happen in rare cases even with correct timeouts (SQS standard queues are at-least-once), so idempotent handlers remain good practice — but there's a difference between designing for a rare edge case and misconfiguring your way into hitting it on every slow invocation.&lt;/p&gt;

&lt;p&gt;Analysis results are cached for 24 hours, and the MCP server refreshes stale analysis at session start, so the numbers your assistant reads track what's actually deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Lambda timeout longer than its queue's visibility timeout guarantees duplicate processing under load.&lt;/strong&gt; SQS redelivers any message whose consumer is still running when the visibility window closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The defaults create the bug:&lt;/strong&gt; queues start at 30 seconds, Lambda timeouts grow over time, and the two values live in different resources that rarely change in the same PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set visibility timeout to 6× the consumer Lambda's timeout&lt;/strong&gt;, per AWS guidance — and pair every triggered queue with a dead-letter queue so retries can't silently discard messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check deployed values, not IaC intent.&lt;/strong&gt; Infrawise compares the live queue attribute against the live function configuration, so console drift gets caught too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give your AI assistant the real numbers.&lt;/strong&gt; With infrawise's MCP tools connected, an assistant generating queue infrastructure reads actual timeouts instead of shipping defaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it against your own account — or against a free LocalStack sandbox if you want to see the findings without touching real AWS: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Block AI-Generated Infrastructure Mistakes in CI Before They Hit Production</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sun, 28 Jun 2026 17:57:30 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/block-ai-generated-infrastructure-mistakes-in-ci-before-they-hit-production-431o</link>
      <guid>https://dev.to/siddharth_pandey_27/block-ai-generated-infrastructure-mistakes-in-ci-before-they-hit-production-431o</guid>
      <description>&lt;p&gt;Last Tuesday your team merged a PR with 40 lines of clean TypeScript. Code review passed — the function was readable, typed correctly, and had a unit test. Twenty minutes after deploy, CloudWatch alerted: your Orders table was being fully scanned on every request. The merged function called &lt;code&gt;.scan()&lt;/code&gt; without a partition key filter. Claude Code wrote it; nobody caught it because nobody — not the reviewer, not the tests, not the linter — had any way to know that Orders has 8 million items.&lt;/p&gt;

&lt;p&gt;This is the gap &lt;code&gt;infrawise check&lt;/code&gt; fills. It's a CI step that reads your actual DynamoDB schemas, PostgreSQL indexes, and query patterns, then fails the build when AI-generated code introduces anti-patterns against your real infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap Between Code Review and Infrastructure Reality
&lt;/h2&gt;

&lt;p&gt;Static analysis tools like ESLint, TypeScript, and Semgrep analyze the code. They can't tell you that &lt;code&gt;listAllOrders()&lt;/code&gt; is doing a full table scan, or that your PostgreSQL &lt;code&gt;users&lt;/code&gt; table has no indexes and the new query will degrade from milliseconds to seconds as data grows.&lt;/p&gt;

&lt;p&gt;AI assistants compound this. They write syntactically correct code that compiles and passes tests — but they're working from source files, not live infrastructure. They don't know your table's partition key distribution. They don't know you already have a GSI on &lt;code&gt;status&lt;/code&gt;. They generate code against an imagined infrastructure and get it wrong in ways that only surface at scale.&lt;/p&gt;

&lt;p&gt;The consequence isn't just performance. A DynamoDB full scan in a high-traffic Lambda reads capacity units proportional to table size on every invocation. A missing index causes query times to grow linearly with rows. Unit tests won't catch either of these; they show up in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  infrawise check — a Build Step That Knows Your Tables
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;infrawise check&lt;/code&gt; runs a fresh analysis of your codebase and cloud infrastructure, then sets exit code 1 when blocking findings exist. That exit code is all a CI pipeline needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infrawise check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actual output on violations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Blocking findings  2 at or above high

  1.  HIGH   Full table scan detected on DynamoDB table "Orders"
             The table "Orders" is being scanned without any filter, which reads every item.
             → Replace Scan with a Query operation using a partition key or GSI.

  2.  HIGH   Full table scan detected on DynamoDB table "Sessions"
             The table "Sessions" is being scanned without any filter. Called from: cleanupExpiredSessions
             → Replace Scan with a Query operation using a partition key or GSI.

  ✗ Check failed
    2 high+ finding(s) must be resolved before deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings reference the exact table name and caller function. The recommendation isn't generic — it points to the specific operation to replace.&lt;/p&gt;

&lt;p&gt;Add it to GitHub Actions in one step:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Infrastructure check&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infrawise check --fail-on high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--fail-on high&lt;/code&gt; means only HIGH findings block the build. Medium and low appear in the log but don't fail the step. Use &lt;code&gt;--fail-on medium&lt;/code&gt; for stricter enforcement.&lt;/p&gt;

&lt;p&gt;Available options for &lt;code&gt;--fail-on&lt;/code&gt;: &lt;code&gt;high&lt;/code&gt; (default), &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing infrawise.yaml for CI
&lt;/h2&gt;

&lt;p&gt;In local development, &lt;code&gt;infrawise start --claude&lt;/code&gt; generates the config and connects your editor. In CI, commit the config and pass credentials through environment variables:&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;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-service&lt;/span&gt;

&lt;span class="na"&gt;aws&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;

&lt;span class="na"&gt;dynamodb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;includeTables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Orders&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Users&lt;/span&gt;

&lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://infrawise_ro:${DB_PASSWORD}@host:5432/mydb&lt;/span&gt;

&lt;span class="na"&gt;lambda&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${DB_PASSWORD}&lt;/code&gt; substitution keeps credentials out of the committed config — infrawise expands environment variables at runtime. Set &lt;code&gt;DB_PASSWORD&lt;/code&gt; as a CI secret.&lt;/p&gt;

&lt;p&gt;For AWS access, infrawise is read-only. The minimum IAM policy it needs for DynamoDB is &lt;code&gt;dynamodb:ListTables&lt;/code&gt; and &lt;code&gt;dynamodb:DescribeTable&lt;/code&gt;. Create an IAM role for CI with just these permissions and configure OIDC trust for your GitHub Actions runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Blocks, What Warns, What Can Wait
&lt;/h2&gt;

&lt;p&gt;Not every finding warrants a hard block. A starting policy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block on HIGH&lt;/strong&gt;: Full table scans, hotspot patterns where multiple functions hammer the same partition key. These have immediate cost and availability impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warn on MEDIUM&lt;/strong&gt;: Tables with no GSIs queried by multiple functions, missing indexes on filterable columns in PostgreSQL. These degrade as data grows — log them and schedule remediation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log only for LOW&lt;/strong&gt;: Non-critical index suggestions, rarely-queried pattern gaps. Useful signal for the next sprint.&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;--fail-on high&lt;/code&gt;. Once the team has cleared existing HIGH findings, tighten to &lt;code&gt;--fail-on medium&lt;/code&gt;. The goal is catching regressions — new findings introduced by merged code — not blocking work on pre-existing debt.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;includeTables&lt;/code&gt; field in &lt;code&gt;infrawise.yaml&lt;/code&gt; lets you scope analysis to specific tables. Roll out the gate incrementally: start with the tables your AI assistant touches most.&lt;/p&gt;

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

&lt;p&gt;The table scan that paged your on-call engineer wasn't wrong code. It was a gap in tooling — your pipeline understood TypeScript but not DynamoDB. &lt;code&gt;infrawise check&lt;/code&gt; closes that gap by giving CI the same infrastructure context your AI assistant should have had when it wrote the query.&lt;/p&gt;

&lt;p&gt;One step in the pipeline. One committed config file. The build fails when generated code would fail in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated code passes code review and unit tests but can introduce DynamoDB full scans and missing index queries that only appear at scale&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infrawise check&lt;/code&gt; runs deterministic infrastructure analysis and exits non-zero on blocking findings — drop-in CI gate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--fail-on high|medium|low&lt;/code&gt; controls what severity blocks the build; &lt;code&gt;high&lt;/code&gt; is the default&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infrawise.yaml&lt;/code&gt; uses &lt;code&gt;${ENV_VAR}&lt;/code&gt; substitution so database passwords never need to be committed&lt;/li&gt;
&lt;li&gt;infrawise is read-only — minimum IAM for DynamoDB is &lt;code&gt;dynamodb:ListTables&lt;/code&gt; and &lt;code&gt;dynamodb:DescribeTable&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Your DB Is Still Red After Adding a Cache — Here's Why</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:21:17 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-db-is-still-red-after-adding-a-cache-heres-why-2e4e</link>
      <guid>https://dev.to/siddharth_pandey_27/your-db-is-still-red-after-adding-a-cache-heres-why-2e4e</guid>
      <description>&lt;p&gt;You deployed a cache in front of your database three weeks ago. The DB is still running at 90% utilization. Traffic doubled last month and you're wondering if the cache is doing anything at all.&lt;/p&gt;

&lt;p&gt;It is — just not as much as you expected, because cache hit rate is not something you configure. It emerges from two things: how much of your working set fits in memory, and how skewed your access patterns are.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://paperstack-app.vercel.app/" rel="noopener noreferrer"&gt;Paperstack&lt;/a&gt; is a free system design simulator that makes this visible. Sketch an architecture, press play, watch utilization numbers and node colors update live. The demo below walks through the cache problem using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hit Rate Isn't a Setting
&lt;/h2&gt;

&lt;p&gt;A cache absorbs reads by serving them from memory instead of forwarding them to the database. The fraction it absorbs — hit rate — depends on one thing: whether the data a request needs is in memory.&lt;/p&gt;

&lt;p&gt;Two variables determine that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Working-set vs memory.&lt;/strong&gt; If your active data is 100,000 keys and your cache holds 50,000, only half the requests can possibly hit — the rest miss and forward to the DB. Your cache isn't broken. It's undersized for the working set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access skew.&lt;/strong&gt; If 80% of requests hit 10% of keys (common in social or content workloads), a much smaller cache can achieve a high hit rate because the hot keys stay warm and rarely get evicted. Paperstack models this directly via the &lt;code&gt;skew&lt;/code&gt; parameter on the Cache node: with an LFU eviction policy, higher skew boosts hit rate beyond raw memory coverage. With LRU, skew gives no benefit — the eviction algorithm doesn't take access frequency into account, so cold pages get evicted as readily as hot ones.&lt;/p&gt;

&lt;p&gt;This is why you can't just set &lt;code&gt;hitRate: 0.9&lt;/code&gt; in the config panel — Paperstack doesn't expose hit rate as a field. You set &lt;code&gt;memory&lt;/code&gt; and &lt;code&gt;workingSet&lt;/code&gt;; hit rate is computed. If the simulation let you enter a hit rate directly, it would be lying to you about what your architecture actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comparative in Action
&lt;/h2&gt;

&lt;p&gt;Here's the experiment worth running in Paperstack: sketch &lt;code&gt;Traffic → App → Cache → Database&lt;/code&gt;. Run the simulation. Watch which nodes go red.&lt;/p&gt;

&lt;p&gt;With cache &lt;code&gt;memory&lt;/code&gt; smaller than &lt;code&gt;workingSet&lt;/code&gt;, most reads miss and forward to the DB. The DB stays red. The cache stays green — it has throughput headroom, it's just not absorbing much.&lt;/p&gt;

&lt;p&gt;Now increase the cache memory past the working-set size. The hit rate climbs. Fewer reads reach the DB. At some threshold the DB color shifts from red to orange to green — and a different node becomes the bottleneck. Maybe the App Server. Maybe the cache's own throughput cap.&lt;/p&gt;

&lt;p&gt;This is what Paperstack calls the comparative: when you change one variable, the bottleneck doesn't disappear — it &lt;em&gt;moves&lt;/em&gt;. That relocation is the lesson. Scaling the cache fixed your DB problem and revealed your next one.&lt;/p&gt;

&lt;p&gt;The inverse is just as instructive. Remove the Cache node and rerun. Watch the DB immediately redline. This is how you build intuition for what a cache is actually doing — not by reading about hit rates, but by watching the utilization delta before and after.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Write Policy Matters
&lt;/h2&gt;

&lt;p&gt;Paperstack exposes three write patterns on the Cache node: &lt;strong&gt;Cache-aside&lt;/strong&gt;, &lt;strong&gt;Write-through&lt;/strong&gt;, and &lt;strong&gt;Write-behind&lt;/strong&gt;. The choice affects both latency and what happens when you kill the cache node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache-aside&lt;/strong&gt; (the default) separates reads from writes entirely. Reads check the cache first; misses go to the DB. Writes bypass the cache and go directly to the DB. The cache is populated on read-miss, not on write. Kill the cache: reads start missing entirely, DB load spikes, but the write path was already going to DB — no disruption there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write-through&lt;/strong&gt; keeps the cache and DB in sync on every write. Writes pay the cache's latency &lt;em&gt;and&lt;/em&gt; the DB's latency on the write path, making writes more expensive than cache-aside. Kill the cache: reads fall through to DB, but every write was already reaching the DB, so nothing is lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write-behind&lt;/strong&gt; is where the kill scenario gets interesting. In this mode, the cache absorbs writes entirely — they never reach the DB during normal operation. Only read-misses reach the DB. The DB is effectively shielded from the write load.&lt;/p&gt;

&lt;p&gt;Kill the cache node in write-behind mode: Paperstack's &lt;code&gt;passThroughOnKill&lt;/code&gt; behavior makes the cache transparent — all traffic falls straight through. The DB suddenly receives the write workload that was never reaching it before. If the DB was sized assuming writes were handled by the cache, it may not have the &lt;code&gt;writeCap&lt;/code&gt; headroom to absorb the sudden change. The simulation shows this directly as DB utilization spiking and requests dropping.&lt;/p&gt;

&lt;p&gt;This failure mode is invisible on a static architecture diagram. The diagram shows cache → DB regardless of write policy. The simulation shows what breaks.&lt;/p&gt;

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

&lt;p&gt;The cache-doesn't-help problem is usually a mismatch between memory and working set, not a configuration error. Once hit rate is computed from real inputs rather than typed in, the DB utilization behavior makes sense.&lt;/p&gt;

&lt;p&gt;Paperstack makes the relationship between working-set size, cache memory, write policy, and DB utilization visible without deploying anything. Sketch the architecture, tune the numbers, kill nodes, and watch the bottleneck move. When the DB finally turns green, you know exactly why.&lt;/p&gt;

&lt;p&gt;Try it at &lt;a href="https://paperstack-app.vercel.app/" rel="noopener noreferrer"&gt;Paperstack&lt;/a&gt; — it runs in the browser, no account needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cache hit rate emerges from &lt;code&gt;memory&lt;/code&gt; vs &lt;code&gt;workingSet&lt;/code&gt; and access &lt;code&gt;skew&lt;/code&gt; — it's computed, not configured. Undersizing cache memory caps hit rate regardless of traffic volume.&lt;/li&gt;
&lt;li&gt;The comparative (change one variable, watch the bottleneck move) is how you build cache intuition: adding cache makes the DB green, revealing the next bottleneck.&lt;/li&gt;
&lt;li&gt;LFU eviction benefits high-skew workloads (popular keys stay warm); LRU does not — &lt;code&gt;skew&lt;/code&gt; only matters with the right eviction policy.&lt;/li&gt;
&lt;li&gt;Write-behind shields the DB from writes during normal operation; kill the cache and the DB suddenly receives the write load it was never sized for.&lt;/li&gt;
&lt;li&gt;Write-through and Cache-aside are safe to kill (writes were already reaching DB); write-behind changes the DB's workload profile on failure.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Your Reranker Isn't Helping Your RAG Pipeline (And How to Prove It)</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:57:40 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/why-your-reranker-isnt-helping-your-rag-pipeline-and-how-to-prove-it-5a4h</link>
      <guid>https://dev.to/siddharth_pandey_27/why-your-reranker-isnt-helping-your-rag-pipeline-and-how-to-prove-it-5a4h</guid>
      <description>&lt;p&gt;You add a cross-encoder reranker to your RAG pipeline, measure answer quality on a test set, see a marginal improvement on 3 of 8 questions, and ship it. Six weeks later your p99 retrieval latency has climbed 200ms per query and you're paying Cohere API costs on every call. Nobody has revisited the decision because there's no data to revisit. The reranker is in the pipeline now. It probably helps.&lt;/p&gt;

&lt;p&gt;That "probably" is the problem. &lt;a href="https://github.com/Sidd27/ragscope" rel="noopener noreferrer"&gt;RAGScope&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/ragscope" rel="noopener noreferrer"&gt;npm&lt;/a&gt; gives you a per-query metric that tells you exactly whether your reranker is earning its cost — or actively making things worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Reranker Gain" Actually Measures
&lt;/h2&gt;

&lt;p&gt;When your RAG app runs a query, it emits OpenTelemetry spans: a retrieval span carrying chunk IDs, scores, and content; an optional reranking span; and an LLM span containing the full prompt text. RAGScope receives these via OTLP on port 4321 and analyzes the full trace end-to-end.&lt;/p&gt;

&lt;p&gt;The rerank-gain metric answers one question: did the reranker pull the chunks the LLM actually used toward the top of the list? RAGScope compares each chunk's retrieval rank (its position before reranking) against its reranked rank (its position after), then measures the average rank improvement of the chunks that ended up in the LLM's prompt. A chunk that was retrieved 8th but reranked to 2nd and appeared in the prompt counts as a large positive gain. A chunk that was retrieved 2nd, reranked to 9th, and got dropped from the prompt counts as a loss.&lt;/p&gt;

&lt;p&gt;The metric only appears in the score when the trace contains a reranker span. When it does, the weights renormalize automatically — precision drops from 40% to 35%, efficiency from 30% to 25%, and rerank-gain takes a 15% slice alongside uniqueness at 15% and coverage at 10%. Traces without a reranker span score exactly as before, so you can compare directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Signal — What Good and Bad Look Like
&lt;/h2&gt;

&lt;p&gt;A reranker earning its cost 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;│  ✓  rerank-gain  88  █████████░  used chunks promoted avg +3.0 ranks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chunks the LLM actually used were promoted an average of 3 positions by the reranker. That means the reranker is doing its job: surfacing the relevant material higher so it reaches the prompt and lands near the edges where the LLM attends most.&lt;/p&gt;

&lt;p&gt;A reranker not earning its cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│  ✗  rerank-gain  25  ███░░░░░░░  used chunks demoted avg -2.0 ranks
│  → Reranker is not surfacing the chunks the LLM actually uses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chunks the LLM used were demoted by the reranker. They reached the prompt despite the reranker, not because of it. The reranker added latency and cost and then moved the useful material further back in the queue.&lt;/p&gt;

&lt;p&gt;The key insight is that RAGScope measures gain on the chunks that actually appeared in the LLM's prompt — not on the full ranked list. A reranker can shuffle 10 results around impressively while consistently pushing the 3 chunks the LLM uses toward position 7, 8, and 9. That's not a reranker working; that's a reranker actively degrading retrieval for this query type.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do When the Reranker Is Hurting
&lt;/h2&gt;

&lt;p&gt;The first step is query segmentation. RAGScope scores every query your pipeline processes individually. Run it for a day and you'll have a distribution of rerank-gain scores broken down by query type. If your reranker earns a score of 80+ on factual lookups but consistently scores below 30 on comparison queries, you have a model-query-type mismatch, not a broken reranker.&lt;/p&gt;

&lt;p&gt;The second step is checking your reranker's training domain. Cross-encoders trained on MS MARCO work well for web-search-style queries. If your documents are internal API docs, legal contracts, or medical literature, the reranker may be applying a relevance signal that's semantically misaligned with your content. A low rerank-gain score on a specific document type is a strong signal to evaluate a domain-specific model.&lt;/p&gt;

&lt;p&gt;If the rerank-gain score is consistently low across query types, the simplest intervention is removing the reranker entirely and routing that latency budget into a higher TOP_K with tighter similarity thresholds. RAGScope's precision metric will tell you immediately whether that trade works: if precision improves and efficiency holds, you've recovered the latency without losing quality.&lt;/p&gt;

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

&lt;p&gt;A reranker is not always additive. It introduces latency, API cost, and an additional failure mode on every query — and most teams have no per-query signal to determine whether it's paying for itself. Aggregate quality metrics on a test set don't expose query-level degradation.&lt;/p&gt;

&lt;p&gt;RAGScope's rerank-gain metric gives you that signal query by query, live in your terminal as the pipeline runs. Start it with &lt;code&gt;npx ragscope start&lt;/code&gt;, add OTLP instrumentation to your retrieval and reranker calls, and you'll know within the first few queries whether the reranker is earning its place in your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/ragscope" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/ragscope" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;rerank-gain measures the average rank improvement of the chunks the LLM actually used — not the full ranked list, which can mask per-query degradation.&lt;/li&gt;
&lt;li&gt;The metric only appears when the trace contains a reranker span; weights renormalize automatically (precision 35%, efficiency 25%, rerank-gain 15%, uniqueness 15%, coverage 10%).&lt;/li&gt;
&lt;li&gt;A reranker with consistently negative rerank-gain is demoting the chunks the LLM uses — adding cost and latency for a net-negative retrieval outcome.&lt;/li&gt;
&lt;li&gt;Query segmentation reveals whether the reranker works for some query types but not others, pointing to model-query-type mismatch.&lt;/li&gt;
&lt;li&gt;If rerank-gain is consistently low across query types, removing the reranker and increasing TOP_K is often a better trade — RAGScope's precision score will validate it immediately.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>opensource</category>
      <category>llm</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Fix N+1 Trigger Patterns Where Lambda Functions Hammer the Same DynamoDB Partition Key</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sat, 20 Jun 2026 18:15:52 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/fix-n1-trigger-patterns-where-lambda-functions-hammer-the-same-dynamodb-partition-key-1m60</link>
      <guid>https://dev.to/siddharth_pandey_27/fix-n1-trigger-patterns-where-lambda-functions-hammer-the-same-dynamodb-partition-key-1m60</guid>
      <description>&lt;p&gt;You add a sixth Lambda trigger to your &lt;code&gt;OrderEvents&lt;/code&gt; table, deploy it, and within 20 minutes your SLA dashboard goes red. Latency on order writes jumps from 4ms to 40ms. The function itself is fine. The table is fine. The problem is that five other Lambdas are already hitting the same partition key on every write, and you just made it six. DynamoDB's internal partition throttling doesn't care that each function looks clean in isolation.&lt;/p&gt;

&lt;p&gt;This is an N+1 trigger problem, and your AI coding assistant cannot catch it. Not because it lacks intelligence, but because the fact that five Lambdas already target that table lives in your AWS account and your full codebase — not in the file your assistant has open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the LLM Can't See the Pattern
&lt;/h2&gt;

&lt;p&gt;When you ask Claude to write a new order processing Lambda, it reads the file you have open and generates code that looks correct — because in the context of that one file, it is correct. It doesn't know about &lt;code&gt;ProcessRefundsLambda&lt;/code&gt;, &lt;code&gt;NotifyFulfillmentLambda&lt;/code&gt;, &lt;code&gt;SyncInventoryLambda&lt;/code&gt;, &lt;code&gt;UpdateAnalyticsLambda&lt;/code&gt;, and &lt;code&gt;AuditTrailLambda&lt;/code&gt;, all of which you wrote in previous sprints and which all write to the &lt;code&gt;Orders&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;This is a category of failure that model quality doesn't fix. A better model produces a more fluent explanation for why your latency spiked. The fact that five functions converge on the same table is a lookup, not a prediction. The source of truth is a combination of your code (which functions exist) and your infrastructure (what they access).&lt;/p&gt;

&lt;p&gt;Infrawise draws that boundary explicitly. It extracts the answer from your code using AST parsing and from your infrastructure using API calls, then hands that graph to the model as structured context — it never generates the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Infrawise Traces Trigger Chains to the Same Table
&lt;/h2&gt;

&lt;p&gt;When Infrawise scans your repository, it uses &lt;a href="https://ts-morph.com" rel="noopener noreferrer"&gt;ts-morph&lt;/a&gt; to walk every &lt;code&gt;CallExpression&lt;/code&gt; in every source file. It's not searching for the string "DynamoDB" — it matches call structure against a known set of SDK patterns in a &lt;code&gt;DYNAMO_OPERATIONS&lt;/code&gt; set: both v2 method names (&lt;code&gt;getItem&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;putItem&lt;/code&gt;, &lt;code&gt;updateItem&lt;/code&gt;, &lt;code&gt;deleteItem&lt;/code&gt;, &lt;code&gt;batchWriteItem&lt;/code&gt;) and v3 command classes (&lt;code&gt;QueryCommand&lt;/code&gt;, &lt;code&gt;PutItemCommand&lt;/code&gt;, &lt;code&gt;UpdateItemCommand&lt;/code&gt;, &lt;code&gt;DeleteItemCommand&lt;/code&gt;). Each matched call becomes an extracted operation: this function performs this operation against this table.&lt;/p&gt;

&lt;p&gt;That list feeds into a &lt;code&gt;SystemGraph&lt;/code&gt;. Nodes represent tables, functions, indexes, queues, and topics. Edges represent query, scan, and write relationships. The graph is what makes the N+1 pattern visible: not just "six functions exist" and "a table exists," but "six functions all write to &lt;code&gt;Orders&lt;/code&gt; with no distribution across paths."&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HotPartitionAnalyzer&lt;/code&gt; walks the graph and fires when a table receives five or more distinct access edges from separate code paths. The threshold is configurable per-table via &lt;code&gt;hotPartitionThresholds&lt;/code&gt; in &lt;code&gt;infrawise.yaml&lt;/code&gt; — Issue #57 resolved false positives on high fan-in systems by making this a per-table setting rather than a single global value. A finding looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Medium severity
Potential hot partition detected on DynamoDB table "Orders"
  Table "Orders" is accessed by 6 distinct code paths, which may create
  hot partition issues at scale. High access concentration on the same
  partition key can throttle requests.
  Recommendation: Consider adding a random suffix or timestamp to partition
  keys (write sharding). Use DynamoDB DAX for read-heavy workloads.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs deterministically. Feed it the same graph, get the same findings. There's no sampling temperature involved.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;infrawise check --fail-on medium&lt;/code&gt; command gates CI on this finding. Since &lt;code&gt;HotPartitionAnalyzer&lt;/code&gt; emits medium severity, you need &lt;code&gt;--fail-on medium&lt;/code&gt; (the default &lt;code&gt;--fail-on high&lt;/code&gt; won't catch it). When violations are found, &lt;code&gt;infrawise check&lt;/code&gt; exits with code 1 — your build fails before the sixth Lambda merges, and the engineer who wrote it sees the finding in the PR, not on a latency dashboard at 11pm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing It — Restructuring the Key or Sharding the Access Pattern
&lt;/h2&gt;

&lt;p&gt;Once Infrawise surfaces the pattern, you have two practical options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write sharding&lt;/strong&gt; adds a random suffix to the partition key — distributing writes across logical partitions. Reads require scatter-gather or a deterministic suffix derived from the order ID. This is the right choice when all six functions are pure writers and reads are handled by a separate query path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access pattern separation&lt;/strong&gt; restructures which functions need direct table access at all. If &lt;code&gt;SyncInventoryLambda&lt;/code&gt; and &lt;code&gt;UpdateAnalyticsLambda&lt;/code&gt; are consuming state that flows through the &lt;code&gt;Orders&lt;/code&gt; table, they shouldn't write to it directly — they should react to a DynamoDB stream and write to their own tables. The fan-in often exists because multiple services treat the same source-of-truth table as a synchronization point when they should be downstream consumers.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;analyze_function&lt;/code&gt; tool helps here. Point it at any function and it traces the full access path: which tables the function reads and writes, which indexes it uses, what event shapes trigger it, and what queues or topics it publishes to. That trace makes it clear which functions can be moved to stream consumption and which genuinely need direct write access.&lt;/p&gt;

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

&lt;p&gt;The N+1 trigger problem is invisible to any tool that works only from your open files. It's not a reasoning failure — no amount of context about a single Lambda reveals that five others already saturate the same table. That fact lives in the intersection of your code and your infrastructure.&lt;/p&gt;

&lt;p&gt;Infrawise puts that intersection in a graph, runs deterministic analyzers over it, and surfaces the finding before it becomes a production incident. The model's job is to decide what to do — restructure the key, introduce a stream, separate the access pattern. The detection is never generated; it's extracted.&lt;/p&gt;

&lt;p&gt;If your AI assistant is writing Lambda functions against DynamoDB, give it the access graph first: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A hot partition problem requires knowing how many code paths hit the same table — that fact lives in your AWS account and your full codebase, not in the file your AI assistant has open.&lt;/li&gt;
&lt;li&gt;Infrawise's &lt;code&gt;HotPartitionAnalyzer&lt;/code&gt; counts distinct code paths hitting each DynamoDB table and fires at a configurable threshold, with per-table overrides via &lt;code&gt;hotPartitionThresholds&lt;/code&gt; in &lt;code&gt;infrawise.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Hot partition findings emit &lt;strong&gt;medium&lt;/strong&gt; severity; use &lt;code&gt;infrawise check --fail-on medium&lt;/code&gt; to gate CI builds on them (the default &lt;code&gt;--fail-on high&lt;/code&gt; won't catch them).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyze_function&lt;/code&gt; traces the full access path for any function — tables, indexes, event shapes, queues — making it easy to separate writers from downstream consumers.&lt;/li&gt;
&lt;li&gt;Write sharding and event-stream separation are the two practical fixes; which one to pick depends on whether converging functions genuinely need to write or are just consuming state.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>Stop Paying For Retrieval Latency On Chunks You Never Use In The Prompt</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:12:31 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/stop-paying-for-retrieval-latency-on-chunks-you-never-use-in-the-prompt-4kh5</link>
      <guid>https://dev.to/siddharth_pandey_27/stop-paying-for-retrieval-latency-on-chunks-you-never-use-in-the-prompt-4kh5</guid>
      <description>&lt;h2&gt;
  
  
  Your pipeline fetched 10 chunks. Your LLM saw 3.
&lt;/h2&gt;

&lt;p&gt;You set &lt;code&gt;TOP_K=10&lt;/code&gt; on your vector store. Ten candidate chunks means more signal for the model — that's the logic. Then you run &lt;code&gt;npx ragscope&lt;/code&gt; and the audit prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  WARN   51/100  █████░░░░░  my-rag-service
  │  "what are the pricing tiers?"
  │
  │  ✗  precision     30  ███░░░░░░░  3/10 chunks used
  │  ✗  efficiency    30  ███░░░░░░░  70% tokens wasted
  │  ✓  uniqueness   100  ██████████  chunks are distinct
  │  ✓  coverage     100  ██████████  all chunks scored
  │
  │  → Reduce TOP_K 10→3 (only 3 chunks reached LLM)
  │  → 70% of retrieved tokens never reached the LLM
  │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of ten chunks made it into the final prompt. You paid for ten round-trips to the vector store and seven went nowhere. The other seven were fetched, scored, and discarded somewhere between your retrieval step and your prompt assembly code.&lt;/p&gt;

&lt;p&gt;This is the gap &lt;a href="https://github.com/Sidd27/ragscope" rel="noopener noreferrer"&gt;ragscope&lt;/a&gt; (&lt;a href="https://www.npmjs.com/package/ragscope" rel="noopener noreferrer"&gt;npm&lt;/a&gt;) was built to surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Chunks Disappear
&lt;/h2&gt;

&lt;p&gt;Retrieval and prompt assembly are separate steps, but most teams treat them as one. The gap is where the waste lives.&lt;/p&gt;

&lt;p&gt;A typical RAG pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Embed the query&lt;/li&gt;
&lt;li&gt;Fetch &lt;code&gt;TOP_K&lt;/code&gt; chunks from the vector store&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;(Optional)&lt;/em&gt; Rerank&lt;/li&gt;
&lt;li&gt;Assemble the prompt — filter by score threshold, truncate to fit the context window&lt;/li&gt;
&lt;li&gt;Send to the LLM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 is where chunks disappear. Post-retrieval filtering — a score threshold, a hard token budget, a deduplication pass — silently drops chunks you already paid to retrieve. If your prompt assembly filters out anything below a certain confidence score and your vector store returns six chunks that don't clear it, those six fetches were wasted. The network round-trip still happened. The latency still accumulated.&lt;/p&gt;

&lt;p&gt;The problem compounds over time. &lt;code&gt;TOP_K=10&lt;/code&gt; gets set as a safe default, the pipeline ships, and the setting never gets revisited. LLM eval scores look fine because the three chunks that do reach the prompt are the right ones. The waste is invisible in your evals — it only shows up in latency and cost.&lt;/p&gt;

&lt;p&gt;Vector stores typically scale retrieval latency with &lt;code&gt;TOP_K&lt;/code&gt;. Fetching ten results takes measurably longer than fetching three, especially at tail latencies. When seven of those ten are discarded before the prompt, you're paying that latency premium on every query for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ragscope Measures &lt;code&gt;precision&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;ragscope runs as a local server (default port 4321) and receives spans from your pipeline via OTLP at &lt;code&gt;http://localhost:4321/v1/traces&lt;/code&gt;. No changes to your RAG code needed — configure your OpenTelemetry exporter to point there.&lt;/p&gt;

&lt;p&gt;When both a retrieval span and an LLM span arrive for the same trace, ragscope compares them. The key field is &lt;code&gt;inContext&lt;/code&gt; on each &lt;code&gt;RagChunk&lt;/code&gt;. It inspects the full text of the LLM span's assembled prompt and checks whether each retrieved chunk's content appears in it — positionally, by string match. A chunk either appears in the prompt or it doesn't. No LLM, no heuristics, no sampling.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;precision&lt;/code&gt; starts as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base = (chunks where inContext is true / total retrieved chunks) × 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one additional penalty: if high-retrieval-rank chunks land in the middle of a long context window — the zone where LLM attention typically falls off — ragscope subtracts 12 points per buried chunk, capped at 36. This is the lost-in-the-middle detection. A pipeline where 3/10 chunks are used but two of those three are buried mid-context will score lower than one where the same 3 chunks sit at the prompt edges.&lt;/p&gt;

&lt;p&gt;When the base falls below 60, ragscope generates a specific recommendation with the exact numbers: &lt;code&gt;Reduce TOP_K 10→3 (only 3 chunks reached LLM)&lt;/code&gt;. It prints this directly below the score bars, in the terminal, at the time the trace arrives — not in a cloud dashboard after a deploy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;efficiency&lt;/code&gt; is a related metric: the fraction of retrieved tokens that reached the LLM. If &lt;code&gt;precision&lt;/code&gt; is 30 and your chunks are roughly uniform in size, &lt;code&gt;efficiency&lt;/code&gt; will also be around 30 — meaning 70% of the tokens you transferred from the vector store never appeared in a prompt. That shows up in retrieval latency and, depending on your pipeline, in processing time downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning TOP_K Based on the Audit
&lt;/h2&gt;

&lt;p&gt;Once you have &lt;code&gt;precision&lt;/code&gt; data, tuning is mechanical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow the recommendation.&lt;/strong&gt; ragscope prints &lt;code&gt;Reduce TOP_K 10→3&lt;/code&gt; because &lt;code&gt;Math.max(used_chunks, 3)&lt;/code&gt; gives you the minimum viable retrieval count with a floor of 3. Start there and re-run the audit against a few real queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check &lt;code&gt;efficiency&lt;/code&gt; alongside.&lt;/strong&gt; Low &lt;code&gt;efficiency&lt;/code&gt; paired with low &lt;code&gt;precision&lt;/code&gt; means your unused chunks are large — a chunking strategy problem as much as a &lt;code&gt;TOP_K&lt;/code&gt; problem. If &lt;code&gt;efficiency&lt;/code&gt; is high but &lt;code&gt;precision&lt;/code&gt; is low, your unused chunks are small and the token cost is modest; fixing &lt;code&gt;TOP_K&lt;/code&gt; will mostly recover the latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check &lt;code&gt;uniqueness&lt;/code&gt; too.&lt;/strong&gt; If the chunks that do reach your prompt include near-duplicates, the &lt;code&gt;uniqueness&lt;/code&gt; metric will flag them. Two near-duplicate chunks in the prompt means one is redundant regardless of your &lt;code&gt;TOP_K&lt;/code&gt; setting — that's a deduplication-at-ingest problem, not a retrieval count problem. ragscope computes overlap between chunk pairs and surfaces high-overlap counts in the audit output.&lt;/p&gt;

&lt;p&gt;The typical path after a low-precision audit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit shows &lt;code&gt;3/10 chunks used&lt;/code&gt; — follow the &lt;code&gt;Reduce TOP_K 10→3&lt;/code&gt; recommendation&lt;/li&gt;
&lt;li&gt;Re-run against real queries — &lt;code&gt;precision&lt;/code&gt; should move above 75 (PASS)&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;uniqueness&lt;/code&gt; also flagged duplicates, fix chunking and re-run&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;efficiency&lt;/code&gt; is still low after fixing &lt;code&gt;TOP_K&lt;/code&gt;, chunks may be too large for the token budget&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Going from &lt;code&gt;TOP_K=10&lt;/code&gt; to &lt;code&gt;TOP_K=3&lt;/code&gt; on a pipeline that was only ever using 3 chunks means 70% fewer vector store round-trips on every query. No model changes, no prompt rewriting, no reranker added.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Number You Should Check First
&lt;/h2&gt;

&lt;p&gt;The assumption that more retrieval means better answers is almost never validated against what the model actually sees. &lt;code&gt;TOP_K&lt;/code&gt; defaults get set once and forgotten. Eval scores stay flat because the chunks that do reach the LLM are the right ones — the waste doesn't affect quality, just cost and latency.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx ragscope&lt;/code&gt; gives you &lt;code&gt;precision&lt;/code&gt;, &lt;code&gt;efficiency&lt;/code&gt;, and &lt;code&gt;uniqueness&lt;/code&gt; in the time it takes to run a dev command. If you haven't checked what fraction of your retrieved chunks survive to the prompt, that's the number to look at first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/ragscope" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/ragscope" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;precision&lt;/code&gt; measures what fraction of retrieved chunks appear in the final LLM prompt — anything below 60/100 triggers a &lt;code&gt;Reduce TOP_K&lt;/code&gt; recommendation with exact numbers&lt;/li&gt;
&lt;li&gt;Vector store round-trips typically scale with &lt;code&gt;TOP_K&lt;/code&gt;; fetching chunks you discard is pure overhead with no quality benefit&lt;/li&gt;
&lt;li&gt;ragscope detects in-prompt chunk presence by matching content against the OTLP LLM span's prompt text — deterministic, no LLM needed&lt;/li&gt;
&lt;li&gt;Low &lt;code&gt;efficiency&lt;/code&gt; paired with low &lt;code&gt;precision&lt;/code&gt; points to a chunking strategy problem, not just a &lt;code&gt;TOP_K&lt;/code&gt; problem&lt;/li&gt;
&lt;li&gt;Low &lt;code&gt;uniqueness&lt;/code&gt; means near-duplicate chunks in the prompt — fix at ingest, not by adjusting retrieval count&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>opensource</category>
      <category>llm</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why Infrawise Uses Deterministic Analysis Instead of an LLM</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sat, 13 Jun 2026 10:58:44 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/why-infrawise-uses-deterministic-analysis-instead-of-an-llm-15fk</link>
      <guid>https://dev.to/siddharth_pandey_27/why-infrawise-uses-deterministic-analysis-instead-of-an-llm-15fk</guid>
      <description>&lt;p&gt;Ask your AI coding assistant which Global Secondary Indexes exist on your &lt;code&gt;Orders&lt;/code&gt; table. It will read your repository, find a few &lt;code&gt;QueryCommand&lt;/code&gt; calls, and answer — fluent, specific, and confident. It also has no way to know. GSI definitions live in AWS, not in your source files. The model isn't lying; the fact simply isn't available to it, so it generates the most statistically plausible substitute and delivers it in the same tone it uses for things it actually knows.&lt;/p&gt;

&lt;p&gt;That failure mode is why &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; (&lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;) — an MCP server that gives AI coding assistants infrastructure context — contains no LLM calls at all. Every answer it serves comes from AST parsing, schema introspection, rule-based analyzers, and graph correlation. The LLM is only ever a consumer of that context, never a producer of it. This post is about why that boundary exists, and what it looks like in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure questions are lookups, not generation
&lt;/h2&gt;

&lt;p&gt;There are two kinds of questions you can ask a tool. "How should I model sessions in DynamoDB?" is a judgment question — many defensible answers, context matters, an LLM is genuinely useful. "Does the &lt;code&gt;Sessions&lt;/code&gt; table have a GSI on &lt;code&gt;userId&lt;/code&gt;?" is a fact question. It has exactly one correct answer, and that answer is sitting in a &lt;code&gt;DescribeTable&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;When you route a fact question through a generative model, you convert a lookup with a perfectly accurate source into a prediction with an unknown error rate. The motivating examples in the Infrawise README are all of this shape: an assistant suggesting a &lt;code&gt;.scan()&lt;/code&gt; on an &lt;code&gt;Orders&lt;/code&gt; table with 50 million rows, recommending a GSI on &lt;code&gt;status&lt;/code&gt; that already exists, or not noticing that five functions are already hammering the same partition key. None of these are reasoning failures. They are missing-fact failures, and no amount of model quality fixes them — a better model just produces a more convincing wrong answer.&lt;/p&gt;

&lt;p&gt;So Infrawise draws a hard line: facts get extracted deterministically, and the model receives them through MCP tool calls instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What deterministic extraction looks like
&lt;/h2&gt;

&lt;p&gt;Infrawise builds its picture of your system from three sources, none of which involve a model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your code, through the compiler's eyes.&lt;/strong&gt; &lt;code&gt;scanRepository()&lt;/code&gt; in &lt;code&gt;src/context/index.ts&lt;/code&gt; loads the repo with &lt;a href="https://ts-morph.com" rel="noopener noreferrer"&gt;ts-morph&lt;/a&gt; — using your own &lt;code&gt;tsconfig.json&lt;/code&gt; when one exists — and walks every &lt;code&gt;CallExpression&lt;/code&gt; node in every source file. It doesn't regex for the word "scan". It matches call structure against known client patterns: a &lt;code&gt;DYNAMO_OPERATIONS&lt;/code&gt; set covering both SDK v2 method names (&lt;code&gt;query&lt;/code&gt;, &lt;code&gt;scan&lt;/code&gt;, &lt;code&gt;getItem&lt;/code&gt;) and SDK v3 command classes (&lt;code&gt;ScanCommand&lt;/code&gt;, &lt;code&gt;QueryCommand&lt;/code&gt;, &lt;code&gt;PutItemCommand&lt;/code&gt;), &lt;code&gt;query&lt;/code&gt;/&lt;code&gt;execute&lt;/code&gt;/&lt;code&gt;exec&lt;/code&gt; calls on PostgreSQL and MySQL clients, and MongoDB collection methods — where &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;aggregate&lt;/code&gt; are classified as scan-type operations and the rest as queries. The output is a list of extracted operations: this function performs this operation type against this table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your databases, through their own catalogs.&lt;/strong&gt; The PostgreSQL adapter doesn't ask a model to summarize your schema. It runs the same introspection queries you would run by hand — &lt;code&gt;information_schema.tables&lt;/code&gt; for tables, &lt;code&gt;information_schema.columns&lt;/code&gt; for columns, &lt;code&gt;pg_indexes&lt;/code&gt; for indexes, and the constraint tables for keys. The docs recommend pointing it at a dedicated read-only user, and the DynamoDB side needs only &lt;code&gt;dynamodb:ListTables&lt;/code&gt; and &lt;code&gt;dynamodb:DescribeTable&lt;/code&gt; permissions. What comes back isn't a description of your schema; it &lt;em&gt;is&lt;/em&gt; your schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlation, through a graph.&lt;/strong&gt; Both streams land in a &lt;code&gt;SystemGraph&lt;/code&gt;: typed nodes for tables, functions, indexes, queues, topics, lambdas, buckets, secrets, parameters, and log groups, connected by typed edges like &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;scan&lt;/code&gt;, and &lt;code&gt;uses_index&lt;/code&gt;. The graph is what turns two boring fact lists into something an analyzer can interrogate — not just "this table exists" and "this function scans something," but "&lt;code&gt;listAllOrders()&lt;/code&gt; scans the &lt;code&gt;Orders&lt;/code&gt; table, and no index covers that access."&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules, not vibes
&lt;/h2&gt;

&lt;p&gt;The analysis layer is where most tools would reach for a model — and where Infrawise stays deterministic. The analyzer index exports 27 rule classes covering DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, S3, Lambda, RDS, secrets, log retention, and Terraform drift. Each one is an ordinary class with an &lt;code&gt;analyze(graph)&lt;/code&gt; method that walks the graph and emits findings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FullTableScanAnalyzer&lt;/code&gt; follows scan-type edges to DynamoDB table nodes and emits a high-severity finding naming the table and every calling function. &lt;code&gt;MissingGSIAnalyzer&lt;/code&gt; flags tables that receive query edges but have no &lt;code&gt;uses_index&lt;/code&gt; edge — medium severity, because it might be intentional. &lt;code&gt;HotPartitionAnalyzer&lt;/code&gt; fires when a table is accessed by five or more distinct code paths (the threshold is a constructor parameter, defaulting to 5).&lt;/p&gt;

&lt;p&gt;Two properties fall out of this design that a model can't give you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Findings are testable.&lt;/strong&gt; Every analyzer is a pure function of the graph. Feed it a fixture, assert on the output, done. There's no eval harness, no sampling temperature, no "run it three times and hope." If &lt;code&gt;FullTableScanAnalyzer&lt;/code&gt; regresses, a unit test catches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failures are contained and honest.&lt;/strong&gt; &lt;code&gt;runAllAnalyzers()&lt;/code&gt; wraps each analyzer in its own try/catch — one analyzer crashing logs a warning while the rest keep running. The combined findings are then sorted by a fixed severity order: &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, and notably &lt;code&gt;verify&lt;/code&gt; — a severity that exists precisely so a deterministic system can say "I detected a pattern but can't confirm the intent" instead of bluffing. An LLM has no equivalent of &lt;code&gt;verify&lt;/code&gt;; everything it says arrives with the same confident fluency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM is the consumer, not the analyst
&lt;/h2&gt;

&lt;p&gt;None of this means LLMs are useless here. It means they belong at a specific layer. Infrawise exposes the graph and findings through 15 MCP tools: &lt;code&gt;get_infra_overview&lt;/code&gt; for a quick snapshot, &lt;code&gt;analyze_function&lt;/code&gt; to trace a single function's tables, queues, secrets, and trigger event shapes, &lt;code&gt;suggest_gsi&lt;/code&gt; to generate a ready-to-use GSI definition for a table and attribute, &lt;code&gt;postgres_index_suggestions&lt;/code&gt; for index advice, and so on. The assistant decides &lt;em&gt;when&lt;/em&gt; to ask and &lt;em&gt;what to do&lt;/em&gt; with the answer. It never produces the answer.&lt;/p&gt;

&lt;p&gt;The plumbing is deliberately boring: analysis results are cached as JSON files under &lt;code&gt;.infrawise/cache&lt;/code&gt;, and the &lt;code&gt;infrawise stdio&lt;/code&gt; process your editor spawns re-runs the analysis when the cache is older than 24 hours. Run &lt;code&gt;infrawise start --claude&lt;/code&gt; once and it writes &lt;code&gt;.mcp.json&lt;/code&gt; so Claude Code reconnects automatically on every future launch.&lt;/p&gt;

&lt;p&gt;This division of labor generalizes well beyond one project. The model handles intent ("the user wants this query to be cheaper") and synthesis ("given these findings, here's the migration plan"). The deterministic layer handles every claim that has a ground truth. The test is simple: if asking the same question twice should yield the same answer, don't generate the answer — look it up.&lt;/p&gt;

&lt;p&gt;If your AI assistant writes code against AWS or a database, give it facts instead of letting it guess: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A fact question routed through a generative model turns a lookup with a perfect source into a prediction with an unknown error rate. Route facts around the model, not through it.&lt;/li&gt;
&lt;li&gt;AST-level extraction (ts-morph walking &lt;code&gt;CallExpression&lt;/code&gt; nodes) catches what schema introspection alone can't see — which function scans which table, and how.&lt;/li&gt;
&lt;li&gt;Rule-based analyzers are unit-testable and fail loudly per rule; model-based analysis is neither.&lt;/li&gt;
&lt;li&gt;A deterministic system can emit a &lt;code&gt;verify&lt;/code&gt; severity when it isn't sure. A model can't reliably tell you when it's guessing.&lt;/li&gt;
&lt;li&gt;Put the LLM at the boundary: it consumes structured facts over MCP and decides what to do next — it never gets to invent the facts.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>Give Your AI Assistant Infrastructure Eyes Before It Writes Another Query</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Tue, 09 Jun 2026 18:18:02 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/give-your-ai-assistant-infrastructure-eyes-before-it-writes-another-query-p0o</link>
      <guid>https://dev.to/siddharth_pandey_27/give-your-ai-assistant-infrastructure-eyes-before-it-writes-another-query-p0o</guid>
      <description>&lt;p&gt;You asked Claude Code to add pagination to your order history endpoint. It generated a clean function — &lt;code&gt;listOrdersByUser()&lt;/code&gt; — using a DynamoDB &lt;code&gt;Scan&lt;/code&gt; with a &lt;code&gt;Limit&lt;/code&gt; parameter. It compiled. Tests passed. You shipped it.&lt;/p&gt;

&lt;p&gt;Three days later your AWS bill had a line item you didn't recognize: 47 million read capacity units consumed in 72 hours. The Orders table has 50M rows. &lt;code&gt;Scan&lt;/code&gt; reads every one of them regardless of &lt;code&gt;Limit&lt;/code&gt; — &lt;code&gt;Limit&lt;/code&gt; only controls how many results come back, not how many items DynamoDB reads.&lt;/p&gt;

&lt;p&gt;Claude Code didn't know your table had 50M rows. It didn't know you had a GSI on &lt;code&gt;userId&lt;/code&gt;. It guessed, and the guess was expensive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;infrawise&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Assistants Don't Know About Your Infrastructure
&lt;/h2&gt;

&lt;p&gt;AI coding assistants read your source files. They understand function signatures, TypeScript types, and import chains. What they cannot see is the infrastructure those functions run against.&lt;/p&gt;

&lt;p&gt;When Claude Code looks at a file that calls &lt;code&gt;dynamoClient.scan({ TableName: "Orders" })&lt;/code&gt;, it has no idea that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Orders table has 50M items&lt;/li&gt;
&lt;li&gt;There is already a GSI named &lt;code&gt;userId-index&lt;/code&gt; on the &lt;code&gt;userId&lt;/code&gt; attribute&lt;/li&gt;
&lt;li&gt;Three other functions are already using &lt;code&gt;Query&lt;/code&gt; against that same GSI&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Sessions&lt;/code&gt; table is accessed by 6 separate code paths, making it a hot partition candidate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that context, the assistant fills the gap with generic patterns. It recommends &lt;code&gt;Scan&lt;/code&gt; because it has no reason not to. It suggests adding a GSI on &lt;code&gt;status&lt;/code&gt; because it doesn't know one exists. It writes &lt;code&gt;SELECT *&lt;/code&gt; because it has no idea which columns are expensive to pull.&lt;/p&gt;

&lt;p&gt;This isn't a bug in the model. It's a missing input. The model was never given your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When infrawise Is in the Loop
&lt;/h2&gt;

&lt;p&gt;infrawise statically analyzes your codebase, your DynamoDB tables, and your PostgreSQL schemas, then exposes that context to your editor through MCP. Claude Code gets 15 tools that answer questions like: which tables exist, what are their partition keys and sort keys, which GSIs are already defined, which functions are already scanning, and which patterns are flagged as high severity.&lt;/p&gt;

&lt;p&gt;The difference in output is concrete. Here's what infrawise surfaces before any code gets written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Findings  3 total

  1.  HIGH   Full table scan detected on DynamoDB table "Orders"
             listAllOrders() scans without any filter — reads every item in the table.
             → Replace Scan with Query using a partition key or add a GSI.

  2.  MED    PostgreSQL table "users" has no index on column "email"
             Filtering on "email" causes sequential scans.
             → CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

  3.  MED    DynamoDB table "Sessions" accessed by 6 distinct code paths
             Hot partition risk — multiple functions hammer the same key.
             → Review access patterns and consider partition key design.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Claude Code has this context, its suggestions change. It knows &lt;code&gt;userId-index&lt;/code&gt; exists and recommends &lt;code&gt;Query&lt;/code&gt; against it instead of &lt;code&gt;Scan&lt;/code&gt;. It knows the &lt;code&gt;email&lt;/code&gt; column has no index and includes the exact &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; statement rather than a generic suggestion. It knows which functions are already hitting a partition hard before it adds another one.&lt;/p&gt;

&lt;p&gt;The recommendations become specific to your actual tables, not generic advice copied from documentation.&lt;/p&gt;

&lt;p&gt;infrawise does none of this with an LLM. The analysis is entirely deterministic: TypeScript AST parsing via ts-morph for the code graph, schema introspection for the database layer, rule-based analyzers for pattern detection, and graph correlation to connect code paths to tables. No model is involved in the analysis — models are only consumers of the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Up — infrawise start --claude
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; infrawise
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
infrawise start &lt;span class="nt"&gt;--claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first run, infrawise asks a few questions and generates &lt;code&gt;infrawise.yaml&lt;/code&gt;. It then scans your AWS services, databases, and codebase, writes &lt;code&gt;.mcp.json&lt;/code&gt; so your editor auto-connects, and opens Claude Code with all 15 MCP tools ready.&lt;/p&gt;

&lt;p&gt;Every session after that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No infrawise command needed. The editor manages the MCP connection. Analysis is cached for 24 hours; when the cache goes stale, &lt;code&gt;infrawise stdio&lt;/code&gt; — spawned automatically at session start — refreshes it. File changes are detected within the session and the code graph updates automatically without re-running AWS extraction.&lt;/p&gt;

&lt;p&gt;For PostgreSQL, infrawise uses a read-only connection. Create the user with these four statements:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;infrawise_ro&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'yourpassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;yourdb&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;infrawise_ro&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;infrawise_ro&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;infrawise_ro&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check findings without opening an editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;infrawise analyze &lt;span class="nt"&gt;--severity&lt;/span&gt; high
infrawise analyze &lt;span class="nt"&gt;--severity&lt;/span&gt; high &lt;span class="nt"&gt;--output&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--severity&lt;/code&gt; flag accepts &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, or &lt;code&gt;verify&lt;/code&gt;. The &lt;code&gt;--output&lt;/code&gt; flag saves findings as a markdown report.&lt;/p&gt;

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

&lt;p&gt;The problem isn't that AI coding assistants write bad code. The problem is that they write code for an infrastructure they've never seen. A &lt;code&gt;Scan&lt;/code&gt; on an empty dev table and a &lt;code&gt;Scan&lt;/code&gt; on a 50M-row production table look identical in source — the model has no way to tell them apart unless something provides that context.&lt;/p&gt;

&lt;p&gt;infrawise makes that context available deterministically, before the code gets written. The assistant stops guessing about your GSIs, your partition keys, and your missing indexes because it no longer needs to guess.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/infrawise" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI coding assistants have no knowledge of your actual infrastructure — they infer from source files and fill gaps with generic patterns&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Scan&lt;/code&gt; with &lt;code&gt;Limit&lt;/code&gt; still reads every item in DynamoDB before applying the limit — the model won't know this unless it knows your table's access patterns&lt;/li&gt;
&lt;li&gt;infrawise exposes your exact schemas, GSIs, partition keys, and flagged patterns to your editor through MCP — 15 tools Claude Code can query before writing a single line&lt;/li&gt;
&lt;li&gt;All analysis is deterministic: TypeScript AST parsing, schema introspection, rule-based detection — no LLM in the analysis path&lt;/li&gt;
&lt;li&gt;Setup is one command: &lt;code&gt;infrawise start --claude&lt;/code&gt; generates config, writes &lt;code&gt;.mcp.json&lt;/code&gt;, and opens your editor with full context ready&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>dynamodb</category>
    </item>
  </channel>
</rss>
