<?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: Avneet bansal</title>
    <description>The latest articles on DEV Community by Avneet bansal (@avneet_bansal_a65b3f31fc4).</description>
    <link>https://dev.to/avneet_bansal_a65b3f31fc4</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%2F4116498%2F405a55bb-2d18-4b5d-b61c-7fc909692a12.jpg</url>
      <title>DEV Community: Avneet bansal</title>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/avneet_bansal_a65b3f31fc4"/>
    <language>en</language>
    <item>
      <title>A Field Guide to Beating Amazon Bedrock Throttling</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:45:01 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/a-field-guide-to-beating-amazon-bedrock-throttling-1b9g</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/a-field-guide-to-beating-amazon-bedrock-throttling-1b9g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnku1c3qv1ochqkmp4qk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnku1c3qv1ochqkmp4qk.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you run real LLM traffic through Amazon Bedrock, throttling is not an edge case. It is the normal operating condition at scale. A &lt;code&gt;ThrottlingException&lt;/code&gt; or &lt;code&gt;TooManyRequestsException&lt;/code&gt; is Bedrock telling you that your request rate or token rate crossed a quota for that model in that region. The fix is rarely one setting. It is a layered defense: understand the quota, retry intelligently, cap concurrency, degrade gracefully, handle timeouts, and know what to escalate to AWS.&lt;/p&gt;

&lt;p&gt;This is the set of strategies I run across production LLM workloads, written as a generic guide. No workload specifics, just the patterns and when each one earns its place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Know your quota first: check Service Quotas, learn which limits are adjustable (soft) and which are not (hard).&lt;/li&gt;
&lt;li&gt;Retry with exponential backoff and full jitter on throttling errors. This is the floor, not the ceiling.&lt;/li&gt;
&lt;li&gt;Cap concurrency so you stop generating load the quota cannot absorb.&lt;/li&gt;
&lt;li&gt;Add a circuit breaker so a throttle storm backs off instead of hammering.&lt;/li&gt;
&lt;li&gt;Handle timeouts separately: shrink the batch for model timeouts, tune and retry for client read timeouts.&lt;/li&gt;
&lt;li&gt;Use cross-region inference profiles to spread token demand across regions.&lt;/li&gt;
&lt;li&gt;Reserve provisioned throughput only when on-demand plus the above is not enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step one: know the quota you are hitting
&lt;/h2&gt;

&lt;p&gt;Before writing a line of retry code, find the actual limit. Bedrock quotas are per model, per region, and split by type: tokens per minute (TPM), requests per minute (RPM), and for some models separate input/output token quotas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html" rel="noopener noreferrer"&gt;Quotas for Amazon Bedrock&lt;/a&gt; — the overview, including that default quotas vary by account age, usage history, and region.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-runtime.html" rel="noopener noreferrer"&gt;Quotas for the bedrock-runtime endpoint&lt;/a&gt; — the runtime invoke limits you hit at inference time. View them in the Service Quotas console under Amazon Bedrock.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/scaling-throughput-best-practices.html" rel="noopener noreferrer"&gt;Scaling and throughput best practices&lt;/a&gt; — how per-model TPM and RPM quotas interact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Soft limits vs hard limits
&lt;/h3&gt;

&lt;p&gt;This distinction decides your whole strategy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Soft limits (adjustable).&lt;/strong&gt; In the &lt;a href="https://docs.aws.amazon.com/general/latest/gr/bedrock.html" rel="noopener noreferrer"&gt;Amazon Bedrock endpoints and quotas&lt;/a&gt; table these are marked &lt;code&gt;Adjustable: Yes&lt;/code&gt;. You can raise them. Request an increase through &lt;a href="https://docs.aws.amazon.com/servicequotas/latest/userguide/request-quota-increase.html" rel="noopener noreferrer"&gt;Service Quotas&lt;/a&gt;; smaller increases often auto-approve, larger ones route to AWS Support for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard limits (not adjustable).&lt;/strong&gt; Marked &lt;code&gt;Adjustable: No&lt;/code&gt;. No amount of asking moves these. Your only lever is architectural: reduce demand, spread it across regions, or change the access pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule: if the limit is soft and you are consistently near it, file the increase and also build the defenses below, because the increase takes time and never makes throttling impossible. If the limit is hard, skip the request entirely and go straight to architecture.&lt;/p&gt;

&lt;p&gt;One often-missed detail: Bedrock reserves TPM quota at the start of a request based on input tokens plus &lt;code&gt;maxTokens&lt;/code&gt;. If you leave &lt;code&gt;maxTokens&lt;/code&gt; unset, it can default to the model maximum and reserve far more quota than your call actually needs, so you throttle earlier than your real token use suggests. Set &lt;code&gt;maxTokens&lt;/code&gt; to a realistic ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer one: retry with backoff and jitter
&lt;/h2&gt;

&lt;p&gt;The baseline defense. Catch the throttling error codes (&lt;code&gt;ThrottlingException&lt;/code&gt;, &lt;code&gt;TooManyRequestsException&lt;/code&gt;), wait, and try again. Two details separate a good retry from a harmful one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff.&lt;/strong&gt; Each attempt waits longer: &lt;code&gt;base * 2^attempt&lt;/code&gt;, capped at a ceiling. This gives the quota window time to refill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full jitter.&lt;/strong&gt; Randomize the delay inside that window. Without jitter, every throttled caller retries at the same instant and you get a synchronized stampede that throttles again. With jitter, retries spread out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A compact pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="n"&gt;THROTTLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ThrottlingException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TooManyRequestsException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;THROTTLE&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# full jitter
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on the boto3 client's own retries. Boto3 has a built-in retry mode (&lt;code&gt;standard&lt;/code&gt; or &lt;code&gt;adaptive&lt;/code&gt;) you set via &lt;code&gt;botocore.config.Config(retries=...)&lt;/code&gt;. You can lean on it, or disable it (&lt;code&gt;max_attempts=0&lt;/code&gt;) and own the retry loop yourself. Owning it is worth the effort when you want the backoff, jitter, and error classification to match the rest of your system rather than two retry layers fighting each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same retry in a Step Functions workflow
&lt;/h3&gt;

&lt;p&gt;If your Bedrock calls run inside a Step Functions state machine, you do not write the backoff loop by hand. You declare it on the task's &lt;code&gt;Retry&lt;/code&gt; block and the service applies exponential backoff with jitter for you. &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/concepts-error-handling.html" rel="noopener noreferrer"&gt;Step Functions error handling&lt;/a&gt; added a &lt;code&gt;JitterStrategy&lt;/code&gt; field so wait times are spread instead of synchronized, the same reason full jitter matters in code.&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;"InvokeBedrock"&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;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&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:states:::bedrock:invokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&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;"ErrorEquals"&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;"ThrottlingException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TooManyRequestsException"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MaxDelaySeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"JitterStrategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FULL"&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;"Catch"&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;"ErrorEquals"&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;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HumanReview"&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;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NextStep"&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;The fields map onto the code version: &lt;code&gt;IntervalSeconds&lt;/code&gt; is the base delay, &lt;code&gt;BackoffRate&lt;/code&gt; is the exponential multiplier, &lt;code&gt;MaxDelaySeconds&lt;/code&gt; is the cap, &lt;code&gt;MaxAttempts&lt;/code&gt; is the retry count, and &lt;code&gt;JitterStrategy: FULL&lt;/code&gt; randomizes each wait. The &lt;code&gt;Catch&lt;/code&gt; routes to a fallback once retries are exhausted, for example a human-review step, so an exhausted retry degrades gracefully instead of failing the whole execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer two: cap concurrency
&lt;/h2&gt;

&lt;p&gt;Retries handle the request that already failed. Concurrency caps stop you from generating more load than the quota can absorb in the first place. If 50 workers all call the same model at once, you will throttle no matter how clever the backoff is.&lt;/p&gt;

&lt;p&gt;Put a ceiling on in-flight Bedrock calls. In async Python a semaphore does it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# max concurrent Bedrock calls
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bounded_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upstream of the application, cap concurrency at the infrastructure layer too. If documents arrive as events, put them on an SQS queue and let the SQS-to-Lambda event source mapping enforce a &lt;code&gt;maximum_concurrency&lt;/code&gt;. Work waits in the queue until a slot frees, with no polling and no custom counters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_event_source_mapping"&lt;/span&gt; &lt;span class="s2"&gt;"sqs_to_worker"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event_source_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;batch_size&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;scaling_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;maximum_concurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;Match the concurrency ceiling to the quota, not to how many workers you could theoretically run. The right number is the one that keeps steady-state token rate under the TPM limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer three: a circuit breaker for throttle storms
&lt;/h2&gt;

&lt;p&gt;Backoff and concurrency caps handle normal pressure. A sustained throttle storm needs something that steps back harder. A circuit breaker watches the throttle rate and, once it crosses a threshold inside a time window, pauses all calls for a cooldown, then resumes at reduced concurrency before restoring full throughput.&lt;/p&gt;

&lt;p&gt;The state machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Closed&lt;/strong&gt; (normal): calls flow, throttles are counted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open&lt;/strong&gt; (tripped): after N throttles in the window, pause all calls for a cooldown and halve the concurrency limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Half-open&lt;/strong&gt; (recovering): after the cooldown, resume at the reduced limit; if clean for a while, restore full concurrency; if it throttles again, reopen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This converts a feedback loop that makes throttling worse (retry, throttle, retry, throttle) into one that actively sheds load until Bedrock recovers. It pairs with the concurrency limiter: the breaker is what turns the limit down and back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer four: handle timeouts, they are not throttling
&lt;/h2&gt;

&lt;p&gt;Throttling rarely travels alone, and the loudest neighbor is the timeout. There are two distinct kinds, and they want opposite fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model timeout: shrink the work
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;ModelTimeoutException&lt;/code&gt; means the request itself was too large to finish in the model's time budget. It is not a rate problem, so retrying the same payload will time it out again. The fix is to reduce the work: halve the batch or chunk the input, then retry smaller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_shrinking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ModelTimeoutException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="c1"&gt;# halve and retry
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Client timeout: tune the budget, then retry the transient ones
&lt;/h3&gt;

&lt;p&gt;A client-side &lt;code&gt;ReadTimeoutError&lt;/code&gt; or &lt;code&gt;ConnectTimeoutError&lt;/code&gt; is different again: the SDK gave up waiting before the model answered. Large prompts and long generations routinely exceed the boto3 default read timeout, which produces timeouts that look like failures but are really an impatient client.&lt;/p&gt;

&lt;p&gt;Two moves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set a realistic &lt;code&gt;read_timeout&lt;/code&gt;&lt;/strong&gt; on the boto3 client so a slow-but-valid response is not cut off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the transient connection timeouts as retryable&lt;/strong&gt;, alongside throttling, rather than letting them bubble up as hard failures.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ThrottlingException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TooManyRequestsException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# also retry ReadTimeoutError / ConnectTimeoutError by exception class name
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Classify before reacting. A throttle wants backoff, a model timeout wants a smaller batch, a client timeout wants a longer budget plus a retry, and a &lt;code&gt;ValidationException&lt;/code&gt; wants no retry at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer five: spread demand with cross-region inference profiles
&lt;/h2&gt;

&lt;p&gt;Quotas are per region. A &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles.html" rel="noopener noreferrer"&gt;cross-region inference profile&lt;/a&gt; lets a single on-demand call be served from one of several regions, so your token demand is spread across each region's quota instead of piling onto one. For on-demand workloads this is often the highest-leverage change: no capacity to buy, and the effective ceiling goes up because you are drawing from multiple regional pools.&lt;/p&gt;

&lt;p&gt;It is not infinite headroom, and it does not change a hard per-model limit in a single region, but for spiky on-demand traffic it buys real room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer six: provisioned throughput, only when you need it
&lt;/h2&gt;

&lt;p&gt;If on-demand plus everything above still throttles under predictable heavy load, Bedrock offers &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/prov-throughput.html" rel="noopener noreferrer"&gt;Provisioned Throughput&lt;/a&gt;: reserved capacity (model units) with a guaranteed token rate, bought by commitment. It removes throttling for that reserved slice because the capacity is yours.&lt;/p&gt;

&lt;p&gt;Treat it as the last layer, not the first. It costs whether or not you use it, so it only makes sense for steady, predictable volume. For bursty or unpredictable traffic, retries, concurrency caps, and cross-region profiles on on-demand are usually the better economics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning in practice
&lt;/h2&gt;

&lt;p&gt;The layers above are the toolbox. How hard you turn each knob depends on the workload, and a couple of patterns are worth stating plainly.&lt;/p&gt;

&lt;p&gt;Match the aggressiveness of retries to the cost of a lost call. Heavy, expensive calls justify more retry attempts, a higher backoff ceiling, and tighter concurrency, because losing one is costly. High-volume, cheaper work is better served by an upstream queue that caps concurrency and a workflow-level self-retry that ends in a manual-review fallback, keeping the per-call retries light. There is no single correct number; the right setting is the one that keeps steady-state token rate under the quota for that workload.&lt;/p&gt;

&lt;p&gt;Two caveats that are easy to get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A "fallback model" can quietly resolve to the same model.&lt;/strong&gt; If both the primary and fallback point at the same model id through shared config, the fallback functions as extra retries against one model, not as model diversity. Calling it a fallback overstates what it does until the config points at a genuinely different model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry, backoff, and concurrency are not the same as reserved capacity.&lt;/strong&gt; A defense built entirely from retries, backoff, concurrency caps, and on-demand cross-region inference profiles has no reserved-capacity floor under a true spike. That can be a deliberate on-demand posture, but name it as one, because on-demand plus retries still has a ceiling.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to decide what to reach for
&lt;/h2&gt;

&lt;p&gt;A short decision order when you see throttling:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the quota.&lt;/strong&gt; Is the limit soft or hard? If soft and you are near it, file the increase now, in parallel with the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm retries have backoff and jitter.&lt;/strong&gt; If not, fix that first. It is the cheapest win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap concurrency&lt;/strong&gt; to keep steady-state rate under the TPM limit, at both the app and the infrastructure layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a circuit breaker&lt;/strong&gt; if throttle storms are recurring rather than occasional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate timeouts from throttles.&lt;/strong&gt; Shrink the batch for model timeouts, tune the read timeout and retry for client timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spread across regions&lt;/strong&gt; with a cross-region inference profile for on-demand traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy provisioned throughput&lt;/strong&gt; only for steady, predictable, high volume the above cannot cover.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Throttling is not a wall you hit once and clear. It is a pressure you manage continuously. The systems that stay up are the ones that treat the quota as a first-class design constraint, not an exception to catch.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Moved My Prompts Into DynamoDB and the Throttling Went Away</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Sun, 18 Oct 2026 23:03:50 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/i-moved-my-prompts-into-dynamodb-and-the-throttling-went-away-3h5o</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/i-moved-my-prompts-into-dynamodb-and-the-throttling-went-away-3h5o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F309p7u33h98257y0elqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F309p7u33h98257y0elqw.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
Prompts feel like configuration, so it is tempting to park them in whatever managed prompt-management service your platform hands you. You get versioning, a console, and a clean API, and for a while that is the right call. The trouble shows up later, under load, when prompt retrieval sits on the hot path of every inference call and the service starts pushing back.&lt;/p&gt;

&lt;p&gt;That is the wall I hit. The prompt-management layer was throttling requests. Because every call had to fetch its prompt before doing any real work, that throttle became a ceiling on the whole pipeline. The model was not the bottleneck. Prompt lookup was.&lt;/p&gt;

&lt;p&gt;I moved prompt serving into DynamoDB and kept the managed service as the authoring source of truth. The throttling went away, and the design has been running well since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a prompt-management service becomes the bottleneck
&lt;/h2&gt;

&lt;p&gt;Prompt-management services are built for authoring and governance, not for being read thousands of times a minute on a latency-sensitive path. They come with request quotas, and those quotas are usually shared across the account rather than scaled to one workload's read traffic. When read volume climbs, you hit the quota long before you hit any limit that has to do with the prompts themselves.&lt;/p&gt;

&lt;p&gt;The tell is subtle at first. You start seeing throttling and too-many-requests errors on prompt retrieval, so you add retries with exponential backoff and maybe a fallback for when the service says no. That is reasonable hygiene, but once you are wrapping a plain config lookup in retry logic, the store has quietly become the throughput limit of the application. You can optimize the model, batch the work, and tune everything downstream, and none of it matters, because the request is already stalled waiting for its prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DynamoDB fits this access pattern
&lt;/h2&gt;

&lt;p&gt;A prompt lookup is about the simplest access pattern there is. You have a key, and you want the item back. That is a single-item read by primary key, which is exactly what DynamoDB is good at.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read throughput scales with the table, not a fixed service quota.&lt;/strong&gt; On-demand capacity absorbs spikes and provisioned capacity with auto scaling handles steady load. The shared quota that was throttling me no longer sat on the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The access pattern is a key lookup.&lt;/strong&gt; Partition on the prompt identifier and every fetch is a single &lt;code&gt;GetItem&lt;/code&gt;. No scans, no filtering, predictable single-digit-millisecond reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads and writes separate cleanly.&lt;/strong&gt; High-volume reads hit DynamoDB. The rare authoring writes stay in the managed service where review and governance already live, and a sync step pushes changes down into DynamoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that makes it safe: checksum-based sync
&lt;/h2&gt;

&lt;p&gt;Caching prompts is easy. Keeping the cache honest is the part worth getting right.&lt;/p&gt;

&lt;p&gt;The managed service stays the source of truth for prompt content. DynamoDB holds the serving copy that the hot path reads. To keep them in agreement without re-uploading everything on every deploy, I sync on a checksum.&lt;/p&gt;

&lt;p&gt;The sync is write-through. When it detects a change at the source, it writes the new prompt into DynamoDB first, and readers pick it up from there afterward. The flow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For each prompt in the source of truth, compute a checksum of its content.&lt;/li&gt;
&lt;li&gt;Compare that checksum against what is already stored for that prompt.&lt;/li&gt;
&lt;li&gt;If they match, do nothing. If they differ, the prompt changed, so write the new content into DynamoDB and record the new checksum.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This turns every sync into a cheap compare instead of a blind rewrite. Unchanged prompts cost a comparison and no write. Only a genuine edit at the source triggers a write. Because the write lands before any read needs it, a new prompt propagates cleanly: the serving copy is already updated by the time the hot path asks for it.&lt;/p&gt;

&lt;p&gt;The nice property is that the checksum is the change signal. You do not need a separate "is this dirty" flag or a manual version bump. If the content is identical, the checksum is identical, and the write is skipped. If someone edits a prompt, the checksum moves and the new text is written on the next sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you need rollback or audit: versioning in DynamoDB
&lt;/h2&gt;

&lt;p&gt;DynamoDB does not hand you versioning the way a prompt-management service does, but you can model it, and it is worth saying so plainly because "DynamoDB has no versioning" is a common misread.&lt;/p&gt;

&lt;p&gt;If a single current copy per prompt is all you need, the checksum layout above is enough. If you want history, rollback, or an audit trail, add a version to the key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition key:&lt;/strong&gt; the prompt identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort key:&lt;/strong&gt; the version. A monotonic integer, an ISO timestamp, or the checksum itself all work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A current pointer:&lt;/strong&gt; one extra item per prompt (for example sort key &lt;code&gt;CURRENT&lt;/code&gt;) that names the active version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hot path reads the current pointer by key, so serving stays a single keyed read. History stays queryable with a &lt;code&gt;Query&lt;/code&gt; on the partition key. The checksum still decides when a new version row gets written, so you are not stacking a new version for an unchanged prompt. If concurrent syncs are possible, a conditional write (&lt;code&gt;ConditionExpression&lt;/code&gt; on the stored checksum) keeps a stale sync from overwriting a newer one.&lt;/p&gt;

&lt;p&gt;The tradeoff is simple: the single-copy layout is less to manage, the versioned layout buys rollback and audit. Both are key reads on the hot path, so neither reintroduces the throttling problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the change
&lt;/h2&gt;

&lt;p&gt;The migration itself was small, which is part of why it was the right call.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model the table around the lookup.&lt;/strong&gt; Partition key on the prompt identifier. That is the whole schema, because the access pattern was known up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read by key on the hot path.&lt;/strong&gt; Each inference call does a single keyed read. No scan runs against this table in production, so cost stays proportional to the reads you actually make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync by checksum, not on a timer or on every deploy.&lt;/strong&gt; The source of truth drives updates, and only real changes cross into DynamoDB.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is that prompt retrieval stopped being a shared, rate-limited dependency and became a key read against a store that scales with the traffic hitting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone before they copy this
&lt;/h2&gt;

&lt;p&gt;This is not an argument against prompt-management services. They earn their place for authoring, review, and governance. The mistake is letting that same service sit on a high-volume read path it was never sized for.&lt;/p&gt;

&lt;p&gt;A few things worth keeping in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The symptom is a throughput ceiling that tracks request volume, not model latency.&lt;/strong&gt; If you are wrapping prompt retrieval in throttling retries, that is the tell. No downstream tuning will move it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design the table around the access pattern first.&lt;/strong&gt; A key lookup wants a key schema. Pick the partition key from the query you actually run, and keep scans out of production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the cache self-correcting.&lt;/strong&gt; A checksum compare against the source of truth means the write-through sync updates the serving copy only when content actually changes, without manual version juggling and without rewriting unchanged data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model versioning if you need it, do not assume it is missing.&lt;/strong&gt; A sort-key version plus a current pointer gives you rollback and audit while keeping the hot path a single keyed read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving prompts into DynamoDB was a small change with a disproportionate payoff. The throttling that was capping the pipeline is gone, retrieval is a clean key lookup, and the checksum sync keeps the serving copy honest against the source of truth. It has held up in production since.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dynamodb</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>The GitOps Bridge: Keep Terraform and ArgoCD Out of Each Other's Way</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Mon, 12 Oct 2026 04:18:15 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/the-gitops-bridge-keep-terraform-and-argocd-out-of-each-others-way-3721</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/the-gitops-bridge-keep-terraform-and-argocd-out-of-each-others-way-3721</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7gycz6ve8i4o2igmoawn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7gycz6ve8i4o2igmoawn.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
Two tools, two clocks. Terraform provisions the cluster, the node groups, the networking, the IAM. It changes rarely and under heavy approval. ArgoCD deploys the applications onto that cluster. It changes many times a day under light approval. The trouble starts when you let them reach into each other, because then a routine app deploy can be blocked behind an infrastructure change, and an infrastructure change can take applications down with it. The fix is not a better tool. It is a thin contract between the two, so each stays in its own lane. I used this to compress a three-environment EKS migration from a planned six months to about three. Here is the contract, and what it actually bought.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure (Terraform) and applications (ArgoCD/Flux) have different change frequencies, approval flows, and blast radii. Coupling them is the root problem.&lt;/li&gt;
&lt;li&gt;The GitOps Bridge is a narrow handoff: Terraform writes cluster facts into a well-defined interface (ConfigMap/Secret labels and annotations), and GitOps controllers read that interface to deploy.&lt;/li&gt;
&lt;li&gt;Neither side imports the other's state. The interface is the only shared surface.&lt;/li&gt;
&lt;li&gt;On a real migration this let three environments come up reproducibly, with the customer executing the IaC themselves under a zero-access constraint, and cut the timeline roughly in half.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The coupling you do not notice until it hurts
&lt;/h2&gt;

&lt;p&gt;The common anti-pattern is to let Terraform deploy applications (via the Helm or kubectl provider) or to let application manifests carry infrastructure facts hard-coded inside them. Both feel convenient. Both create coupling that only shows its cost later.&lt;/p&gt;

&lt;p&gt;When Terraform deploys apps, every application change rides the Terraform lifecycle: a &lt;code&gt;terraform apply&lt;/code&gt; that should only touch a node group now also reconciles your workloads, and a drift in one app can fail a plan that was supposed to be about infrastructure. When app manifests hard-code infrastructure facts (the cluster's OIDC issuer, a role ARN, a VPC ID, the region), those manifests are no longer portable across environments, and every environment promotion becomes a hand-edit. The blast radius of each change grows to cover both layers, and the audit trail blurs, because you can no longer tell from the change history whether a given commit was an infrastructure decision or an application one.&lt;/p&gt;

&lt;p&gt;The two layers genuinely have different natures. Infrastructure changes are infrequent, high-risk, and want slow, careful approval. Application changes are frequent, lower-risk, and want to flow. Forcing them through one pipeline makes the fast side as slow as the slow side, and the slow side as risky as the fast side.&lt;/p&gt;
&lt;h2&gt;
  
  
  The contract
&lt;/h2&gt;

&lt;p&gt;The bridge is deliberately small. Terraform's job ends at provisioning the cluster and then publishing a set of facts about it into a known location. In practice that location is a ConfigMap and a Secret in a known namespace, carrying labels and annotations that describe the cluster: its name, region, the OIDC provider URL, role ARNs for workloads, networking identifiers, and any environment-specific values the apps will need. Terraform owns writing these. It never installs an application.&lt;/p&gt;

&lt;p&gt;ArgoCD's job begins by reading that interface. A root application points at the known ConfigMap/Secret, pulls the cluster facts, and uses them to parameterize everything it deploys. The app manifests themselves carry no hard-coded infrastructure values. They carry references into the bridge. ArgoCD owns reading these. It never provisions infrastructure.&lt;/p&gt;

&lt;p&gt;The important property is what is absent. Terraform does not read ArgoCD's state, and ArgoCD does not read Terraform's state. There is no shared state file, no circular dependency where each waits on the other. The only thing they share is the interface, and the interface is data, not control. Terraform writes facts; ArgoCD reads facts. That one-directional, data-only handoff is the whole design, and it is what lets each side keep its own clock.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this made a hard migration tractable
&lt;/h2&gt;

&lt;p&gt;The migration that proved it out had a constraint that is more common than people admit: I could not directly touch the target environments. The work had to be delivered as infrastructure-as-code that the customer ran themselves, across three environments, an internal lab, a sandbox, and production, with no interactive access to any of them.&lt;/p&gt;

&lt;p&gt;That constraint is exactly where the bridge pays off. Because the apps consume cluster facts through the interface rather than embedding them, the same application definitions deploy unchanged to all three environments. Each environment's Terraform writes its own values into its own bridge, and the GitOps layer adapts automatically. There was no per-environment hand-editing of manifests, which is where this kind of migration usually loses weeks and acquires drift. The customer could run the IaC in each environment and get a reproducible result, because the only thing that varied between environments lived in the bridge, generated by that environment's own provisioning.&lt;/p&gt;

&lt;p&gt;For one customer, the combination (Terraform plus the bridge plus GitOps, with Karpenter handling node provisioning on the cluster side) took a migration planned for about six months down to roughly three. The halving did not come from any single tool being fast. It came from removing the coupling that would otherwise have forced serial, hand-held, per-environment work. When each environment comes up the same way from the same definitions, you stop paying the integration tax three times.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to adopt it
&lt;/h2&gt;

&lt;p&gt;Start by drawing the line. List every value your application manifests currently hard-code that is really an infrastructure fact: cluster name, region, OIDC issuer, IAM role ARNs, subnet or security-group IDs, DNS zone names. Those are the contents of your bridge.&lt;/p&gt;

&lt;p&gt;Have Terraform write them. After it provisions the cluster, have it create a ConfigMap (for non-secret facts) and a Secret (for anything sensitive) in a known namespace, populated from its outputs. Decide the key names once and treat them as the contract; renaming a bridge key is now a breaking change, which is the right level of ceremony for it.&lt;/p&gt;

&lt;p&gt;Have GitOps read them. Point a root ArgoCD (or Flux) application at that namespace, and parameterize your app set from the bridge values rather than from literals. Each environment gets its own bridge from its own Terraform, and the same app definitions follow.&lt;/p&gt;

&lt;p&gt;A minimal, generic version of both sides looks like this. First, Terraform publishes the facts after it provisions the cluster (the values are illustrative placeholders, not from any real environment):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform: write the bridge after the cluster exists.&lt;/span&gt;
&lt;span class="c1"&gt;# The labels are what ArgoCD's ApplicationSet selects on; the data is the facts.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_config_map"&lt;/span&gt; &lt;span class="s2"&gt;"cluster_bridge"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cluster-bridge"&lt;/span&gt;
    &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"argocd"&lt;/span&gt;
    &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"bridge.example.com/environment"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="c1"&gt;# dev | staging | prod&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cluster_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
    &lt;span class="nx"&gt;oidc_issuer&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oidc_provider&lt;/span&gt;          &lt;span class="c1"&gt;# for IRSA&lt;/span&gt;
    &lt;span class="nx"&gt;account_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;
    &lt;span class="c1"&gt;# a workload's IAM role ARN, resolved per environment:&lt;/span&gt;
    &lt;span class="nx"&gt;app_irsa_role&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_irsa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam_role_arn&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;Then an ArgoCD ApplicationSet reads that ConfigMap and templates every app from it, so the app definitions themselves carry references, never literals:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ApplicationSet&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&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;platform-addons&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;clusters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# or a configMap/plugin generator reading the bridge&lt;/span&gt;
        &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;bridge.example.com/environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{name}}-addons'&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;project&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;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/your-org/platform-addons&lt;/span&gt;
        &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;addons&lt;/span&gt;
        &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# bridge facts flow in as values; no hard-coded ARNs or issuers in the chart&lt;/span&gt;
          &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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;region&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{metadata.annotations.region}}'&lt;/span&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;oidcIssuer&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{metadata.annotations.oidc_issuer}}'&lt;/span&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;appIrsaRole&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{metadata.annotations.app_irsa_role}}'&lt;/span&gt;
      &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{server}}'&lt;/span&gt;
        &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kube-system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact generator you use (cluster, configMap, or a plugin generator) depends on your setup; the invariant is the one that matters: Terraform owns the left side and writes facts, ArgoCD owns the right side and reads them, and the app charts reference bridge values instead of embedding them. Swap &lt;code&gt;prod&lt;/code&gt; for &lt;code&gt;dev&lt;/code&gt; or &lt;code&gt;staging&lt;/code&gt; and nothing in the app definitions changes, only the bridge each environment's Terraform wrote.&lt;/p&gt;

&lt;p&gt;Hold the boundary. The one discipline that keeps this working is refusing to cross the line later under deadline pressure: no Helm-provider app installs sneaking into Terraform, no infrastructure ARNs sneaking back into manifests. The value is entirely in the separation, so the separation is the thing to protect.&lt;/p&gt;

&lt;p&gt;The reward is that your fast layer stays fast and your slow layer stays safe. An application deploy no longer waits on an infrastructure plan. An infrastructure change no longer reconciles your workloads as a side effect. And a new environment is a new bridge, not a new round of manual edits. Keep the two clocks separate, and let a thin, one-way, data-only contract be the only thing between them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you run Terraform and a GitOps controller together, it is worth asking which infrastructure facts your app manifests still hard-code. Those are the ones the bridge is for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>terraform</category>
      <category>gitops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your Readiness Probe Is Lying to You</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Sat, 10 Oct 2026 21:14:36 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/your-readiness-probe-is-lying-to-you-5e1g</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/your-readiness-probe-is-lying-to-you-5e1g</guid>
      <description>&lt;p&gt;A readiness probe is a promise. It tells Kubernetes "route traffic to me, I can handle it." Most of the time the promise is kept because the probe checks the thing that actually matters. The failures worth writing about are the ones where the probe keeps saying Ready while the component has quietly lost the one connection it needs to do any useful work. The traffic keeps arriving. None of it can be served. Nothing looks broken on the dashboard.&lt;/p&gt;

&lt;p&gt;I hit exactly this in KEDA, the CNCF event-driven autoscaler, and the fix merged as &lt;a href="https://github.com/kedacore/keda/pull/8226" rel="noopener noreferrer"&gt;kedacore/keda #8226&lt;/a&gt;. The bug is a good teaching case on its own. The part I did not expect was the second bug hiding inside the fix, which a maintainer caught in review.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KEDA's Metrics Server reported Ready even when its gRPC connection to the keda-operator was down.&lt;/li&gt;
&lt;li&gt;In that state the HPA silently stops receiving scale signals, while the replica still looks healthy.&lt;/li&gt;
&lt;li&gt;The fix makes the readiness probe observe the real gRPC connection state and report NotReady when the connection is unusable.&lt;/li&gt;
&lt;li&gt;A naive version of that fix has a trap: once a replica goes NotReady it stops receiving RPCs, so an Idle connection never gets woken and the replica can stay NotReady forever.&lt;/li&gt;
&lt;li&gt;The merged fix nudges an Idle connection to reconnect from inside the readiness check itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup: who talks to whom
&lt;/h2&gt;

&lt;p&gt;KEDA has two moving parts here. The keda-operator does the actual work of talking to scalers and computing metrics. The Metrics Server (a custom metrics API server) is what the Kubernetes HPA queries when it wants to know "how many of these should be running." The Metrics Server does not compute metrics itself. It forwards the request to the operator over a gRPC connection and relays the answer.&lt;/p&gt;

&lt;p&gt;That gRPC connection is the Metrics Server's entire reason to exist. If it is down, the Metrics Server is a shell. It can accept an HPA query and has nothing to answer it with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lie
&lt;/h2&gt;

&lt;p&gt;The Metrics Server's readiness probe did not check that connection. It reported Ready as long as the process was up and its HTTP server was listening. So when the gRPC connection to the operator dropped, the following happened, all at once and all invisible: the replica stayed Ready, Kubernetes kept it in the APIService endpoints, the HPA kept sending it metric queries, and every one of those queries failed or hung because there was no live operator connection behind them. The autoscaler stops scaling, and the one signal you would look at, pod readiness, is green.&lt;/p&gt;

&lt;p&gt;This is the specific shape of outage that is worst to debug. A crash is loud. A readiness probe that passes while the component cannot serve is silent, and it points you away from the real cause, because the first thing anyone checks is "are the pods ready," and they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, version one
&lt;/h2&gt;

&lt;p&gt;The fix is conceptually simple. Make readiness reflect the dependency. gRPC already tracks connection state through &lt;code&gt;grpc.ClientConn.GetState()&lt;/code&gt;, which returns one of &lt;code&gt;Idle&lt;/code&gt;, &lt;code&gt;Connecting&lt;/code&gt;, &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;TransientFailure&lt;/code&gt;, or &lt;code&gt;Shutdown&lt;/code&gt;. Crucially, &lt;code&gt;GetState()&lt;/code&gt; is a pure observer. It reports the current state and never initiates a connection, so it is safe to call from a probe with no side effects.&lt;/p&gt;

&lt;p&gt;So version one added a small accessor on the metrics-service client that returned the connection state, and wired a &lt;code&gt;readyz&lt;/code&gt; check in the adapter that reported NotReady unless the connection was &lt;code&gt;Ready&lt;/code&gt;. When the operator connection drops, the probe fails, Kubernetes pulls the replica from the APIService endpoints, and the HPA stops sending it queries it cannot answer. When the connection recovers, the probe passes again. That is the behavior you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug, caught in review
&lt;/h2&gt;

&lt;p&gt;Here is the part I like. A maintainer looked at version one and pointed out a failure mode that only exists because the fix works.&lt;/p&gt;

&lt;p&gt;Think through what happens after the probe correctly reports NotReady. The replica gets removed from the APIService endpoints. Now no RPCs flow through that client at all, because nothing is routed to it. And gRPC, by default, parks a connection in the &lt;code&gt;Idle&lt;/code&gt; state after a period with no RPCs (&lt;code&gt;GRPC_CLIENT_IDLE_TIMEOUT_MS&lt;/code&gt;, 30 minutes by default). An idle gRPC connection does not reconnect on its own. It waits for someone to use it.&lt;/p&gt;

&lt;p&gt;So the naive fix can deadlock the recovery. The connection goes bad, the probe reports NotReady, the replica stops getting RPCs, the connection drifts to Idle, and nothing ever wakes it, because the only thing that would wake it is an RPC, and RPCs only resume once the probe reports Ready again, which requires the connection to be awake. The replica can sit NotReady permanently, needing a manual restart to recover. The fix for a silent hang would have introduced a silent hang.&lt;/p&gt;

&lt;p&gt;The resolution is to make the readiness check active, not passive, in exactly one narrow case. When the accessor observes an &lt;code&gt;Idle&lt;/code&gt; connection, it triggers a non-blocking reconnect before returning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// GetConnectionState returns the current connectivity state of the underlying&lt;/span&gt;
&lt;span class="c"&gt;// gRPC connection to the KEDA metrics service.&lt;/span&gt;
&lt;span class="c"&gt;//&lt;/span&gt;
&lt;span class="c"&gt;// When the connection is Idle it also triggers a non-blocking reconnection&lt;/span&gt;
&lt;span class="c"&gt;// attempt (moving it towards Connecting) before returning. Once a non-Ready&lt;/span&gt;
&lt;span class="c"&gt;// state removes the replica from the APIService endpoints, no RPCs flow through&lt;/span&gt;
&lt;span class="c"&gt;// this client, so nothing else would ever wake an Idle connection.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;GrpcClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetConnectionState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Idle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&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;Connect()&lt;/code&gt; is non-blocking and a no-op when the connection is not Idle, so it is safe to call from a probe. The probe still returns the pre-nudge state (so a just-nudged Idle connection reports NotReady that cycle), but the connection is now moving toward &lt;code&gt;Connecting&lt;/code&gt; and then &lt;code&gt;Ready&lt;/code&gt; on its own. The replica recovers without a human. The probe went from a pure observer to an observer-with-one-side-effect, and that single side effect is what closes the recovery loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;p&gt;The regression test does not need a cluster or a running operator. It exercises the connection-state logic directly with an in-process gRPC client, so you can run it from a clean checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 20 https://github.com/kedacore/keda.git
&lt;span class="nb"&gt;cd &lt;/span&gt;keda
go &lt;span class="nb"&gt;test&lt;/span&gt; ./pkg/metricsservice/... &lt;span class="nt"&gt;-run&lt;/span&gt; &lt;span class="s1"&gt;'TestGetConnectionState'&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tests cover the two behaviors above. One asserts that a freshly created client (gRPC does not dial until first use) is not &lt;code&gt;Ready&lt;/code&gt;, so a probe built on it correctly reports NotReady for a replica that has not connected yet. The other asserts the review fix: calling the accessor on an &lt;code&gt;Idle&lt;/code&gt; connection moves it off Idle within a couple of seconds without any RPC being issued, so a sidelined replica can recover on its own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expected: both subtests pass. (If you are on a network that cannot reach the Go module proxy, the clone's large dependency tree may need &lt;code&gt;go clean -modcache&lt;/code&gt; or a working &lt;code&gt;GOPROXY&lt;/code&gt; first.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How to adopt the pattern
&lt;/h2&gt;

&lt;p&gt;The KEDA-specific takeaway is small: upgrade to a release that includes #8226 and your Metrics Server will stop advertising readiness while blind to the operator.&lt;/p&gt;

&lt;p&gt;The portable takeaway is the one worth internalizing. A readiness probe should check the dependency that the component cannot work without, not just that the process is up. For a forwarding service, that dependency is the downstream connection. And when you make a probe gate traffic on a connection's health, check whether removing traffic from an unhealthy replica also removes the thing that would let it recover. If recovery depends on activity, and your probe stops activity, you need the probe itself to drive recovery, carefully and without blocking. Check the real dependency, and make sure the healthy path back exists.&lt;/p&gt;

&lt;p&gt;A probe that only ever reports the process is alive is answering a question nobody asked. The question Kubernetes is actually asking is "can you serve." Answer that one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you run forwarding or adapter services behind readiness probes, it is worth auditing what they actually check. The gap between "process is up" and "can serve" is where the quiet outages live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>devops</category>
      <category>grpc</category>
    </item>
    <item>
      <title>A Prompt-Injection Firewall for AWS: What I'd Build, and What I Measured</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Sat, 10 Oct 2026 20:10:28 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/a-prompt-injection-firewall-for-aws-what-id-build-and-what-i-measured-14b4</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/a-prompt-injection-firewall-for-aws-what-id-build-and-what-i-measured-14b4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmw44kpu3yjhskg3ekeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmw44kpu3yjhskg3ekeh.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt injection has sat at the top of the OWASP Top 10 for LLM Applications since the list began in 2023, and it is still number one in the 2025 and 2026 editions. That is unusual. Most vulnerability classes get a real fix and slide down the list. SQL injection had parameterized queries. XSS had output encoding and CSP. Prompt injection has no equivalent, because the flaw is architectural: an LLM reads the system prompt, the user's input, and any retrieved documents or tool output as one undifferentiated stream of text, with no built-in boundary between trusted instructions and untrusted data.&lt;/p&gt;

&lt;p&gt;I work on LLM systems on AWS, and this is the problem I keep coming back to. This post is not a product announcement. It is the design I would build if I were building a prompt-injection firewall today, grounded in what the research actually says and an honest account of what already exists. I will be clear throughout about which parts are established fact and which are my proposed approach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt injection is OWASP's #1 LLM risk across every edition, and a 2026 proof shows pure input-wrapper defenses face a hard trilemma: they cannot be continuous, utility-preserving, and complete at the same time.&lt;/li&gt;
&lt;li&gt;The space is no longer empty. NeuralTrust ships a commercial "Generative Application Firewall," Check Point acquired Lakera, Cloudflare has Firewall for AI, and open-source projects exist. The honest gap is a cloud-native, AWS-reference, agent-aware design, not "nobody has done this."&lt;/li&gt;
&lt;li&gt;My proposal: a multi-layer ensemble with a weighted decision engine, output and tool-call enforcement, and a feedback loop, built the trilemma into the design rather than pretending to beat it.&lt;/li&gt;
&lt;li&gt;I built the offline core (heuristic + vector detectors + decision engine) and benchmarked it on 285 labeled prompts. The measured trade-off is stark: at the threshold that catches 92% of attacks, 25% of legitimate prompts get flagged; tighten it until false positives are near zero and detection falls to 39%. No setting gives both. That is the trilemma, measured, and the argument for the semantic layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this problem resists a clean fix
&lt;/h2&gt;

&lt;p&gt;A 2026 paper, &lt;a href="https://arxiv.org/abs/2604.06436" rel="noopener noreferrer"&gt;"Why Prompt Injection Defense Wrappers Fail?"&lt;/a&gt;, formalizes something practitioners had felt for a while. It argues that a defense that wraps the input faces a trilemma of three properties that cannot all hold at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuity&lt;/strong&gt; — a small change to the input should not flip the defense decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utility preservation&lt;/strong&gt; — legitimate prompts must still get through and work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness&lt;/strong&gt; — every injection attempt must be caught.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can get two. Not three. The paper is explicit that this bounds &lt;em&gt;input-wrapper&lt;/em&gt; style defenses specifically, and that it does not rule out training-time alignment, architectural changes, or defenses that deliberately trade away some utility. That nuance matters, and I will come back to it, because it is the single most important design constraint.&lt;/p&gt;

&lt;p&gt;The practical consequence is that any honest firewall should stop promising completeness. It should instead be explicit about where on the continuity/utility/completeness surface it chooses to sit, and make that choice configurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What already exists (the part the hype usually skips)
&lt;/h2&gt;

&lt;p&gt;When I first sketched this idea, my note to myself said "no one has built this." That was wrong, and it is worth correcting in public, because anyone who works in LLM security will know the landscape.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lakera Guard&lt;/strong&gt; — commercial, API-first prompt-injection and jailbreak detection. &lt;a href="https://www.csoonline.com/article/4058653/check-point-acquires-lakera-to-build-a-unified-ai-security-stack.html" rel="noopener noreferrer"&gt;Check Point acquired Lakera in September 2025&lt;/a&gt; and folded it into its platform. Its headline accuracy and sub-50ms latency are the vendor's own figures, not something I measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NeuralTrust&lt;/strong&gt; — ships a commercial product it literally calls a Generative Application Firewall, an inline enforcement layer for LLM apps and agents. So the "GAF" concept is not paper-only; it is a shipping product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Firewall for AI&lt;/strong&gt; — a commercial layer that blocks unsafe prompts and addresses several OWASP LLM risks at the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source&lt;/strong&gt; — several GitHub projects named &lt;code&gt;prompt-shield&lt;/code&gt; (and the one-word &lt;code&gt;promptshield&lt;/code&gt;) implement detector ensembles, reverse-proxy enforcement, and threat scoring. &lt;strong&gt;LLM Guard&lt;/strong&gt; is an active rule-based scanner set, and &lt;strong&gt;NeMo Guardrails&lt;/strong&gt; (NVIDIA) handles conversation-flow control. &lt;strong&gt;Rebuff&lt;/strong&gt;, an earlier multi-layer project, is archived.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "GAF" framing itself comes from a 2026 paper, &lt;a href="https://arxiv.org/abs/2601.15824" rel="noopener noreferrer"&gt;"Introducing the Generative Application Firewall (GAF)"&lt;/a&gt;, which proposes a single enforcement point that unifies prompt filters, output validators, and data masking, maintains session context, and covers agents and their tool calls, by analogy to a WAF for web traffic.&lt;/p&gt;

&lt;p&gt;So the honest gap is narrower and more specific than "nobody has built a firewall." It is: an open, cloud-native &lt;strong&gt;reference architecture on AWS primitives&lt;/strong&gt; that is agent-aware (intercepts tool calls, not just prompts), uses a multi-detector ensemble rather than a single technique, and is explicit about the trilemma trade-off it makes. That is a real and useful gap. It is not a claim of inventing the category.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture I'd build
&lt;/h2&gt;

&lt;p&gt;The design is a pipeline: detect, decide, enforce, learn. Each stage maps cleanly onto AWS primitives, which is the point, it should be deployable without inventing new infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          ┌───────────────────────────────────────────────┐
 request  │  1. DETECTION  (ensemble, run in parallel)      │
 ───────► │   heuristic · ML classifier · LLM-as-judge      │
          │   vector similarity to known attacks            │
          └───────────────────────┬───────────────────────┘
                                   ▼
          ┌───────────────────────────────────────────────┐
          │  2. DECISION  (weighted vote + session context) │
          │   configurable threshold → the trilemma dial    │
          └───────────────────────┬───────────────────────┘
                                   ▼
          ┌───────────────────────────────────────────────┐
          │  3. ENFORCE   allow / block / sanitize / review │
          │   also: scan the OUTPUT, gate TOOL CALLS         │
          └───────────────────────┬───────────────────────┘
                                   ▼
          ┌───────────────────────────────────────────────┐
          │  4. LEARN   log verdicts, feed back FPs,         │
          │   grow the attack-vector store                   │
          └───────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On AWS, I'd map it like this: API Gateway as the entry point; the detector ensemble as Lambda functions, with the ML classifier on SageMaker and the LLM-as-judge on Bedrock; the attack-vector store on OpenSearch; the decision engine as a small Step Functions or Lambda orchestration reading session history from DynamoDB; and CloudWatch plus S3 for the audit trail. None of that is exotic, which is deliberate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why an ensemble, and what each layer is actually good at
&lt;/h3&gt;

&lt;p&gt;No single detector is enough, and the reasons are well documented. The &lt;a href="https://arxiv.org/abs/2608.27990v1" rel="noopener noreferrer"&gt;CAITLYN paper&lt;/a&gt; puts the trade-off bluntly: deterministic signature and regex filters run at sub-millisecond speed with zero token cost but fall to trivial obfuscation and semantic rewrites. So the ensemble is not padding; each layer covers a different failure mode of the others.&lt;/p&gt;

&lt;p&gt;A heuristic layer is the cheap first pass. Illustrative, not a complete ruleset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SUSPICIOUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore (all )?previous instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disregard the (system|above) prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;you are now [a-z ]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# role-override attempts
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reveal (your )?(system )?prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heuristic_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUSPICIOUS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# saturate; this is a signal, not a verdict
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That catches the lazy attacks for free and catches nothing clever, which is exactly why it only contributes a signal. A semantic layer (an ML classifier, or an LLM-as-judge on Bedrock) catches the paraphrased and indirect attacks the regex misses, at real token cost and latency. A vector-similarity layer compares the input against an embedding store of known attack patterns, so a variant of something you have seen scores high even if the wording is new.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decision engine is where the trilemma becomes a dial
&lt;/h3&gt;

&lt;p&gt;This is the heart of the honest version. Instead of a single detector shouting "block," the engine combines the signals into one score and compares it to a threshold you set per tenant or per use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;WEIGHTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heuristic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flag_for_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threshold &lt;em&gt;is&lt;/em&gt; the trilemma choice, made explicit. A bank's internal agent can run it high and tolerate more false positives, because a missed injection is expensive. A low-stakes public chatbot runs it lower to protect utility. The firewall does not pretend to be complete; it hands you the knob and logs where you set it. That framing is the design taking the arXiv result seriously instead of marketing past it.&lt;/p&gt;

&lt;p&gt;The weights above show the full four-detector design. The benchmark in the next section only exercises the two detectors I actually built offline (heuristic and vector), so it runs with two weights rather than four. The semantic detectors in this snippet are the layer I have not built yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  I built the offline core and measured it
&lt;/h2&gt;

&lt;p&gt;Design claims are cheap, so I implemented the two offline layers (the heuristic detector and the vector-similarity detector) plus the decision engine, and ran them against a labeled corpus of 285 prompts: 192 injection and 93 benign. The injection set is deliberately &lt;em&gt;held out&lt;/em&gt;, paraphrases, obfuscations (spaced-out and hyphenated triggers), and indirect-via-document payloads that are &lt;strong&gt;not&lt;/strong&gt; the examples the vector detector was fitted on, so the catch rate reflects generalisation rather than memorisation. The benign set includes hard negatives that mention "instructions," "system prompt," "override," and "ignore" innocently. Everything is pure standard-library Python with a fixed seed, so the run is deterministic and reproducible.&lt;/p&gt;

&lt;p&gt;Here is the actual output, sweeping the decision threshold (weights: heuristic 0.4, vector 0.6):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; thresh  detect%    FP%  precision     F1
--------------------------------------------
   0.15      98%    52%       0.80   0.88
   0.18      92%    25%       0.88   0.90
   0.20      84%    14%       0.93   0.88
   0.25      60%     8%       0.94   0.74
   0.30      39%     1%       0.99   0.55
   0.40      10%     0%       1.00   0.18
   0.60       5%     0%       1.00   0.09
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that top to bottom and the trilemma stops being an abstraction. At a threshold of 0.18 the firewall catches &lt;strong&gt;92%&lt;/strong&gt; of the injections, but it also flags &lt;strong&gt;a quarter of legitimate prompts&lt;/strong&gt;, the hard benign negatives about writing system prompts and documenting team instructions get swept up. Tighten the threshold to 0.30 and false positives drop to &lt;strong&gt;1%&lt;/strong&gt; (precision 0.99), but detection collapses to &lt;strong&gt;39%&lt;/strong&gt;: most attacks walk straight through. There is no row where detection is high and false positives are low at the same time. The best F1 (0.90) sits near the aggressive end, where you have already accepted a real false-positive cost.&lt;/p&gt;

&lt;p&gt;That is not a tuning failure I can polish away with better weights. It is the shape the 2604.06436 result predicts for this class of defense, and watching it fall out of real numbers is more convincing than any single headline accuracy figure. The honest reading is specific: a cheap offline ensemble reliably catches the obvious and the lexically similar, and genuinely struggles to separate a &lt;em&gt;subtle paraphrase&lt;/em&gt; of an attack from a &lt;em&gt;legitimate request that happens to talk about prompts&lt;/em&gt;. That overlap is exactly the region a semantic layer, an ML classifier or an LLM-as-judge on Bedrock, is supposed to resolve, and it is why I treat that layer as the next build step rather than a nice-to-have.&lt;/p&gt;

&lt;p&gt;A note on honesty, because it is the whole point of this post: the corpus is synthetic and template-generated, so its diversity is bounded by the templates; the numbers are operational rather than a formal benchmark against published baselines; and the implementation is not open-sourced. I would not quote these figures without having run the code, and I would not dress them up as production-grade. They are a measured illustration of a trade-off, nothing more and nothing less.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two things that make it agent-aware
&lt;/h3&gt;

&lt;p&gt;Most of the field still treats this as "scan the user's prompt." For modern agentic systems that is half the surface. Two additions matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output scanning.&lt;/strong&gt; The model's response can carry an injected instruction or leak a system prompt. Scanning the output, not just the input, is where several designs (including NeuralTrust's and the GAF paper's) converge, and I'd treat it as mandatory rather than optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool-call gating.&lt;/strong&gt; This is the one I care most about, and where indirect injection lives. When an agent decides to call a tool, that decision can be the payload: a poisoned document retrieved via RAG tells the agent to call &lt;code&gt;send_email&lt;/code&gt; or &lt;code&gt;delete_record&lt;/code&gt;. Gating the tool call, checking the proposed action and arguments against policy before execution, is a different and arguably more important checkpoint than filtering the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;denied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_human&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;args_risky&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2502.01822" rel="noopener noreferrer"&gt;"Firewalls to Secure Dynamic LLM Agentic Networks"&lt;/a&gt; work takes this further, projecting cross-agent messages onto a structured, task-scoped protocol so that persuasive framing and embedded instructions are stripped by construction. That is a stronger idea than scoring, and it is the direction I would want the tool-call layer to grow toward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations, up front
&lt;/h2&gt;

&lt;p&gt;Because a firewall that oversells itself is worse than none:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;cannot be complete.&lt;/strong&gt; The 2604.06436 result makes that a theorem for input-wrapper defenses, not a matter of effort. Anyone claiming 100% is wrong or redefining the problem.&lt;/li&gt;
&lt;li&gt;The semantic layers &lt;strong&gt;cost latency and tokens.&lt;/strong&gt; The sub-50ms figures you see are vendor numbers for narrower, single-layer detectors; a multi-layer ensemble with an LLM-judge step will cost more, and I would not quote a latency number I had not measured.&lt;/li&gt;
&lt;li&gt;It is &lt;strong&gt;defense in depth, not a boundary.&lt;/strong&gt; The durable lesson from the evaluation literature is that security boundaries belong in application code and permissions. A firewall lowers the hit rate; least-privilege tool scoping and human approval on destructive actions are what contain the ones that get through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  On the compliance angle
&lt;/h2&gt;

&lt;p&gt;It is tempting to attach urgency to regulation, so here is the accurate version. The EU AI Act's &lt;strong&gt;high-risk&lt;/strong&gt; obligations were pushed back by the 2026 Digital Omnibus to December 2027 and August 2028, not August 2026 as early drafts implied. What did take effect on August 2, 2026 is the transparency regime (Article 50) and penalty powers for general-purpose models. So the regulatory pressure is real but phased, and the honest pitch is that security controls for LLM apps are becoming table stakes over the next two years, not that a deadline hits tomorrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The path forward
&lt;/h2&gt;

&lt;p&gt;The offline core and the trade-off measurement above are done. From here the order would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the semantic layer&lt;/strong&gt; (an ML classifier, or an LLM-as-judge on Bedrock) specifically to attack the overlap region the offline ensemble could not separate, and re-measure whether it buys real detection without pushing false positives back up. That is the hypothesis the current numbers set up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harden output scanning and tool-call gating&lt;/strong&gt;, and benchmark against indirect-injection scenarios (poisoned RAG content, malicious tool results), since that is the underserved part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish it as an AWS reference architecture&lt;/strong&gt; with the numbers attached, honest about where it sits relative to NeuralTrust, Lakera, and the OSS projects, rather than claiming to replace them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close the loop&lt;/strong&gt;: feed false positives back to retune thresholds and grow the attack-vector store, and treat that feedback data as the real moat over time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is a build I would stand behind, because every claim in it is either cited or measured. The gap it fills is specific: an open, agent-aware, AWS-native reference design that treats the defense trilemma as a dial instead of a thing to lie about.&lt;/p&gt;

&lt;p&gt;If you are working on LLM security, I'd genuinely like to hear which layer you think earns its keep, and whether tool-call gating or output scanning has mattered more in your own systems.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OWASP Top 10 for LLM Applications (2025, 2026) — prompt injection ranked #1 across editions. &lt;a href="https://genai.owasp.org/llm-top-10/" rel="noopener noreferrer"&gt;https://genai.owasp.org/llm-top-10/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;arXiv 2604.06436 — "Why Prompt Injection Defense Wrappers Fail?" (the defense trilemma)&lt;/li&gt;
&lt;li&gt;arXiv 2601.15824 — "Introducing the Generative Application Firewall (GAF)"&lt;/li&gt;
&lt;li&gt;arXiv 2608.27990 — "CAITLYN: Can LLM Agents Autonomously Synthesize Defenses against Emerging Injection Attacks?"&lt;/li&gt;
&lt;li&gt;arXiv 2502.01822 — "Firewalls to Secure Dynamic LLM Agentic Networks"&lt;/li&gt;
&lt;li&gt;Check Point acquires Lakera (Sept 2025) — csoonline.com&lt;/li&gt;
&lt;li&gt;NeuralTrust Generative Application Firewall; Cloudflare Firewall for AI (vendor sources)&lt;/li&gt;
&lt;li&gt;EU AI Act timeline after the 2026 Digital Omnibus (high-risk obligations deferred to Dec 2027 / Aug 2028)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;The benchmark figures come from my own offline reference implementation (pure standard-library Python: regex heuristic + character tri-gram cosine detectors + a weighted decision engine), run against a synthetic, template-generated labeled corpus of 192 injection and 93 benign prompts (285 total), with seed attacks held disjoint from the test set. The implementation is not open-sourced; the numbers are operational, not a formal benchmark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Cut LLM Document-Extraction Cost by 85% Without Losing Accuracy</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Wed, 07 Oct 2026 02:23:09 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/cut-llm-document-extraction-cost-by-85-without-losing-accuracy-34ip</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/cut-llm-document-extraction-cost-by-85-without-losing-accuracy-34ip</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwd23fnsikaq17lqr2kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwd23fnsikaq17lqr2kh.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;br&gt;
Most production LLM extraction pipelines route every field through a frontier model, every time. It works, and for a while nobody questions the bill. Then volume grows, or someone runs the per-document math, and the real question shows up. How much of this spend is buying accuracy, and how much is just habit.&lt;/p&gt;

&lt;p&gt;In practice, most of it is habit. The majority of fields in a typical extraction job do not need a frontier model at all. A large share of the model calls answer questions the document never asked. A surprising fraction of calls re-extract data that has not changed since the last run. None of that is a model problem. It is an architecture problem, and architecture problems are the good kind, because you can fix them without waiting on anyone's roadmap.&lt;/p&gt;

&lt;p&gt;I build cost-optimized inference for document-extraction workloads with 100+ fields. This is the practitioner's version of what actually moves the number. The moves below, applied in sequence, take per-document cost down by &lt;strong&gt;85 to 93 percent&lt;/strong&gt; with negligible accuracy impact. The interesting part is not any single technique. It is why they compound instead of overlap.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set a trusted baseline with the expensive model first, then prove a cheaper model can match it.&lt;/li&gt;
&lt;li&gt;Skip fields a document type cannot contain (adaptive selection).&lt;/li&gt;
&lt;li&gt;Route each field to the cheapest model that can do the job (tiered routing + distillation).&lt;/li&gt;
&lt;li&gt;Stop re-extracting unchanged fields (field-level caching).&lt;/li&gt;
&lt;li&gt;Stop paying for idle (serverless scale-to-zero).&lt;/li&gt;
&lt;li&gt;The savings multiply, not add, because each move attacks a different kind of waste.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  🧭 Set the baseline with the expensive model first
&lt;/h2&gt;

&lt;p&gt;Before optimizing anything, run the workload on a frontier model and treat those results as the baseline. Early on the goal is not low cost. It is output you trust and a confidence signal you believe. Once that baseline exists, it becomes the yardstick for everything cheaper.&lt;/p&gt;

&lt;p&gt;Then, field by field, test whether a smaller and cheaper model can match it. Run the cheap model over the same documents, compare against the frontier baseline, and keep the fields where agreement is high. The fields that match move to the cheaper tier. The fields that do not stay on the frontier model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is how the tier percentages below get decided in practice. You are not guessing which fields a lightweight model can handle. You are measuring it against output you already trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same comparison runs later as the quality-floor monitor at the end of this post. It starts here, as the step that earns each downgrade.&lt;/p&gt;
&lt;h2&gt;
  
  
  ✂️ Start by not doing unnecessary work: adaptive field selection
&lt;/h2&gt;

&lt;p&gt;The cheapest extraction call is the one you never make.&lt;/p&gt;

&lt;p&gt;A contract schema might define 100+ fields, but a given document, say a simple service agreement, only contains 40 to 50 of them. Extracting the rest produces "not found" responses that cost tokens and return nothing. Classify the document type first, then extract only the fields that can actually appear in it.&lt;/p&gt;

&lt;p&gt;This one move eliminates 30 to 40 percent of calls per document. It is the right thing to do first, because every later optimization then operates on a smaller base.&lt;/p&gt;
&lt;h2&gt;
  
  
  🎯 Right-size the model to the field: tiered routing
&lt;/h2&gt;

&lt;p&gt;Not every field needs the same horsepower. In a typical extraction job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~65 to 70 percent of fields are structurally simple:&lt;/strong&gt; dates, amounts, party names, reference numbers, categorical flags. Predictable formats, consistent locations. A lightweight model handles these at a fraction of frontier cost with equivalent accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~20 percent need moderate reasoning:&lt;/strong&gt; interpreting indirect phrasing, resolving ambiguous references, synthesizing across sections. A mid-tier model (for example a Haiku or Nova Lite class model) is plenty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~10 percent genuinely need a frontier model:&lt;/strong&gt; obligation summaries, risk assessment, termination-condition analysis, cross-document context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Route each field to the cheapest tier that can do the job. The routing decision is nearly free. It is a field-type to tier lookup table plus a confidence-based fallback, so a tier that returns low confidence escalates to the next one. No ML is needed for routing, because the mapping is stable for a given document corpus. Blended per-document cost drops 60 to 75 percent, and quality holds because the hard fields still get frontier attention. The split above is not assumed. It comes straight out of the baseline comparison from the previous section.&lt;/p&gt;
&lt;h3&gt;
  
  
  Distillation: earn your way to a cheaper Tier 1
&lt;/h3&gt;

&lt;p&gt;Tiered routing gets better when Tier 1 is a model you distilled for your own documents. Run a frontier model (the teacher) over 200 to 500 representative documents to produce gold-standard extractions, then train a smaller student model to reproduce that behavior on your specific fields. Managed services such as Amazon Bedrock Model Distillation handle the training without ML expertise.&lt;/p&gt;

&lt;p&gt;The student model learns your narrow task, the dates, amounts, party names, and term lengths you see every day, and it runs much cheaper and faster with a small accuracy gap (about 2 percent) on the tasks it was trained for. Fields outside its training distribution fall through to the mid-tier or frontier model.&lt;/p&gt;

&lt;p&gt;The bootstrap problem is obvious. You do not have 200 documents on day one, so distill progressively. Months one and two run routing-only on general-purpose models (40 to 50 percent savings) while storing every validated result as future training data. In month three, once you have accumulated enough examples, distillation runs and Tier 1 becomes your student model (another 30 to 40 percent off). Each quarter you re-distill with more data, and the share of fields handled by the cheapest tier climbs from about 70 percent toward 85 percent. The cost curve bends down the longer the system runs.&lt;/p&gt;
&lt;h2&gt;
  
  
  ♻️ Stop paying twice: field-level caching
&lt;/h2&gt;

&lt;p&gt;Documents get reprocessed when schemas are updated, prompts improve, or a quality review flags systematic errors. Without caching, reprocessing 200 documents costs exactly what processing them the first time did.&lt;/p&gt;

&lt;p&gt;Key each result by the document-section SHA-256, the field name, the prompt version, and the model version. If all four match, return the cached value with zero inference. When you update a prompt for one field type, only that field's cache entries invalidate, and everything else serves from cache. A prompt update typically touches three to five field types, so 85 to 90 percent of fields serve from cache on reprocessing. For a 200-document corpus reprocessed after a four-field prompt change, that is 800 extractions instead of 7,000, an 88 percent reduction on that cycle.&lt;/p&gt;
&lt;h2&gt;
  
  
  🛑 Stop paying for idle: serverless scale-to-zero
&lt;/h2&gt;

&lt;p&gt;A 200-document-per-month workload is idle most of the time. Documents land in object storage, a lightweight function writes metadata that triggers a stream event, and a containerized extraction agent runs only when work exists. During idle periods the system costs nothing beyond storage. For bursty, low-volume workloads this removes the fixed infrastructure floor that usually makes small deployments uneconomical. KEDA is a clean way to get scale-to-zero and event-driven scaling for this on Kubernetes.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧮 Why these compound instead of add
&lt;/h2&gt;

&lt;p&gt;A tempting mistake is to add the savings together. Distillation saves 75 percent, adaptive selection 35 percent, caching 30 percent, so the total must be 140 percent. That is impossible. The techniques do not each operate on the original cost. Each operates on the residual the previous stage left behind. They address different sources of waste, so they multiply:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Operates on&lt;/th&gt;
&lt;th&gt;Residual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Adaptive field selection&lt;/td&gt;
&lt;td&gt;Total field count&lt;/td&gt;
&lt;td&gt;60 to 70 percent of baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Distillation + tiered routing&lt;/td&gt;
&lt;td&gt;Remaining fields&lt;/td&gt;
&lt;td&gt;20 to 25 percent of baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Field-level caching&lt;/td&gt;
&lt;td&gt;Reprocessed documents&lt;/td&gt;
&lt;td&gt;10 to 15 percent on reprocessing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Serverless scale-to-zero&lt;/td&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Zero idle cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compound effect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;End to end&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 to 15 percent of naive baseline&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly &lt;code&gt;(1 - 0.35) x (1 - 0.65) x (1 - 0.25)&lt;/code&gt;, which lands around 0.07 to 0.15 of the original, an &lt;strong&gt;85 to 93 percent reduction&lt;/strong&gt;. Each technique matters most at a different stage. Adaptive selection eliminates unnecessary work, distillation reduces the cost of necessary work, caching eliminates repeated work, and serverless eliminates idle overhead. No one of them gets you there alone.&lt;/p&gt;
&lt;h2&gt;
  
  
  🛡️ Making it safe: the quality-floor monitor
&lt;/h2&gt;

&lt;p&gt;Aggressive cost optimization earns an obvious objection. If 70 percent of fields go to a cheaper model, you need a way to know it is not silently getting worse, or drifting as document patterns change.&lt;/p&gt;

&lt;p&gt;Build the check in. Each week, randomly re-run 5 to 10 percent of Tier-1 fields through the frontier model as a shadow extraction and compare. This is the same baseline comparison from the start of the post, now running continuously. Agreement at or above 98 percent means the floor is holding. Below 95 percent raises an alert. Below 90 percent auto-triggers re-distillation on the last three months of data. Shadow validation adds only about 3 to 5 percent to total cost, and it means the system monitors itself, with no dedicated QA team required.&lt;/p&gt;

&lt;p&gt;The same monitor also spots promotions. If the mid-tier and frontier models consistently agree on a field currently routed to Tier 2, that field is a candidate to fold into the next distillation cycle, so the system keeps shifting work to the cheapest tier on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  💡 The payoff beyond the bill
&lt;/h2&gt;

&lt;p&gt;The reason this is worth doing well is not only the savings on an existing workload. Every percentage point off per-unit cost widens the set of use cases where automation is worth building at all. A workload that only penciled out for one of your five document types at naive pricing might clear the bar for three or four once the architecture is right. Cost optimization is not a late-stage cleanup. Treat it as a first-class design requirement alongside accuracy, latency, and reliability, and more of what you want to automate becomes viable.&lt;/p&gt;

&lt;p&gt;Five moves, five kinds of waste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expensive-model baseline, then prove the cheap model&lt;/strong&gt; turns every downgrade into a measured decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive field selection&lt;/strong&gt; removes unnecessary work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered routing + distillation&lt;/strong&gt; reduces the cost of necessary work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field-level caching&lt;/strong&gt; removes repeated work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless scale-to-zero&lt;/strong&gt; removes idle overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apply them in sequence, guard them with a self-checking quality floor, and you get frontier-grade extraction at a small fraction of the naive cost, without betting accuracy on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two patterns I open-sourced
&lt;/h2&gt;

&lt;p&gt;Two of the ideas above are available as small, dependency-light libraries under AWS Samples (MIT-0), if you want a reference to build from:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/aws-samples" rel="noopener noreferrer"&gt;
        aws-samples
      &lt;/a&gt; / &lt;a href="https://github.com/aws-samples/sample-textract-field-memory" rel="noopener noreferrer"&gt;
        sample-textract-field-memory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Spatial field location memory for document processing pipelines. Learns field positions, validates extractions, identifies document types by layout, detects drift, and monitors template health. Zero dependencies, pure Python.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;textract-field-memory&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Disclaimer&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This is sample code, for non-production usage. You should work with your security
and legal teams to meet your organizational security, regulatory, and compliance
requirements before deployment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When processing documents containing PII (e.g. SSNs), PHI, or payment data, ensure your implementation meets applicable compliance requirements (HIPAA, PCI-DSS, GDPR, etc.). This library stores only field names and bounding-box coordinates — never field values or document content — but you remain responsible for securing the underlying documents.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Every system that processes recurring structured data starts from scratch each time. It has no memory of what it saw yesterday.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;                    WITHOUT spatial memory
┌──────────────┐
│  Document 1  │──► OCR/Extract ──► "Employee Name" at (0.05, 0.10) ──► ✓ stored nowhere
│  Document 2  │──► OCR/Extract ──► "Employee Name" at (0.05, 0.10) ──► ✓ re-discovered
│  Document 3  │──► OCR/Extract ──► "Employee Name" at (0.05, 0.10) ──► ✓ re-discovered again
│&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/aws-samples/sample-textract-field-memory" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/aws-samples" rel="noopener noreferrer"&gt;
        aws-samples
      &lt;/a&gt; / &lt;a href="https://github.com/aws-samples/sample-prompt-correction-memory" rel="noopener noreferrer"&gt;
        sample-prompt-correction-memory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Self-improving LLM document extraction on AWS. Each human QA correction improves future extractions via few-shot self-healing and deterministic rule graduation — no retraining, no redeployment. Costs decrease over time.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;sample-prompt-correction-memory&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A sample project showing automated prompt self-correction for LLM document extraction on AWS. Each human correction improves future extractions — no retraining, no redeployment.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5266ef63d0fec1ad4cee104c7aee0ad0f084d6c1388d39adfe556c420a0a28d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e392532422d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/5266ef63d0fec1ad4cee104c7aee0ad0f084d6c1388d39adfe556c420a0a28d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e392532422d626c7565" alt="Python 3.9+"&gt;&lt;/a&gt; &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/e63a95ba96d478a9121e4719c337752da4f4cbd273c68ca5bbb73a1af276dfb5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d2d302d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/e63a95ba96d478a9121e4719c337752da4f4cbd273c68ca5bbb73a1af276dfb5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d2d302d626c7565" alt="License"&gt;&lt;/a&gt; &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/37fe70d0c1cfa6589fb7f88ed2367f10070c3f228370fc72224e11210ef61571/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d73616d706c652d6f72616e6765"&gt;&lt;img src="https://camo.githubusercontent.com/37fe70d0c1cfa6589fb7f88ed2367f10070c3f228370fc72224e11210ef61571/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d73616d706c652d6f72616e6765" alt="Status"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This is sample code, for non-production usage. You should work with your security and legal teams to meet your organizational security, regulatory and compliance requirements before deployment. Do not use this sample with personal data, health data or other regulated data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/aws-samples/sample-prompt-correction-memory/docs/flywheel.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Faws-samples%2Fsample-prompt-correction-memory%2FHEAD%2Fdocs%2Fflywheel.svg" alt="Correction memory flywheel: rules, cloud LLM, and self-healing tiers feeding a growing correction log" width="720"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Every LLM-based extraction pipeline makes the same mistakes repeatedly. Your QA team corrects an error today, but the model makes the identical error on tomorrow's document. The correction evaporates.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;                    WITHOUT correction memory
┌──────────────┐
│  Document 1  │──► LLM Extract ──► "quarterly" for payment_terms  ──► QA corrects to "30"
│  Document 2  │──► LLM Extract ──► "quarterly" for payment_terms  ──► QA corrects AGAIN
│  Document 3  │──► LLM Extract ──► "quarterly" for payment_terms  ──► QA corrects AGAIN
│     ...      │
└──────────────┘
    Same&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/aws-samples/sample-prompt-correction-memory" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;






&lt;p&gt;&lt;em&gt;If you are optimizing an extraction pipeline, I would be curious where your biggest cost sink turned out to be: unnecessary calls, over-powered models, reprocessing, or idle. Drop a comment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Your LLM Keeps Making the Same Extraction Mistake. Here's How to Make It Learn.</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Sat, 03 Oct 2026 01:32:58 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/your-llm-keeps-making-the-same-extraction-mistake-heres-how-to-make-it-learn-5bjb</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/your-llm-keeps-making-the-same-extraction-mistake-heres-how-to-make-it-learn-5bjb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhubyrft3pyfvanjeee2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhubyrft3pyfvanjeee2.png" alt=" " width="800" height="419"&gt;&lt;/a&gt;&amp;gt; &lt;strong&gt;TL;DR&lt;/strong&gt; — I open-sourced an AWS sample that makes LLM document extraction &lt;em&gt;learn from human corrections&lt;/em&gt;. Each QA fix is captured once and reused, so the pipeline gets &lt;strong&gt;more accurate and cheaper over time&lt;/strong&gt; — with no retraining and no redeployment. Repo: &lt;a href="https://github.com/aws-samples/sample-prompt-correction-memory" rel="noopener noreferrer"&gt;github.com/aws-samples/sample-prompt-correction-memory&lt;/a&gt;. You can run the whole self-healing loop locally in 30 seconds, no AWS account needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: corrections that evaporate
&lt;/h2&gt;

&lt;p&gt;If you've built an LLM extraction pipeline, you know this pain.&lt;/p&gt;

&lt;p&gt;Your QA analyst opens today's invoice. The model extracted &lt;code&gt;payment_terms&lt;/code&gt; as &lt;strong&gt;"quarterly."&lt;/strong&gt; That's wrong — "quarterly" is the &lt;em&gt;billing frequency&lt;/em&gt;; the actual payment term is &lt;strong&gt;Net 30&lt;/strong&gt;. The analyst corrects it. Done.&lt;/p&gt;

&lt;p&gt;Tomorrow, an identical invoice from the same vendor arrives. The model extracts... &lt;strong&gt;"quarterly"&lt;/strong&gt; again. Same mistake. The analyst corrects it again.&lt;/p&gt;

&lt;p&gt;The correction &lt;em&gt;evaporated.&lt;/em&gt; It fixed one document and taught the system nothing.&lt;/p&gt;

&lt;p&gt;Multiply that across thousands of documents and dozens of recurring error patterns, and you get the two options most teams settle for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pay analysts to re-fix the same patterns forever&lt;/strong&gt; (QA cost never goes down), or&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tune a model&lt;/strong&gt; (expensive, slow, needs ML ops, and you redo it every time the patterns shift).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a third way that needs neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: treat corrections as durable memory
&lt;/h2&gt;

&lt;p&gt;What if every human correction became a permanent, reusable asset?&lt;/p&gt;

&lt;p&gt;That's the pattern behind &lt;a href="https://github.com/aws-samples/sample-prompt-correction-memory" rel="noopener noreferrer"&gt;&lt;code&gt;sample-prompt-correction-memory&lt;/code&gt;&lt;/a&gt;. Instead of throwing corrections away, it stores them and feeds them back into future extractions — so the same mistake is never made twice.&lt;/p&gt;

&lt;p&gt;It works as &lt;strong&gt;three tiers&lt;/strong&gt;, and the magic is that traffic gradually shifts from the expensive tier to the free one:&lt;/p&gt;

&lt;h3&gt;
  
  
  🟢 Tier 1 — Graduated rules ($0, no LLM call)
&lt;/h3&gt;

&lt;p&gt;When the same correction pattern recurs enough times, the system synthesizes a &lt;strong&gt;deterministic rule&lt;/strong&gt; (a regex, a lookup, a normalization) that handles that field with zero LLM cost and sub-millisecond latency. "Net 30" → &lt;code&gt;30&lt;/code&gt; becomes a rule; it never needs a model again.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔵 Tier 2 — Cloud LLM extraction (confidence-gated)
&lt;/h3&gt;

&lt;p&gt;Fields with no matching rule go to &lt;strong&gt;Amazon Bedrock (Claude Haiku 4.5)&lt;/strong&gt;. Every field comes back with a &lt;strong&gt;confidence score&lt;/strong&gt;. If confidence clears the threshold, accept it and move on.&lt;/p&gt;

&lt;h3&gt;
  
  
  🟠 Tier 3 — Self-healing (only when needed)
&lt;/h3&gt;

&lt;p&gt;If confidence is &lt;em&gt;below&lt;/em&gt; threshold, the system retrieves the most relevant past corrections, formats them as &lt;strong&gt;few-shot examples&lt;/strong&gt;, and re-extracts with a stronger model (&lt;strong&gt;Claude Sonnet 4.5&lt;/strong&gt;). The model learns from the specific mistakes it made before — in-context, no training.&lt;/p&gt;

&lt;p&gt;Every QA correction flows into a &lt;strong&gt;correction log&lt;/strong&gt; (DynamoDB) that powers both the self-healing retrieval &lt;em&gt;and&lt;/em&gt; the rule graduation. So the more the system is used, the smarter and cheaper it gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters (the practical payoff)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ML ops.&lt;/strong&gt; No retraining, no GPUs, no deployment cycles. A correction takes effect on the &lt;em&gt;very next&lt;/em&gt; document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost drops as quality rises.&lt;/strong&gt; Normally you trade one for the other. Here, recurring fixes graduate to $0 rules — so accuracy climbs while per-document cost falls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA effort compounds instead of repeating.&lt;/strong&gt; Fix it once → fixed forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a QA action, not an ML project.&lt;/strong&gt; Improving the pipeline no longer requires the ML team. An analyst uploads a correction; the system improves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clearest fit is &lt;strong&gt;high-repetition, back-office document processing&lt;/strong&gt; — accounts payable, contracts, compliance filings, supply-chain docs — where the same field errors recur and analyst time is the real expense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Fully serverless, deployed with AWS SAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document ──► S3 ──► EventBridge ──► SQS ──► Lambda (extract)
                                              │
                          ┌───────────────────┼───────────────────┐
                          ▼                    ▼                   ▼
                   Tier 1: Rules       Tier 2: Bedrock      Tier 3: Self-heal
                   (deterministic)     (Claude Haiku 4.5)   (Claude Sonnet 4.5)
                          ▲                                        │
                          │                                        ▼
                          └──────── Correction Log (DynamoDB) ◄────┘
                                    (grows from QA feedback)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;QA corrections are uploaded as JSON to an S3 &lt;code&gt;corrections/&lt;/code&gt; prefix; a second Lambda validates and ingests them into the correction log. Everything is encrypted with a customer-managed KMS key, and IAM is scoped to the specific Bedrock model ARNs the sample uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it in 30 seconds (no AWS account)
&lt;/h2&gt;

&lt;p&gt;The quickstart runs the entire self-healing loop with a mocked Bedrock client, so you can watch the mechanism without deploying anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/aws-samples/sample-prompt-correction-memory
&lt;span class="nb"&gt;cd &lt;/span&gt;sample-prompt-correction-memory
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
python examples/quickstart.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Initial extraction (no correction memory)
  Field:      effective_date
  Value:      March 2024        Confidence: 0.55   ❌ NO

Step 2: Self-healing triggers (0.55 &amp;lt; threshold 0.70)
  Retrieving corrections for 'effective_date'...
  Found: "March 2024" → "2024-03-01"

Step 3: Re-extraction with correction memory
  Value:       2024-03-01       Confidence: 0.95   ✓ YES
  Self-Healed: True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Low-confidence extraction → retrieve past correction → re-extract → correct answer. No retraining. No redeployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to AWS
&lt;/h2&gt;

&lt;p&gt;With the AWS CLI, SAM CLI, and Bedrock model access for Claude Haiku 4.5 + Sonnet 4.5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make deploy    &lt;span class="c"&gt;# S3, DynamoDB, Lambda, EventBridge, SQS, KMS&lt;/span&gt;
make seed      &lt;span class="c"&gt;# load sample corrections into the correction log&lt;/span&gt;
make trigger   &lt;span class="c"&gt;# upload a sample document → triggers real extraction&lt;/span&gt;
make verify    &lt;span class="c"&gt;# read back results (fields, confidence, self_healed)&lt;/span&gt;
make destroy   &lt;span class="c"&gt;# empty buckets + delete the stack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;make verify&lt;/code&gt; prints each extracted field with its confidence and whether self-healing kicked in — your proof it works end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A companion: spatial memory
&lt;/h2&gt;

&lt;p&gt;This sample provides &lt;strong&gt;semantic&lt;/strong&gt; correction memory (what was extracted wrong, and why). Its companion, &lt;a href="https://github.com/aws-samples/sample-textract-field-memory" rel="noopener noreferrer"&gt;&lt;code&gt;sample-textract-field-memory&lt;/code&gt;&lt;/a&gt;, provides &lt;strong&gt;spatial&lt;/strong&gt; memory (where fields appear on a document layout). Together they form a dual memory for document pipelines: use the cheap spatial lookup when you're confident where a field is, and fall back to self-healing extraction when you're not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;Because it's more useful when it's used well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's &lt;strong&gt;sample/reference code&lt;/strong&gt;, not a production library. Harden IAM, add monitoring, and put a human in the loop for high-stakes fields before real use.&lt;/li&gt;
&lt;li&gt;It helps most where the &lt;strong&gt;same field errors recur&lt;/strong&gt;. For wildly heterogeneous, one-off documents, there's less pattern to learn.&lt;/li&gt;
&lt;li&gt;Graduated rules currently take effect &lt;strong&gt;without a human approval step&lt;/strong&gt; — in production you'd add a review gate before a synthesized rule starts answering on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't use it with regulated data&lt;/strong&gt; (PII/PHI/PCI) as-is; that path needs a proper compliance review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it and tell me
&lt;/h2&gt;

&lt;p&gt;The repo is open source under &lt;code&gt;aws-samples&lt;/code&gt;, MIT-0 licensed, with a benchmark suite, an interactive dashboard, and an offline test harness.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/aws-samples/sample-prompt-correction-memory" rel="noopener noreferrer"&gt;github.com/aws-samples/sample-prompt-correction-memory&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What repetitive extraction error is your team still fixing by hand? I'd love to hear which patterns you'd want a system like this to learn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is an open-source AWS sample intended for demonstration and non-production use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>LLM Serving on Kubernetes in 2026: What's Solved and What's Still Open</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Sat, 03 Oct 2026 01:11:35 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/llm-serving-on-kubernetes-in-2026-whats-solved-and-whats-still-open-52ad</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/llm-serving-on-kubernetes-in-2026-whats-solved-and-whats-still-open-52ad</guid>
      <description>&lt;p&gt;&lt;strong&gt;A field survey of the Kubernetes + LLM inference stack in 2026 — the problems that now have serious players, and the edges that are still genuinely underserved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run large language models on Kubernetes, you've probably noticed the ground shifting fast. A year ago most of the hard problems were open. In 2026 a lot of them have serious, well-funded players — which changes the calculus if you're deciding where to build. Competing with a GA project is very different from pioneering an empty space.&lt;/p&gt;

&lt;p&gt;This is a map of the territory: what's crowded, and where I think the real gaps still are. I'm deliberately not pitching any of these as "my novel architecture" — several have academic papers or shipping projects attached. The point is to show where the open problems are so you can decide where your effort pays off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's already crowded
&lt;/h2&gt;

&lt;p&gt;These are the areas where the obvious pain points now have mature answers. Building here means competing, not pioneering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KV-cache-aware routing / model-aware gateways.&lt;/strong&gt; Largely solved. The Gateway API Inference Extension (a kubernetes-sigs project) defines a standard &lt;code&gt;InferencePool&lt;/code&gt; routing contract, and llm-d, NVIDIA Dynamo, and GKE Inference Gateway (which went GA in 2026) all do prefix-cache affinity and cache-aware routing on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefill/decode disaggregation.&lt;/strong&gt; NVIDIA Dynamo and llm-d cover this well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fractional GPU sharing (MIG / MPS / time-slicing).&lt;/strong&gt; Run:ai, cast.ai, the NVIDIA GPU Operator, and vCluster all address it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU scheduling primitives (DRA, gang scheduling).&lt;/strong&gt; KAI Scheduler, Grove, Kueue, and Volcano are all in play, with Dynamic Resource Allocation graduating in core Kubernetes. Crowded, though still messy at the edges.&lt;/p&gt;

&lt;p&gt;If your idea lives in one of those buckets, go in with eyes open: you're entering a race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the real gaps still are
&lt;/h2&gt;

&lt;p&gt;Three edges still look genuinely underserved, and each is shaped like a Kubernetes-native problem — a CRD plus a controller plus some node-level machinery.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Model-weight distribution as a first-class Kubernetes primitive
&lt;/h3&gt;

&lt;p&gt;This is the one I find most interesting. Model weights are often 100+ GB, and today they're still delivered largely by ad-hoc downloads from object storage — without the pull-caching, digest addressing, and content verification that Kubernetes already gives you for container images. That gap is a major driver of the two-to-ten-minute cold starts that make scale-to-zero economically painful for inference, since every new replica pays that cost before serving its first request.&lt;/p&gt;

&lt;p&gt;The literature backs up the problem. An empirical study (arXiv 2607.16596, "Cold-Start Model Delivery in Kubernetes Inference Serving") measures the delivery paths — modelcar sidecars, native image volumes, object-storage download — on artifacts sized to 1B-, 7B-, and 70B-class weights, and a companion paper (arXiv 2609.20874) frames the same two-to-ten-minute startup as the reason reactive autoscaling is structurally late. So the pain is validated; what's missing is a first-class primitive for it. Note that these papers study and characterize the problem — they don't ship the CRD-shaped solution below, and neither do I claim to have invented it.&lt;/p&gt;

&lt;p&gt;The interesting shape of a solution: a &lt;code&gt;ModelArtifact&lt;/code&gt; / &lt;code&gt;ModelCache&lt;/code&gt; CRD, a controller, and a node-level DaemonSet that treats weights like OCI-addressable, content-verified, node-locally-cached blobs — with topology-aware peer-to-peer prewarming (pull from a neighbor node's cache over the fast fabric instead of re-pulling from object storage), plus scheduler hints so pods land where the weights already live. NVIDIA Dynamo's ModelExpress already does some of this (one worker publishes weights, others pull over NIXL/RDMA), so even here you'd be refining a direction others have started, not working in a vacuum.&lt;/p&gt;

&lt;p&gt;Why it's a good build target: it's infrastructure-shaped, CRD-heavy, and not yet a solved commodity. You can prototype the caching, P2P, and scheduler-hint logic and measure cold-start reduction on modest hardware — or even with simulated weights — without a large GPU budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SLO-driven inference autoscaling that understands request heterogeneity
&lt;/h3&gt;

&lt;p&gt;A caution up front: "SLO-aware inference autoscaling" as a headline is already a shipped commodity — AWS, KEDA, and academia all have versions of it. So this is only interesting if you go narrower than the headline.&lt;/p&gt;

&lt;p&gt;Current autoscaling scales on queue depth or KV-cache utilization. But the Cascade paper (arXiv 2608.06557, "Exploiting SLO-Aware latency budget for fair and high goodput LLM inference serving") shows that requests under the same SLO can differ in urgency by a wide margin — a short chat versus a long agentic reasoning trace — and that you can direct queueing and data-movement overhead toward the requests that can absorb it. Related systems like Scorpio (arXiv 2505.23022) exploit the same SLO heterogeneity for admission and scheduling, so this is an active research line, not an empty one. The defensible wedge is per-request-&lt;em&gt;class&lt;/em&gt; latency budgeting plus admission control: an &lt;code&gt;InferenceSLO&lt;/code&gt; CRD where you declare per-class latency budgets, and a controller that combines a batch-latency predictor with HPA/KEDA to make admission and scaling decisions on predicted goodput rather than raw queue length.&lt;/p&gt;

&lt;p&gt;This one is more research-flavored and genuinely publishable — but only if you build and benchmark it against the existing SLO-aware baselines. Without numbers, it reads as a restatement of what's already shipping.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-LoRA adapter lifecycle as a CRD
&lt;/h3&gt;

&lt;p&gt;Serving hundreds of tenant-specific LoRA adapters over one base model is now a real production pattern (arXiv 2511.22880), and vLLM and Dynamo support runtime adapter loading. What's still hand-rolled is lifecycle management: which adapters are hot, cache eviction by usage, routing requests to workers that already hold the adapter, and VRAM budgeting. Dynamo has some declarative Kubernetes support here, but it's early.&lt;/p&gt;

&lt;p&gt;The shape: a &lt;code&gt;LoRAAdapter&lt;/code&gt; + &lt;code&gt;AdapterPool&lt;/code&gt; CRD that manages adapter placement, usage-based eviction, and adapter-affinity routing hints — essentially "KV-cache-aware routing," but for adapters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'd choose
&lt;/h2&gt;

&lt;p&gt;If you want a portfolio or OSS operator project: &lt;strong&gt;idea #1.&lt;/strong&gt; It's clearly the most infrastructure-shaped, there's a fresh paper validating the problem, and it's demonstrable without a big GPU budget.&lt;/p&gt;

&lt;p&gt;If you'd rather write something paper-worthy: &lt;strong&gt;idea #2&lt;/strong&gt;, with the per-request-class framing and real benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idea #3&lt;/strong&gt; is a reasonable middle ground.&lt;/p&gt;

&lt;p&gt;A closing note on honesty, because it matters in this space: none of these are empty green fields. Each sits next to active prior art, and dev.to readers who work on inference will know it. The value here isn't a claim of novelty — it's a clear-eyed map of where the race is already crowded and where there's still room to build something useful.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gateway API Inference Extension — kubernetes-sigs/gateway-api-inference-extension (github.com))&lt;/li&gt;
&lt;li&gt;NVIDIA Dynamo; ModelExpress weight distribution (docs.nvidia.com)&lt;/li&gt;
&lt;li&gt;Run:ai, cast.ai, NVIDIA GPU Operator, vCluster (GPU sharing)&lt;/li&gt;
&lt;li&gt;KAI Scheduler, Grove, Kueue, Volcano; Dynamic Resource Allocation in Kubernetes&lt;/li&gt;
&lt;li&gt;arXiv 2607.16596 — "Cold-Start Model Delivery in Kubernetes Inference Serving: An Empirical Study of OCI-Based Distribution and Its Integrity" (G. Kliukovkin)&lt;/li&gt;
&lt;li&gt;arXiv 2609.20874 — "Decomposing Predictive Kubernetes Autoscaling for LLM Serving Under Long Startup Delays"&lt;/li&gt;
&lt;li&gt;arXiv 2608.06557 — "Cascade: Exploiting SLO-Aware latency budget for fair and high goodput LLM inference serving"&lt;/li&gt;
&lt;li&gt;arXiv 2505.23022 — "Scorpio: Serving Right Requests at the Right Time for Heterogeneous SLOs in LLM Inference"&lt;/li&gt;
&lt;li&gt;arXiv 2511.22880 — "Serving Heterogeneous LoRA Adapters in Distributed LLM Inference Systems"&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>llm</category>
      <category>platformengineering</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Give Your Document OCR Pipeline a Memory</title>
      <dc:creator>Avneet bansal</dc:creator>
      <pubDate>Tue, 08 Sep 2026 22:30:40 +0000</pubDate>
      <link>https://dev.to/avneet_bansal_a65b3f31fc4/give-your-document-ocr-pipeline-a-memory-59l1</link>
      <guid>https://dev.to/avneet_bansal_a65b3f31fc4/give-your-document-ocr-pipeline-a-memory-59l1</guid>
      <description>&lt;p&gt;After OCR, most pipelines throw the layout away and re-discover the same form from scratch every time. &lt;a href="https://github.com/aws-samples/sample-textract-field-memory" rel="noopener noreferrer"&gt;&lt;code&gt;textract-field-memory&lt;/code&gt;&lt;/a&gt; is a small, zero-dependency Python layer that remembers where fields sit, checks they landed in the right place, tells you which layout a document is, and warns you when a form quietly changes. It doesn't do OCR. It makes the OCR you already run a lot more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this started
&lt;/h2&gt;

&lt;p&gt;I kept running into the same annoying gap on document pipelines. You send a form through Textract (or Tesseract, or whatever), you get back field names and bounding boxes, you pull the values, and then you throw all the positional information away. The next copy of that exact same form shows up an hour later and the pipeline treats it like it's never seen anything like it before.&lt;/p&gt;

&lt;p&gt;That bugged me for a few reasons.&lt;/p&gt;

&lt;p&gt;Nothing checks that a field came from the right spot on the page. If "Total" gets read out of the shipping box instead of the amount box, the value still looks like a number, so it sails downstream and nobody blinks.&lt;/p&gt;

&lt;p&gt;Nothing notices when a form changes. A vendor nudges their invoice template, fields move a centimeter, and you find out three weeks later when a downstream job starts choking on garbage.&lt;/p&gt;

&lt;p&gt;And there's no cheap way to ask "have I seen this layout before?" So every document, familiar or not, takes the same expensive road.&lt;/p&gt;

&lt;p&gt;So I wrote a little library that sits &lt;em&gt;after&lt;/em&gt; OCR and keeps the memory the pipeline was throwing out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is, and what it definitely isn't
&lt;/h2&gt;

&lt;p&gt;It's a spatial memory layer. You hand it field names plus normalized bounding boxes (x, y, width, height between 0 and 1). It learns roughly where each field tends to appear on a given template, and from then on it can validate positions, identify which template a document belongs to, and watch for drift.&lt;/p&gt;

&lt;p&gt;It is not an OCR engine and it never touches pixels. Something upstream has to extract the fields first. This library just remembers where they ended up and reasons about that.&lt;/p&gt;

&lt;p&gt;Under the hood there's nothing exotic going on, which I think is a feature. Spatial scoring is intersection-over-union plus the distance between box centers. Name matching is a normalized Levenshtein distance. Template identification is Jaccard overlap of the field-name sets combined with the spatial score. No model, no training step, no network calls. It's pure standard-library Python with zero dependencies, and templates are just JSON files on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick look at the API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;field_memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemplateMemory&lt;/span&gt;

&lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TemplateMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my_templates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Learn — feed documents after OCR extraction
&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;employment-form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Locate — spatial field lookup with a confidence score
&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Employee Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → FieldMatch(combined_score=0.95, spatial_score=0.93, within_expected_region=True)
&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Identify — which known layout is this?
&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → TemplateMatch(template_id="employment-form", similarity_score=0.87)
&lt;/span&gt;
&lt;span class="c1"&gt;# 4. Detect drift — has the layout shifted?
&lt;/span&gt;&lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect_drift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;employment-form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → DriftReport(is_drifting=False, overall_drift_score=0.02)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clustering comes for free. If you call &lt;code&gt;record()&lt;/code&gt; and leave out the &lt;code&gt;template_id&lt;/code&gt;, it either merges the document into a template it recognizes or spins up a new one. Run that for a while and you end up with a map of every distinct layout flowing through your pipeline without ever configuring one by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there's no LLM inside
&lt;/h2&gt;

&lt;p&gt;For a layout you've already seen, answering "where's this field?" is basically a dictionary lookup. Sub-millisecond, and free. The same question routed through an LLM is half a second to two seconds and costs you per field. Paying that toll on forms you've processed a thousand times is silly.&lt;/p&gt;

&lt;p&gt;So the design leans into being complementary instead of competitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;smart_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;within_expected_region&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;combined_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;key_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spatial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# free, instant
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;call_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# pay only when needed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it as the cheap first pass that decides whether you even need the expensive one. The 0.85 threshold is just a starting point. Crank it up if a wrong answer is costly and you'd rather fall back to the LLM more often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually helps
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;What it remembers&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invoice processing&lt;/td&gt;
&lt;td&gt;Where fields sit per vendor layout&lt;/td&gt;
&lt;td&gt;Vendor template changes, wrong-position reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insurance claims&lt;/td&gt;
&lt;td&gt;Field positions across many form types&lt;/td&gt;
&lt;td&gt;Unknown forms that need routing for review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Healthcare intake&lt;/td&gt;
&lt;td&gt;Where fields land on clinic forms&lt;/td&gt;
&lt;td&gt;Layout revisions before they break downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manufacturing QA&lt;/td&gt;
&lt;td&gt;Component positions on boards&lt;/td&gt;
&lt;td&gt;Parts that shifted from baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only real requirement is that your data has named things with bounding boxes. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some numbers, and the caveats that go with them
&lt;/h2&gt;

&lt;p&gt;I put it through a stress test using real Textract (FORMS) on generated PDFs plus scanned, deliberately degraded versions of the same documents.&lt;/p&gt;

&lt;p&gt;Layout identification came out at 100% across five vendors whose invoices look nothing alike, trained on just three documents each. On dense tax returns with 33 fields crammed into a tight three-column grid, roughly 0.03 units apart, it located every field. Drift detection behaved the way I hoped: as I pushed forms further off their baseline, the drift scores climbed smoothly from 0.012 up to 0.047 and tripped the flag once they crossed the threshold. On injected anomalies (shifted, scattered, reversed, wrong-form) it caught four of five. And because it's all local computation, lookups stay under a millisecond.&lt;/p&gt;

&lt;p&gt;Now the honest part.&lt;/p&gt;

&lt;p&gt;The benchmark runs on synthetic PDFs generated with ReportLab and their scanned variants, not a big real-world labeled corpus. Sample sizes are small, around 20 forms per scenario.&lt;/p&gt;

&lt;p&gt;There's no proper precision/recall/F1 yet because I don't have a ground-truth label set built. The numbers above are operational rather than a formal comparison against baselines, and I'd treat them that way.&lt;/p&gt;

&lt;p&gt;The one anomaly it missed was the "same layout, just fewer fields" case. The fields that remain are still sitting in the right places, so spatially nothing looks wrong. You could argue that's correct behavior, but it's worth knowing going in.&lt;/p&gt;

&lt;p&gt;Handwritten forms are a dead end, because Textract FORMS gives back nothing to learn from. That's an upstream limitation, not something this library can paper over.&lt;/p&gt;

&lt;p&gt;I'd rather you hit these on the page than in production.&lt;/p&gt;

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

&lt;p&gt;Clone it and install from source. This works right now, nothing to sign up for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/aws-samples/sample-textract-field-memory
&lt;span class="nb"&gt;cd &lt;/span&gt;sample-textract-field-memory
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
python examples/demo.py   &lt;span class="c"&gt;# runs on synthetic data, no AWS needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;demo.py&lt;/code&gt; walks through template learning, spatial matching, anomaly detection, and drift analysis on synthetic data, so you can watch the whole thing work without credentials or config.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/aws-samples/sample-textract-field-memory" rel="noopener noreferrer"&gt;github.com/aws-samples/sample-textract-field-memory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It lives under &lt;code&gt;aws-samples&lt;/code&gt; but there's no AWS dependency in it. Pure standard-library Python, works completely offline with any OCR source that gives you field names and boxes. MIT-0 licensed.&lt;/p&gt;

&lt;p&gt;If you run document pipelines, the thing I'd most like feedback on is turning these stress tests into a real labeled benchmark. And if the "remember it, stop re-discovering it" idea resonates, you've got the whole pitch.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ocr</category>
      <category>textract</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
