<?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>Your 12-Minute Frontend Build Is Now Your AI Agent's Bottleneck</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:30:12 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-12-minute-frontend-build-is-now-your-ai-agents-bottleneck-2ilb</link>
      <guid>https://dev.to/siddharth_pandey_27/your-12-minute-frontend-build-is-now-your-ai-agents-bottleneck-2ilb</guid>
      <description>&lt;p&gt;Your coding agent just opened five PRs in a morning. Each one changes a handful of files in one product area. Each one triggers a twelve-minute build of the entire frontend: search, account, admin, marketing pages, all of it. That is an hour of CI to validate maybe two hundred lines, and the review queue is now the slowest part of a workflow you adopted to go faster.&lt;/p&gt;

&lt;p&gt;The same twelve minutes used to be an annoyance you absorbed a few times a day. At agent throughput it is the bottleneck.&lt;/p&gt;

&lt;p&gt;It gets worse before it gets better. You ask the agent "where does checkout read the pricing config from?" and it opens forty files across four product areas before answering, because as far as the tooling is concerned there is exactly one application and it is enormous. Then it changes six words of copy in the checkout confirmation screen, and every user on the site redownloads a main bundle whose hash moved for reasons that have nothing to do with them.&lt;/p&gt;

&lt;p&gt;One build unit, one bundle graph, one context blob. Micro frontends in a monorepo split all three, and the monorepo part is what keeps it from turning into a distributed systems problem you did not sign up for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four taxes you are paying
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build fan-out.&lt;/strong&gt; Your CI has no idea that checkout and search do not touch each other. Any commit invalidates the whole build. Build time is a function of repo size, not change size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundle coupling.&lt;/strong&gt; Webpack and Vite chunk splitting help with what gets loaded, but not with what gets invalidated. If checkout and search compile into one build output, a checkout change can shift chunk hashes across the app and evict cached bytes users already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery coupling.&lt;/strong&gt; Two teams shipping to the same artifact means every deploy is a merge queue negotiation. The team that is ready waits on the team that is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context cost.&lt;/strong&gt; This is the newest one and the least discussed. An AI coding agent working in a repo has a finite attention budget. Give it one 1,200-file application with no enforced internal boundaries and it will read broadly, guess about ownership, and occasionally reach into a module it had no business touching. Give it &lt;code&gt;apps/checkout&lt;/code&gt; plus three shared libs and it reads less, guesses less, and produces a diff you can actually review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting without splitting the repo
&lt;/h2&gt;

&lt;p&gt;The mistake people make is equating micro frontends with repo-per-team. That buys you deploy independence and charges you version drift, six duplicate CI configs, and cross-repo refactors that nobody ever does.&lt;/p&gt;

&lt;p&gt;Keep one repo. Split the build.&lt;/p&gt;

&lt;p&gt;With Nx, scaffolding the pieces is one command. Nx 23 renamed the generators: the app that loads federated modules is a &lt;strong&gt;consumer&lt;/strong&gt; (previously &lt;code&gt;host&lt;/code&gt;), the app that exposes them is a &lt;strong&gt;provider&lt;/strong&gt; (previously &lt;code&gt;remote&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nx g @nx/react:consumer apps/shell &lt;span class="nt"&gt;--bundler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rspack &lt;span class="nt"&gt;--providerNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;checkout,search
nx g @nx/react:provider apps/account &lt;span class="nt"&gt;--consumer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;@nx/react:host&lt;/code&gt; and &lt;code&gt;@nx/react:remote&lt;/code&gt; generators still ship, so existing workspaces keep working, but new work should use consumer/provider. With the new generators, providers are registered at runtime from an inline list in the consumer's &lt;code&gt;src/mf.ts&lt;/code&gt; rather than being frozen into build config.&lt;/p&gt;

&lt;p&gt;Each provider gets its own build target and its own deployable output. Underneath, the Module Federation build plugin config looks like this:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createModuleFederationConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@module-federation/enhanced/rspack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createModuleFederationConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shell&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout@https://cdn.example.com/checkout/mf-manifest.json&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;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&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;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;singleton: true&lt;/code&gt; on React is not optional. Without it you ship two React copies, hooks break across the boundary, and you spend an afternoon on an error message about invalid hook calls that has nothing to do with your hooks.&lt;/p&gt;

&lt;p&gt;For remotes that are not known at build time (a plugin surface, a tenant-specific module, an A/B variant), the runtime API takes over:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadRemote&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@module-federation/runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shell&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.example.com/promo/remoteEntry.js&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Promo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promo/Banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The performance case, stated honestly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cache invalidation gets a boundary.&lt;/strong&gt; Each remote publishes its own entry and its own chunks. A copy change in checkout produces new checkout bytes. Search bytes keep their hashes and stay in the user's cache. On a large app this is the difference between shipping a small delta and making every user re-download a vendor bundle because one hash cascaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loading follows navigation.&lt;/strong&gt; A remote is fetched when the route that needs it is entered. Users who never open admin never pay for admin. You can approximate this with route-level lazy imports in a monolith, but you cannot approximate the invalidation boundary, and lazy imports quietly re-couple every time someone adds a top-level import for a type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local dev stops rebuilding what you are not editing.&lt;/strong&gt; &lt;code&gt;nx serve shell&lt;/code&gt; starts the whole composed app with the remotes built and served statically. Only the remote you are actively editing needs a real dev server, and since Nx 21 you get that by serving the remote itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nx serve checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Angular, or on webpack Module Federation without inferred tasks, the equivalent is &lt;code&gt;nx serve shell --devRemotes=checkout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now be honest about the costs, because they are real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each remote costs a manifest fetch plus an entry fetch. On a route that pulls three remotes, that is latency you did not have before. Preload the manifests for likely-next routes.&lt;/li&gt;
&lt;li&gt;Shared singletons only work if versions are compatible. A remote built against React 18 and a host on React 19 fails at runtime rather than at build time, which is the worst place to find out. This is the single biggest operational hazard of the pattern, and it is exactly the failure a monorepo prevents: one lockfile, one version, enforced by the repo rather than by a wiki page.&lt;/li&gt;
&lt;li&gt;Below a certain size this is all overhead. One team, a 90-second build, three routes? Do not do this. The pattern earns its place when multiple teams contend for one pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the AI speedup actually comes from
&lt;/h2&gt;

&lt;p&gt;Two mechanisms, both boring, both effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Affected-only work.&lt;/strong&gt; Nx computes a project graph from real imports, so it knows checkout and search are unrelated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; build
nx affected &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test &lt;/span&gt;lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent that opens a PR touching only &lt;code&gt;apps/checkout&lt;/code&gt; triggers a build of checkout and its dependents. Not the repo. Those five PRs from the opening now cost five checkout builds instead of an hour of full-frontend CI, and they run in parallel because they no longer contend for the same build. The twelve minutes did not get optimized away, it stopped being charged for work that never changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boundaries the agent cannot argue with.&lt;/strong&gt; Tag every project and let ESLint enforce who may import whom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nx/enforce-module-boundaries&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;depConstraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sourceTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onlyDependOnLibsWithTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:shared&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="na"&gt;sourceTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onlyDependOnLibsWithTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:checkout&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="na"&gt;sourceTag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onlyDependOnLibsWithTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope:search&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;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part that changes how it feels to work with an agent. A prompt that says "do not import from other teams' code" is a suggestion the model may or may not follow. A lint rule that fails CI is a fact. When an agent takes the shortcut of reaching into &lt;code&gt;apps/search/src/internal/pricing.ts&lt;/code&gt; from checkout, the build tells it no, and it fixes the mistake in the same session instead of you finding it in review three days later.&lt;/p&gt;

&lt;p&gt;The context effect compounds. Scoping an agent to one remote plus the shared libs means the files it reads are the files that matter. Smaller context, fewer distractor files, more of the budget spent on the actual change. The same architecture decision that gave you independent deploys gave you a natural unit of work for an agent, which is not a coincidence: both are asking for the same thing, which is a piece of the system you can reason about without loading the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration path that does not stall
&lt;/h2&gt;

&lt;p&gt;Do not carve up the whole app. Pick the boundary that hurts most, usually the one where two teams collide in the merge queue, and extract exactly that one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the consumer, keep the existing app as the first provider. Nothing changes for users.&lt;/li&gt;
&lt;li&gt;Extract the contended area into a second provider. Shared code goes to &lt;code&gt;libs/shared&lt;/code&gt;, tagged &lt;code&gt;scope:shared&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turn on &lt;code&gt;enforce-module-boundaries&lt;/code&gt; in warn mode, fix the violations it finds, then flip it to error.&lt;/li&gt;
&lt;li&gt;Switch CI to &lt;code&gt;nx affected&lt;/code&gt;. Measure the build time delta before extracting a third remote.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step 4 does not show a meaningful improvement, stop. You have a dependency-graph problem, not an architecture problem, and adding remotes will not fix it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Agent throughput turns a tolerable build time into a hard bottleneck. Five PRs a day made twelve minutes annoying; five PRs an hour makes it the constraint on everything.&lt;/li&gt;
&lt;li&gt;Micro frontends buy independent build, cache, and deploy boundaries. A monorepo keeps the single lockfile and single-commit refactors. You want both, not one or the other.&lt;/li&gt;
&lt;li&gt;Mark framework packages &lt;code&gt;singleton: true&lt;/code&gt; in &lt;code&gt;shared&lt;/code&gt;, or hooks break across the remote boundary at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nx affected -t build&lt;/code&gt; turns build time into a function of change size instead of repo size, which is what makes multi-PR agent work practical rather than theoretical.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@nx/enforce-module-boundaries&lt;/code&gt; with tags converts your architecture from a convention into a CI failure, and an AI agent respects a failing build far more reliably than a prompt.&lt;/li&gt;
&lt;li&gt;Extract one boundary, measure, then decide. Below a couple of teams and a couple of minutes of build time, this pattern costs more than it returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs worth reading before you start: &lt;a href="https://nx.dev/docs/technologies/module-federation/concepts/micro-frontend-architecture" rel="noopener noreferrer"&gt;Nx Module Federation&lt;/a&gt; and &lt;a href="https://module-federation.io/" rel="noopener noreferrer"&gt;Module Federation Core&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your Cognito Login Code Fails Because the App Client Never Allowed That Auth Flow</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Sun, 02 Aug 2026 20:39:22 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-cognito-login-code-fails-because-the-app-client-never-allowed-that-auth-flow-1hmf</link>
      <guid>https://dev.to/siddharth_pandey_27/your-cognito-login-code-fails-because-the-app-client-never-allowed-that-auth-flow-1hmf</guid>
      <description>&lt;p&gt;Your login screen works locally. You point it at the staging user pool and every sign-in comes back as an error before it ever reaches a password check.&lt;/p&gt;

&lt;p&gt;The code your AI assistant wrote calls &lt;code&gt;InitiateAuth&lt;/code&gt; with &lt;code&gt;AuthFlow: 'USER_PASSWORD_AUTH'&lt;/code&gt;. Reasonable guess: it is the flow in most Cognito tutorials. But the app client in staging was created by a Terraform module that set &lt;code&gt;explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]&lt;/code&gt;, and that client also has a secret. So the call fails twice over: the flow is not enabled for this client, and the request is missing &lt;code&gt;SECRET_HASH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of that is visible in your source files. The assistant read your repo, found no answer, and produced the most statistically common Cognito snippet on the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app client is the config, and it is not in your repo
&lt;/h2&gt;

&lt;p&gt;Cognito's failure modes are almost all per app client settings, not per user pool settings. Four of them break generated code immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowed auth flows.&lt;/strong&gt; &lt;code&gt;ExplicitAuthFlows&lt;/code&gt; is a whitelist. If &lt;code&gt;ALLOW_USER_PASSWORD_AUTH&lt;/code&gt; is not on it, &lt;code&gt;USER_PASSWORD_AUTH&lt;/code&gt; is rejected regardless of whether the username and password are correct. Client A in dev may allow it while client B in prod does not, and the same handler code then works in one environment and not the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client secret.&lt;/strong&gt; A client with &lt;code&gt;GenerateSecret: true&lt;/code&gt; requires every auth call to carry &lt;code&gt;SECRET_HASH&lt;/code&gt;, a base64 HMAC-SHA256 of &lt;code&gt;username + clientId&lt;/code&gt; keyed with the client secret. An assistant that does not know a secret exists will never emit that field, and the call fails on secret verification rather than on credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MFA.&lt;/strong&gt; With MFA set to &lt;code&gt;ON&lt;/code&gt;, &lt;code&gt;InitiateAuth&lt;/code&gt; typically returns a &lt;code&gt;ChallengeName&lt;/code&gt; and a session instead of tokens. Code written against the happy path reads &lt;code&gt;response.AuthenticationResult.IdToken&lt;/code&gt;, gets &lt;code&gt;undefined&lt;/code&gt;, and throws somewhere three functions away from the actual cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token validity units.&lt;/strong&gt; &lt;code&gt;AccessTokenValidity: 60&lt;/code&gt; means nothing on its own. With &lt;code&gt;TokenValidityUnits.AccessToken = 'minutes'&lt;/code&gt; it is one hour. With &lt;code&gt;'days'&lt;/code&gt; it is two months. Refresh logic built on the wrong unit either hammers the token endpoint or lets sessions die.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth settings.&lt;/strong&gt; If you are using the hosted UI instead of direct auth, the client carries its own &lt;code&gt;AllowedOAuthFlows&lt;/code&gt;, &lt;code&gt;AllowedOAuthScopes&lt;/code&gt;, and &lt;code&gt;CallbackURLs&lt;/code&gt;. Build a redirect against a URL that is not in the callback list, or request a scope the client does not allow, and Cognito refuses the request at the authorize endpoint before your app sees anything. An assistant writing the redirect has no way to know the staging client only registered &lt;code&gt;https://staging.example.com/callback&lt;/code&gt; while your local dev URL was never added.&lt;/p&gt;

&lt;p&gt;Every one of those lives in AWS, not in your repository. Even when the pool is defined in Terraform, the assistant would have to find the right module, resolve the variables, and know which client the running service actually uses. In practice it does not, so it guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the real client config
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;Infrawise&lt;/a&gt; extracts this and hands it to your assistant over MCP. The Cognito extractor in &lt;code&gt;src/adapters/aws/services.ts&lt;/code&gt; walks four read-only calls: &lt;code&gt;ListUserPools&lt;/code&gt; for every pool, then per pool &lt;code&gt;DescribeUserPool&lt;/code&gt;, then &lt;code&gt;ListUserPoolClients&lt;/code&gt;, then &lt;code&gt;DescribeUserPoolClient&lt;/code&gt; for each client. Both listings are paginated with &lt;code&gt;NextToken&lt;/code&gt;, so a pool with 80 app clients does not get silently truncated at the first page.&lt;/p&gt;

&lt;p&gt;For each client it keeps exactly the fields that change how you write the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clientName, clientId
authFlows          &amp;lt;- ExplicitAuthFlows
oauthFlows         &amp;lt;- AllowedOAuthFlows
oauthScopes        &amp;lt;- AllowedOAuthScopes
callbackUrls       &amp;lt;- CallbackURLs
generatesSecret    &amp;lt;- !!ClientSecret
accessTokenValidity, idTokenValidity, refreshTokenValidity
tokenValidityUnits &amp;lt;- { accessToken, idToken, refreshToken }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;generatesSecret&lt;/code&gt;. &lt;code&gt;DescribeUserPoolClient&lt;/code&gt; does return the secret value, and infrawise converts it to a boolean at the point of extraction. The value is never stored in the graph, never cached, and never returned by any tool. Your assistant learns that a secret exists and that &lt;code&gt;SECRET_HASH&lt;/code&gt; is mandatory, without ever seeing the secret. Same for users: infrawise never calls any user API. Sign-in code is what it helps you write, not a directory it reads.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;get_cognito_overview&lt;/code&gt; MCP tool returns the whole thing:&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;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&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;"Client secret values and user data are 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;"userPools"&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;"app-users-staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-south-1_XXXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mfaConfiguration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPTIONAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"clients"&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;"clientName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-spa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"clientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4h1..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"authFlows"&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;"ALLOW_USER_SRP_AUTH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOW_REFRESH_TOKEN_AUTH"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"oauthFlows"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"oauthScopes"&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;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"callbackUrls"&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;"https://staging.example.com/callback"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"generatesSecret"&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;"accessTokenValidity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"tokenValidityUnits"&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;"accessToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minutes"&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;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;That is the difference between an assistant guessing &lt;code&gt;USER_PASSWORD_AUTH&lt;/code&gt; and an assistant writing SRP with a &lt;code&gt;SECRET_HASH&lt;/code&gt;, because the whitelist and the secret flag are sitting right there in its context.&lt;/p&gt;

&lt;p&gt;The tool description registered in &lt;code&gt;src/server/index.ts&lt;/code&gt; tells the model when to reach for it and when not to: call it before writing any sign-in, sign-up, or token-refresh code; do not call it to look up users or tokens. That last clause matters more than it looks. Tool descriptions are the only thing steering which tool an agent picks, and a tool that sounds like a user directory will get called for the wrong reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on
&lt;/h2&gt;

&lt;p&gt;Cognito is off by default. &lt;code&gt;infrawise start&lt;/code&gt; writes an &lt;code&gt;infrawise.yaml&lt;/code&gt; with &lt;code&gt;cognito: { enabled: false }&lt;/code&gt;, because most repos have no Cognito and there is no reason to make an API call for them. Auth work means flipping one key:&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;cognito&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 IAM policy is four read actions, and nothing else:&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="s"&gt;cognito-idp:ListUserPools&lt;/span&gt;
&lt;span class="s"&gt;cognito-idp:DescribeUserPool&lt;/span&gt;
&lt;span class="s"&gt;cognito-idp:ListUserPoolClients&lt;/span&gt;
&lt;span class="s"&gt;cognito-idp:DescribeUserPoolClient&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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;That probes your environment, runs the analysis, writes &lt;code&gt;.mcp.json&lt;/code&gt; so your editor reconnects on every future launch, and opens Claude Code with all 21 tools available. From then on you just run &lt;code&gt;claude&lt;/code&gt;. Results are cached for 24 hours, and &lt;code&gt;get_infra_overview&lt;/code&gt; reports a &lt;code&gt;freshness&lt;/code&gt; object with the analysis age and a &lt;code&gt;stale&lt;/code&gt; flag so the assistant can tell when it is looking at yesterday's picture.&lt;/p&gt;

&lt;p&gt;Ask "write me a sign-in handler for the staging pool" and the flow is no longer a guess. The assistant calls &lt;code&gt;get_cognito_overview&lt;/code&gt;, sees &lt;code&gt;ALLOW_USER_SRP_AUTH&lt;/code&gt; and &lt;code&gt;generatesSecret: true&lt;/code&gt;, and writes SRP with a secret hash the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually buys you
&lt;/h2&gt;

&lt;p&gt;The bug class here is boring, which is why it eats so much time. Nothing crashes at build time. Types are fine. Tests that mock the Cognito client pass. The failure only shows up against a real user pool, as an exception whose message is about a flow name rather than about the app client that disallowed it, and the fix is a config value you have to go read in a console tab.&lt;/p&gt;

&lt;p&gt;Cognito is one instance of the general pattern. The information needed to write correct code is split between your repo and your cloud account, and the assistant only has half. Infrawise closes that gap deterministically: no LLM in the extraction path, just SDK calls, AST parsing, and rule-based analyzers producing a graph that MCP tools read from.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &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;Cognito failures are per app client, not per user pool. The same code succeeds against one client and fails against another in the same pool.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;callbackUrls&lt;/code&gt; and &lt;code&gt;oauthScopes&lt;/code&gt; before building a hosted UI redirect. A URL that is not registered is rejected at the authorize endpoint.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;generatesSecret&lt;/code&gt; is true, every auth call needs &lt;code&gt;SECRET_HASH&lt;/code&gt;. An assistant that does not know a secret exists will never emit it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AccessTokenValidity&lt;/code&gt; is meaningless without &lt;code&gt;TokenValidityUnits&lt;/code&gt;. 60 is an hour or two months depending on the unit.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;cognito: enabled: true&lt;/code&gt; in &lt;code&gt;infrawise.yaml&lt;/code&gt; (it defaults to false) and grant the four &lt;code&gt;cognito-idp&lt;/code&gt; read actions.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;get_cognito_overview&lt;/code&gt; before writing sign-in, sign-up, or refresh code. It never returns client secret values or user data.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your Lambda Passes Tests Then Throws AccessDeniedException in Production</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:12:56 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-lambda-passes-tests-then-throws-accessdeniedexception-in-production-28bc</link>
      <guid>https://dev.to/siddharth_pandey_27/your-lambda-passes-tests-then-throws-accessdeniedexception-in-production-28bc</guid>
      <description>&lt;p&gt;You asked Claude Code to add a step to your order handler: pull the Stripe key out of Secrets Manager, then publish an &lt;code&gt;order.settled&lt;/code&gt; event to SNS. It wrote clean code. Your unit tests mock both SDK clients, so they pass. CI is green. You deploy.&lt;/p&gt;

&lt;p&gt;Then the first real invocation logs this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/order-processor-role/order-processor
is not authorized to perform: sns:Publish on resource: arn:aws:sns:us-east-1:123456789012:order-events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing was wrong with the code. The execution role was written months ago, when the function only touched DynamoDB. The code grew a new dependency and the policy did not follow.&lt;/p&gt;

&lt;p&gt;This is one of the most annoying classes of AWS bug because it is invisible to every tool in the loop. The type checker sees a valid &lt;code&gt;PublishCommand&lt;/code&gt;. The linter sees valid TypeScript. The AI assistant reads your handler and has no idea what &lt;code&gt;order-processor-role&lt;/code&gt; allows, because the policy lives in AWS (or in a Terraform file three directories away), not in the file it is editing.&lt;/p&gt;

&lt;p&gt;Infrawise closes that gap by comparing the two directly. &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;
  
  
  Why the assistant cannot see this on its own
&lt;/h2&gt;

&lt;p&gt;An AI coding assistant works from the files you give it. When it writes a Lambda handler, it can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the handler source&lt;/li&gt;
&lt;li&gt;imports, so it knows which AWS SDK clients you use&lt;/li&gt;
&lt;li&gt;maybe a nearby test file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it cannot see is the runtime identity that code will execute as. The link between "this handler" and "this IAM role" is a &lt;code&gt;Role&lt;/code&gt; property on the deployed function, resolved through an ARN, and the policy behind that ARN is a set of JSON documents attached to a role name. Three lookups away from anything in your repo.&lt;/p&gt;

&lt;p&gt;So the assistant does the only thing it can: it assumes the permissions exist. Ninety percent of the time they do, which is exactly what makes the other ten percent expensive. The failure surfaces at runtime, in an environment where you are reading CloudWatch instead of your editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What infrawise actually compares
&lt;/h2&gt;

&lt;p&gt;Infrawise already builds a graph of your infrastructure and your code. The AST scan produces typed edges from functions to the things they touch: a &lt;code&gt;query&lt;/code&gt; or &lt;code&gt;scan&lt;/code&gt; edge to a DynamoDB table, a &lt;code&gt;reads_secret&lt;/code&gt; edge to a Secrets Manager secret, a &lt;code&gt;reads_parameter&lt;/code&gt; edge to an SSM parameter, a &lt;code&gt;publishes_to&lt;/code&gt; edge to an SQS queue or an SNS topic.&lt;/p&gt;

&lt;p&gt;Separately, the Lambda extractor pulls &lt;code&gt;roleArn&lt;/code&gt; off every function, then fetches what that role permits. It batches the fetch per unique role ARN, so ten functions sharing one role cost one IAM round trip, not ten. For each role it walks both attached managed policies and inline policies, decodes the policy documents, and reduces every &lt;code&gt;Allow&lt;/code&gt; statement to the service prefix of its actions. &lt;code&gt;sns:Publish&lt;/code&gt; becomes &lt;code&gt;sns&lt;/code&gt;. &lt;code&gt;dynamodb:GetItem&lt;/code&gt; becomes &lt;code&gt;dynamodb&lt;/code&gt;. A bare &lt;code&gt;"*"&lt;/code&gt; action becomes &lt;code&gt;*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is &lt;code&gt;allowedServices&lt;/code&gt;, a flat list like &lt;code&gt;["logs", "dynamodb"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the comparison is trivial. Walk the function's outgoing edges, map each to the service prefix it requires, and subtract &lt;code&gt;allowedServices&lt;/code&gt;. What is left is what the code needs and the role does not grant.&lt;/p&gt;

&lt;p&gt;The mapping is deliberately narrow. Five edge types map to five services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge&lt;/th&gt;
&lt;th&gt;Required service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;query&lt;/code&gt; or &lt;code&gt;scan&lt;/code&gt; on a DynamoDB table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dynamodb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reads_secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;secretsmanager&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reads_parameter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;publishes_to&lt;/code&gt; a queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;publishes_to&lt;/code&gt; a topic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sns&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two cases produce no finding at all, on purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No IAM data.&lt;/strong&gt; If the IAM read fails (missing permission, SCP, whatever), &lt;code&gt;allowedServices&lt;/code&gt; comes back undefined and the analyzer skips the function entirely. Infrawise will not tell you a permission is missing when it could not read the policy. Silence beats a false alarm you learn to ignore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wildcard.&lt;/strong&gt; If the role has &lt;code&gt;"Action": "*"&lt;/code&gt; anywhere, the service list contains &lt;code&gt;*&lt;/code&gt; and the function is skipped. AdministratorAccess is its own problem, but it is not a missing-permission problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you see it
&lt;/h2&gt;

&lt;p&gt;Two places, and they cover the two moments where it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;While you are writing the handler.&lt;/strong&gt; &lt;code&gt;analyze_function&lt;/code&gt; returns &lt;code&gt;missingPermissions&lt;/code&gt; alongside everything else it knows about the function:&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;"function"&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-processor"&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;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/handlers/order-processor.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"triggers"&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;"type"&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;"source"&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;"eventShape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event.Records[0].body"&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;"accesses"&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;"targetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"table:dynamo: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;"edgeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"targetName"&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;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"table"&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;"targetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secret:aws:stripe/live"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"edgeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reads_secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"targetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stripe/live"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secret"&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;"targetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"topic:aws: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;"edgeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publishes_to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"targetName"&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;"targetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"topic"&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;"missingPermissions"&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;"secretsmanager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sns"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&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="err"&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;"recommendations"&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="err"&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;That is the whole point. The assistant asked one question before writing code and got back the trigger event shape &lt;em&gt;and&lt;/em&gt; the fact that two of the three services this function talks to are not in its role. It can tell you to fix the policy in the same breath as it writes the handler.&lt;/p&gt;

&lt;p&gt;Note the field is omitted entirely when IAM data was unavailable, rather than returned as an empty array. An empty &lt;code&gt;missingPermissions&lt;/code&gt; means "checked, nothing missing." An absent one means "not checked." Those are different answers and conflating them is how you get misplaced confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you deploy.&lt;/strong&gt; The same comparison runs as a high-severity analyzer in &lt;code&gt;infrawise analyze&lt;/code&gt; and &lt;code&gt;infrawise check&lt;/code&gt;:&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   Lambda "order-processor" accesses sns but execution role has no sns permissions
       "order-processor" calls sns in code but its IAM execution role (arn:aws:iam::123456789012:role/order-processor-role) has no sns:* permissions. This will cause AccessDeniedException at runtime — code passes tests but fails in AWS.
       → Add sns permissions to the execution role for "order-processor". Minimum required: sns:Publish.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recommendation names actual actions, not &lt;code&gt;sns:*&lt;/code&gt;. Each service has a minimal action set baked in: &lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt; for secrets, &lt;code&gt;ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath&lt;/code&gt; for parameters, &lt;code&gt;sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes&lt;/code&gt; for queues, and the CRUD-plus-query set for DynamoDB. You can paste the list into the policy without thinking about whether you just granted &lt;code&gt;dynamodb:DeleteTable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because it is a high-severity finding, &lt;code&gt;infrawise check&lt;/code&gt; fails the build on it by default. That is the whole CI story: &lt;code&gt;check&lt;/code&gt; runs a fresh analysis (never a cached graph, since CI gating on stale data is worse than no gating) and exits non-zero when anything at or above &lt;code&gt;--fail-on&lt;/code&gt; shows up. Default threshold is &lt;code&gt;high&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two things to know before you trust it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Names have to line up.&lt;/strong&gt; The check joins a code function node to a Lambda node by exact name match. If your handler is exported as &lt;code&gt;handler&lt;/code&gt; in &lt;code&gt;src/orders.ts&lt;/code&gt; but deployed as &lt;code&gt;order-processor&lt;/code&gt;, infrawise finds no code node for that Lambda and produces no finding. It fails closed, which is the right direction, but it means the check is only as good as your naming discipline. If you use a framework that derives function names from the deployed name, you get this for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granularity stops at the service prefix.&lt;/strong&gt; Infrawise compares &lt;code&gt;sns&lt;/code&gt; against &lt;code&gt;sns&lt;/code&gt;, not &lt;code&gt;arn:aws:sns:us-east-1:123456789012:order-events&lt;/code&gt; against the resource in your &lt;code&gt;PublishCommand&lt;/code&gt;. A role scoped to one topic passes the check even if your code publishes to a different topic. So this catches "the policy has no SNS at all," which is the common case, not "the policy has the wrong SNS resource." Resource-level checking is a different problem and pretending otherwise would make the finding untrustworthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It needs read access to IAM.&lt;/strong&gt; Five actions: &lt;code&gt;iam:ListAttachedRolePolicies&lt;/code&gt;, &lt;code&gt;iam:ListRolePolicies&lt;/code&gt;, &lt;code&gt;iam:GetRolePolicy&lt;/code&gt;, &lt;code&gt;iam:GetPolicy&lt;/code&gt;, &lt;code&gt;iam:GetPolicyVersion&lt;/code&gt;. They are optional in the infrawise IAM policy. Drop them and everything else still works, you just lose this check. Infrawise is read-only across the board, so nothing here writes to your account.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&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 probes your environment, writes &lt;code&gt;infrawise.yaml&lt;/code&gt; and &lt;code&gt;.mcp.json&lt;/code&gt;, and opens Claude Code with all 21 tools connected. Ask it to write a Lambda that reads a secret, and watch it check the role before it writes the code.&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;
&lt;code&gt;AccessDeniedException&lt;/code&gt; in a Lambda is almost never a code bug. It is a drift bug: the code grew a dependency, the execution role did not follow.&lt;/li&gt;
&lt;li&gt;Your AI assistant cannot catch this from source files alone, because the role that code runs as is three lookups away from anything in your repo.&lt;/li&gt;
&lt;li&gt;Infrawise compares the function's actual outgoing edges (DynamoDB query, secret read, SSM read, SQS send, SNS publish) against the service prefixes its IAM role permits, then reports the difference.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;analyze_function&lt;/code&gt; before writing a handler to get &lt;code&gt;missingPermissions&lt;/code&gt; alongside the trigger event shape. An absent field means IAM was not readable; an empty array means it was checked and clean.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;infrawise check&lt;/code&gt; in CI. Missing permissions are high severity, so the default &lt;code&gt;--fail-on high&lt;/code&gt; blocks the deploy.&lt;/li&gt;
&lt;li&gt;The check is service-level, not resource-level, and joins code to Lambda by exact function name. Know both limits so you know what a clean result actually proves.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How I Solved Claude Code Silently Missing the SQS Trigger on My Lambda</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:50:35 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/how-i-solved-claude-code-silently-missing-the-sqs-trigger-on-my-lambda-5a</link>
      <guid>https://dev.to/siddharth_pandey_27/how-i-solved-claude-code-silently-missing-the-sqs-trigger-on-my-lambda-5a</guid>
      <description>&lt;p&gt;I asked Claude Code to add a handler for &lt;code&gt;processOrders&lt;/code&gt;, a Lambda wired up to consume from &lt;code&gt;orders-queue&lt;/code&gt; and read the matching row out of the &lt;code&gt;Orders&lt;/code&gt; DynamoDB table. It wrote one immediately — confidently, in one shot, the way it writes most things.&lt;/p&gt;

&lt;p&gt;It also parsed the event body wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: Claude Code didn't know the Lambda had an SQS trigger
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;processOrders&lt;/code&gt; is invoked by &lt;code&gt;orders-queue&lt;/code&gt; through an event source mapping — that's not in &lt;code&gt;processOrders&lt;/code&gt;'s source file, it's in the AWS account. Claude Code, reading only the handler's code, had no way to see that mapping. So it guessed at the event shape instead of knowing it, and reached for &lt;code&gt;event.body&lt;/code&gt; — the shape you'd expect from an API Gateway proxy integration, not from SQS. The actual shape for an SQS-triggered Lambda is &lt;code&gt;event.Records[0].body&lt;/code&gt;. First message in, first field access, and the handler throws.&lt;/p&gt;

&lt;p&gt;That wasn't the only thing invisible from source alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;orders-queue&lt;/code&gt; has no dead-letter queue. If the handler throws, SQS retries up to &lt;code&gt;maxReceiveCount&lt;/code&gt; times and then discards the message — no alert, no queue-depth spike, nothing in the logs pointing at what happened. A failed order just vanishes.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Orders&lt;/code&gt; table has one partition key (&lt;code&gt;orderId&lt;/code&gt;) and zero GSIs. Any lookup by anything else becomes a full table scan the moment the handler ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is visible from reading &lt;code&gt;processOrders&lt;/code&gt;'s current code. It's the state of the AWS account, and an AI assistant reading only source files fills the gap with a plausible guess instead of a fact — starting with which trigger the function even has.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: infrawise's MCP tools in the loop, in order
&lt;/h2&gt;

&lt;p&gt;Here's what the same session looked like once I wired &lt;a href="https://github.com/Sidd27/infrawise" rel="noopener noreferrer"&gt;infrawise&lt;/a&gt; into Claude Code as an MCP server and asked it to redo the handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start wide: &lt;code&gt;get_infra_overview&lt;/code&gt;.&lt;/strong&gt; No arguments, just a snapshot — table names, queue names, lambda count, high-severity findings. It comes back showing the &lt;code&gt;Orders&lt;/code&gt; table, &lt;code&gt;orders-queue&lt;/code&gt;, and &lt;code&gt;processOrders&lt;/code&gt; already in the graph, plus a high-severity finding already attached to that Lambda. That finding surfaces before the handler is written, not after the deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrow to the function: &lt;code&gt;analyze_function&lt;/code&gt;.&lt;/strong&gt; Called with &lt;code&gt;function: "processOrders"&lt;/code&gt;, this is the tool built for "I'm about to write or review this handler," and it returns four things Claude Code would otherwise guess at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;The trigger event shape.&lt;/em&gt; &lt;code&gt;processOrders&lt;/code&gt; is triggered by SQS, so the correct shape is &lt;code&gt;event.Records[0].body&lt;/code&gt; — not &lt;code&gt;event.body&lt;/code&gt;. Infrawise keeps a fixed map per trigger type (SNS is &lt;code&gt;event.Records[0].Sns.Message&lt;/code&gt;, S3 is &lt;code&gt;event.Records[0].s3.object.key&lt;/code&gt;, DynamoDB Streams is &lt;code&gt;event.Records[0].dynamodb.NewImage&lt;/code&gt;), so the entry point is right on the first draft.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;The DLQ finding, made concrete.&lt;/em&gt; Infrawise's &lt;code&gt;LambdaMissingTriggerDLQAnalyzer&lt;/code&gt; walks every Lambda's SQS/Kinesis/DynamoDB-stream triggers, checks whether the source queue has a DLQ, and raises high severity when it doesn't: &lt;em&gt;"processOrders is triggered by orders-queue which has no DLQ — if the Lambda handler fails, messages will be retried and eventually discarded with no failure record."&lt;/em&gt; Same problem named in the previous section, now attached to the exact function and queue instead of being a fact nobody checked.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Missing IAM permissions.&lt;/em&gt; When execution-role data is available, &lt;code&gt;analyze_function&lt;/code&gt; diffs what the function's code actually calls against what its role allows, and returns the gap — catching an &lt;code&gt;AccessDeniedException&lt;/code&gt; before it happens in prod instead of after.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Deduplicated recommendations,&lt;/em&gt; collapsed into one list instead of several warnings to reconcile by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Confirm the queue directly: &lt;code&gt;get_queue_details&lt;/code&gt;.&lt;/strong&gt; The handler's retry behavior depends on more than DLQ presence, so Claude Code checks the queue itself: &lt;code&gt;hasDLQ: false&lt;/code&gt; confirmed, plus visibility timeout, encryption, and &lt;code&gt;isFifo&lt;/code&gt;. That last field matters more than it looks — if the queue were FIFO, every &lt;code&gt;SendMessage&lt;/code&gt; call in the handler would need a &lt;code&gt;MessageGroupId&lt;/code&gt;, and skipping it isn't a lint warning, it's a runtime error the first time the handler publishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the schema right: &lt;code&gt;get_table_schema&lt;/code&gt;.&lt;/strong&gt; Called with &lt;code&gt;tables: ["Orders"]&lt;/code&gt;, this returns the partition key (&lt;code&gt;orderId&lt;/code&gt;), billing mode (&lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;), and the fact that there are no GSIs. If the handler needs to look up an order by anything other than &lt;code&gt;orderId&lt;/code&gt;, that's a &lt;code&gt;Scan&lt;/code&gt;, and infrawise's scan analyzer flags it the moment it lands in the code graph — so Claude Code reaches for &lt;code&gt;suggest_gsi&lt;/code&gt; up front instead of shipping the scan and fixing it in the next PR. Row data is never included — column names, types, keys, and index definitions only, and only for the tables this handler actually touches, not the whole account's schema dumped into the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped instead
&lt;/h2&gt;

&lt;p&gt;With all four calls back, the rewritten handler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses &lt;code&gt;event.Records[0].body&lt;/code&gt; — the correct shape for this trigger, not a guess&lt;/li&gt;
&lt;li&gt;Queries &lt;code&gt;Orders&lt;/code&gt; by &lt;code&gt;orderId&lt;/code&gt; — no scan, because the schema call already ruled that out&lt;/li&gt;
&lt;li&gt;Ships with an explicit callout that &lt;code&gt;orders-queue&lt;/code&gt; needs a DLQ before this goes to production&lt;/li&gt;
&lt;li&gt;Doesn't publish anywhere without checking &lt;code&gt;isFifo&lt;/code&gt; first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those four things was either wrong or invisible in the first version — the one Claude Code wrote from source files alone. None of it required me to open the AWS console mid-task or remember which table has which GSI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a repeatable pattern, not a one-off
&lt;/h2&gt;

&lt;p&gt;The four tools compose in a fixed order because each answers a narrower question than the last: &lt;code&gt;get_infra_overview&lt;/code&gt; says what exists, &lt;code&gt;analyze_function&lt;/code&gt; says what's wrong with the one thing being touched, &lt;code&gt;get_queue_details&lt;/code&gt; and &lt;code&gt;get_table_schema&lt;/code&gt; fill in the operational and schema detail neither of the first two carries at full resolution. That's the same shape as infrawise's own "reviewing an entire service" and "before writing a query" patterns — it holds for any handler, not just this one.&lt;/p&gt;

&lt;p&gt;None of this runs an LLM against the infrastructure. Extraction and analysis are deterministic — AST parsing, live AWS reads, rule-based analyzers, graph correlation. Claude Code only consumes that graph through MCP, the same way it consumes source files. The difference is the graph doesn't guess.&lt;/p&gt;

&lt;p&gt;Try it in your own project — &lt;code&gt;npx infrawise start --claude&lt;/code&gt; writes the &lt;code&gt;.mcp.json&lt;/code&gt; and opens Claude Code with all 21 tools wired in, no server to babysit. &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;Call &lt;code&gt;analyze_function&lt;/code&gt; before writing or reviewing any Lambda handler — it returns the correct trigger event shape, missing IAM permissions, and any DLQ/trigger findings in one call.&lt;/li&gt;
&lt;li&gt;A queue with a live SQS trigger and no DLQ is a high-severity finding for a reason: failed messages are retried up to &lt;code&gt;maxReceiveCount&lt;/code&gt; times, then discarded with no record.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;isFifo&lt;/code&gt; on any queue before writing &lt;code&gt;SendMessage&lt;/code&gt; calls — a missing &lt;code&gt;MessageGroupId&lt;/code&gt; on a FIFO queue is a runtime error, not a style issue.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;get_table_schema&lt;/code&gt; with just the tables a handler touches instead of dumping the whole schema into the prompt — it also tells you whether the GSI your query needs already exists.&lt;/li&gt;
&lt;li&gt;None of this requires an LLM to analyze your infrastructure — the graph is deterministic; Claude Code only reads it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Your Lambda List Was Silently Truncated at 200 — And Your AI Assistant Never Knew</title>
      <dc:creator>Siddharth Pandey</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:01:08 +0000</pubDate>
      <link>https://dev.to/siddharth_pandey_27/your-infrastructure-tool-silently-drops-functions-past-200-and-so-might-yours-1eke</link>
      <guid>https://dev.to/siddharth_pandey_27/your-infrastructure-tool-silently-drops-functions-past-200-and-so-might-yours-1eke</guid>
      <description>&lt;h2&gt;
  
  
  Hook
&lt;/h2&gt;

&lt;p&gt;An AWS account with 240 Lambda functions runs &lt;code&gt;infrawise analyze&lt;/code&gt;. The tool prints a clean summary: services detected, findings generated, no errors. A developer asks their AI assistant, wired up through MCP, "does this function have a dead-letter queue?" for a function that happens to be function #217 in the account. The assistant calls &lt;code&gt;get_lambda_overview&lt;/code&gt;, gets back a list, doesn't find the function, and confidently tells the developer it doesn't exist — or worse, silently skips it and reasons about a different function with a similar name. No error. No warning. Just a wrong answer delivered with total confidence, because 40 functions never made it into the graph in the first place.&lt;/p&gt;

&lt;p&gt;That was &lt;a href="https://github.com/Sidd27/infrawise/issues/40" rel="noopener noreferrer"&gt;Infrawise issue #40&lt;/a&gt;, and it's a useful case study in how a tool whose entire pitch is "stop AI assistants from guessing" can quietly start guessing itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 200-function wall
&lt;/h2&gt;

&lt;p&gt;Infrawise builds an in-memory graph of a project's infrastructure — tables, functions, queues, indexes — and exposes it to AI coding assistants through 21 MCP tools. &lt;code&gt;extractLambdaMetadata&lt;/code&gt; in &lt;code&gt;src/adapters/aws/services.ts&lt;/code&gt; is the function responsible for pulling every Lambda function into that graph. It calls AWS's &lt;code&gt;ListFunctionsCommand&lt;/code&gt; in pages of 50 and loops until AWS says there's nothing left:&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;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&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;ListFunctionsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;MaxItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="c1"&gt;// ...push each function into the array...&lt;/span&gt;
  &lt;span class="nx"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NextMarker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That trailing &lt;code&gt;&amp;amp;&amp;amp; functions.length &amp;lt; 200&lt;/code&gt; is the whole bug. Once the running total hit 200 — four pages in — the loop stopped, even though AWS was still handing back a &lt;code&gt;NextMarker&lt;/code&gt; and had more functions to give. There was no error, no thrown exception, no log line. The function just returned early with a partial list and reported success.&lt;/p&gt;

&lt;p&gt;For a small project this never surfaces. Most side projects don't have 200 Lambda functions. But "common in larger orgs," as the issue itself puts it, is exactly the audience infrawise is trying to win — teams with enough infrastructure that pasting schemas into a prompt every session actually hurts. Those are the accounts most likely to hit the cap, and the ones with the least tolerance for a tool that quietly drops data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why silent is worse than wrong
&lt;/h2&gt;

&lt;p&gt;Infrawise's own docs describe two pillars: &lt;em&gt;Context&lt;/em&gt; — give the AI assistant your real schema instead of making the developer paste it — and &lt;em&gt;Guard&lt;/em&gt; — catch expensive infrastructure mistakes before they ship. The 200-function cap broke both, and it broke them without telling anyone.&lt;/p&gt;

&lt;p&gt;Every function past the cap disappeared from the graph entirely — no node, no edges, nothing for the graph engine to attach event-source mappings, IAM roles, or trigger metadata to. Three consumer paths inherited that gap directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LambdaMissingTriggerDLQAnalyzer&lt;/code&gt; — the analyzer that flags Lambda triggers with no dead-letter queue — silently produced zero findings for those functions. Not "skipped, see warning." Zero findings, same as a function that's perfectly configured.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyze_function&lt;/code&gt;, the MCP tool an AI assistant calls before writing or reviewing a handler, would report no issues for a function it never saw, rather than saying it didn't have data.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_lambda_overview&lt;/code&gt;, the tool that lists every function's runtime, memory, timeout, and triggers, just wouldn't include the function in the list at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these paths distinguish "checked and found nothing wrong" from "never checked." That distinction is the entire value proposition of a deterministic tool. Infrawise's pitch, explicit in its own docs, is that it doesn't use an LLM to analyze infrastructure — everything is AST parsing, schema introspection, and rule-based analyzers, specifically so an AI assistant gets ground truth instead of a guess. A silent truncation at 200 functions turns that ground truth into a guess anyway, just one dressed up as certainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pattern, not a one-off
&lt;/h2&gt;

&lt;p&gt;The 200-function cap wasn't an isolated mistake — it's one of several bugs in Infrawise's own issue tracker that share the same shape: a limit or a stale value fails quietly instead of loudly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Sidd27/infrawise/issues/45" rel="noopener noreferrer"&gt;Issue #45&lt;/a&gt; found that &lt;code&gt;CACHE_DIR&lt;/code&gt; was computed from &lt;code&gt;process.cwd()&lt;/code&gt; at module load time, so running &lt;code&gt;infrawise analyze&lt;/code&gt; from one directory and &lt;code&gt;infrawise dev&lt;/code&gt; from another meant the second command silently read a cache built for a different invocation path — no mismatch warning, just answers that looked current and weren't. &lt;a href="https://github.com/Sidd27/infrawise/issues/38" rel="noopener noreferrer"&gt;Issue #38&lt;/a&gt; found that &lt;code&gt;--no-cache&lt;/code&gt; was accepted by the CLI parser but never actually read by &lt;code&gt;runAnalyze&lt;/code&gt;, so a developer explicitly asking for a fresh scan got a cached one anyway, with no indication the flag had been ignored. And &lt;a href="https://github.com/Sidd27/infrawise/issues/39" rel="noopener noreferrer"&gt;issue #39&lt;/a&gt; found that &lt;code&gt;LambdaMissingTriggerDLQAnalyzer&lt;/code&gt; was wired into the one-time &lt;code&gt;infrawise analyze&lt;/code&gt; path but missing from &lt;code&gt;runCodeRefresh&lt;/code&gt;, the path the file watcher calls on every save — so DLQ findings that showed up on the first run quietly vanished after the first edit, with the developer none the wiser.&lt;/p&gt;

&lt;p&gt;Different subsystems, same failure mode: nothing crashed, nothing logged, the tool just answered with less than it should have and gave no sign of it. For a tool that stakes its value on being more trustworthy than an LLM's guess, "wrong but confident" is the one failure mode it can't afford — and it's the one that shipped three separate times before anyone caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The fix for #40 was a one-line diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- } while (marker &amp;amp;&amp;amp; functions.length &amp;lt; 200);
&lt;/span&gt;&lt;span class="gi"&gt;+ } while (marker);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pagination now runs until AWS says there's nothing left, full stop. The fix direction noted in the issue also called for a warning log if pagination ever gets cut short for another reason — worth keeping in mind if you're building a similar extraction loop: an arbitrary safety cap on a paginated API call is rarely wrong to have, but silence when it triggers always is. A &lt;code&gt;logger.warn&lt;/code&gt; costs one line and turns a silent data gap into a debuggable one.&lt;/p&gt;

&lt;p&gt;If you're pointing an AI assistant at your own AWS account and it has more than 200 Lambda functions, this fix landed in the same release cycle as the other three — worth confirming you're on a current version.&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;A pagination cap with no warning is a silent data-loss bug waiting for an account large enough to trigger it — 200 felt generous until "larger orgs" turned out to be the target audience.&lt;/li&gt;
&lt;li&gt;Distinguish "checked and found nothing" from "never checked" in any analyzer or query result — collapsing them into the same empty response is the failure mode that erodes trust fastest.&lt;/li&gt;
&lt;li&gt;Audit every loop bounded by both a natural termination condition (a pagination marker) and an arbitrary one (a hardcoded count) — the arbitrary one is usually the one that fails silently.&lt;/li&gt;
&lt;li&gt;Cache keys derived from &lt;code&gt;process.cwd()&lt;/code&gt; or other invocation-time state can go stale in ways a static cache TTL never catches — tie cache identity to something stable, not to where the process happened to start.&lt;/li&gt;
&lt;li&gt;If a CLI flag exists, verify the code path actually reads it. &lt;code&gt;--no-cache&lt;/code&gt; sitting in the parser but unused is exactly the kind of gap that only turns up when someone reads the issue tracker, not the tests.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>aws</category>
      <category>debugging</category>
    </item>
    <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>
  </channel>
</rss>
