<?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: Bry</title>
    <description>The latest articles on DEV Community by Bry (@brywritescode).</description>
    <link>https://dev.to/brywritescode</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%2F4009547%2Fe6e5c498-b824-493d-b06e-fef6445e4ef7.jpeg</url>
      <title>DEV Community: Bry</title>
      <link>https://dev.to/brywritescode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brywritescode"/>
    <language>en</language>
    <item>
      <title>Building a RAG Pipeline from Scratch: Embeddings, Retrieval, and Claude</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/building-a-rag-pipeline-from-scratch-embeddings-retrieval-and-claude-1h96</link>
      <guid>https://dev.to/brywritescode/building-a-rag-pipeline-from-scratch-embeddings-retrieval-and-claude-1h96</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt; connects an LLM to your private data at query time — no fine-tuning, no retraining, no data leakage into model weights.&lt;/li&gt;
&lt;li&gt;The pipeline has five stages: &lt;strong&gt;Ingest → Chunk → Embed → Store → Query&lt;/strong&gt; (retrieve, augment, generate). Getting chunking and retrieval right matters more than which LLM you pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChromaDB&lt;/strong&gt; runs in-process for local dev with zero infrastructure — &lt;code&gt;collection.add()&lt;/code&gt; to insert, &lt;code&gt;collection.query()&lt;/code&gt; to retrieve. Production options include Pinecone and pgvector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common failure modes&lt;/strong&gt; — chunks too large, no deduplication, no query expansion, skipping evaluation — are all avoidable. This article shows how.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Large language models hallucinate when they don't know the answer. The standard fix — fine-tuning on your private data — costs tens of thousands of dollars, takes weeks, and produces a static model that goes stale as soon as your data changes. RAG solves a different problem: instead of baking knowledge into weights, it retrieves the relevant facts at query time and gives them to the model as context. The model answers from evidence, not memory. I've built RAG pipelines for internal knowledge bases and customer-facing chat tools — the architecture is the same whether you're indexing 500 internal docs or 500,000 support tickets; the parameters are what change.&lt;/p&gt;

&lt;p&gt;This article builds a complete RAG pipeline in Python using ChromaDB for vector storage, &lt;code&gt;sentence-transformers&lt;/code&gt; for local embeddings, and Claude for generation. You will understand each stage, why the design choices matter, and what breaks in production if you skip them. The full working implementation is in &lt;code&gt;src/pipeline.py&lt;/code&gt; and &lt;code&gt;src/main.py&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What RAG Is — and What It Replaces
&lt;/h2&gt;

&lt;p&gt;Before writing code, align on why RAG exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-tuning&lt;/strong&gt; trains the model on your data. It is expensive (typically $5,000–$50,000+ for a production run), takes days to weeks, and produces a checkpoint that bakes in your data as of the training date. If your documentation changes next week, the model doesn't know. Fine-tuning is correct when you need the model to learn a style, a domain vocabulary, or a task format — not when you need it to answer questions from a document set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt-only (stuffing)&lt;/strong&gt; puts your documents directly into the context window. It works for small document sets (tens of pages) but breaks on large corpora: context windows have limits, filling them with irrelevant text degrades answer quality, and at scale the cost per query becomes prohibitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG&lt;/strong&gt; indexes your documents, retrieves only the chunks relevant to each query, and gives the model a focused context. It handles large corpora, stays current as documents are updated, and costs a fraction of fine-tuning. The tradeoff is a retrieval layer you have to build and maintain.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Data scale&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fine-tuning&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;Low (no retrieval)&lt;/td&gt;
&lt;td&gt;High (training + inference)&lt;/td&gt;
&lt;td&gt;Static — retrain to update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-only&lt;/td&gt;
&lt;td&gt;Small (&amp;lt; 50 pages)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Fresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Large (any)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Medium (retrieval + LLM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Low–Medium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fresh (re-embed on update)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  RAG Pipeline Architecture
&lt;/h2&gt;

&lt;p&gt;A RAG pipeline has two sides: the &lt;strong&gt;ingest side&lt;/strong&gt; (run once per document update) and the &lt;strong&gt;query side&lt;/strong&gt; (run on every user request). Together they form five stages.&lt;/p&gt;

&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%2Fv5t12u3s1nqh0mr6vmpp.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%2Fv5t12u3s1nqh0mr6vmpp.png" alt="RAG Pipeline Architecture" width="800" height="1290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Ingest side (left) runs document processing once. Query side (right) runs on every user request.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The five stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingest&lt;/strong&gt; — load raw documents. Source can be text files, PDFs, database records, or API responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk&lt;/strong&gt; — split documents into pieces small enough to embed meaningfully. Chunk size is the most consequential parameter in the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed&lt;/strong&gt; — convert each chunk to a vector. The embedding model maps semantic meaning to a point in high-dimensional space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; — persist vectors (with their source text and metadata) in a vector database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query&lt;/strong&gt; — embed the user's question, find the nearest chunks, inject them into a prompt, and generate an answer.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Chunking Strategies
&lt;/h2&gt;

&lt;p&gt;Chunking is where most RAG pipelines fail. A bad chunking strategy produces irrelevant retrievals; irrelevant retrievals produce hallucinated answers. The model can't conjure information that wasn't in the retrieved chunks. In practice, I spend more time tuning chunk size and overlap than on any other pipeline parameter — getting it wrong is invisible until a user catches the model confidently citing the wrong passage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixed-Size Chunking
&lt;/h3&gt;

&lt;p&gt;Split every N characters or tokens. Simple to implement, ignores document structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Fixed-size chunking — simple but blunt
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk_fixed&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="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&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;list&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;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;  &lt;span class="c1"&gt;# overlap preserves context at boundaries
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overlap matters: without it, a sentence split across two chunks retrieves each half separately and both halves are incomplete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursive Chunking
&lt;/h3&gt;

&lt;p&gt;Try splitting on paragraph breaks (&lt;code&gt;\n\n&lt;/code&gt;), then line breaks (&lt;code&gt;\n&lt;/code&gt;), then spaces. Each level is a fallback when the previous splitter still produces chunks over the target size. This respects document structure — paragraphs before sentences before words.&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;_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&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;list&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recursively split text into chunks, preferring natural boundaries.

    Args:
        text: The input text to split.
        max_tokens: Approximate maximum chunk size in tokens (1 token ≈ 4 chars).

    Returns:
        List of text chunks, each within the max_tokens limit.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;max_chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;  &lt;span class="c1"&gt;# rough token → char estimate
&lt;/span&gt;
    &lt;span class="k"&gt;if&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&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="nf"&gt;strip&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;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Try splitting on paragraph breaks first, then newlines, then spaces
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&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="se"&gt;\n&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; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&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;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;separator&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&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;current&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&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;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;
                &lt;span class="k"&gt;else&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;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="c1"&gt;# Recurse on oversized parts
&lt;/span&gt;                    &lt;span class="k"&gt;if&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;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# No separator found — hard split
&lt;/span&gt;    &lt;span class="k"&gt;return&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;max_chars&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&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;max_chars&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Semantic Chunking
&lt;/h3&gt;

&lt;p&gt;Group sentences by semantic similarity — a sentence joins the current chunk if its embedding is similar enough; otherwise it starts a new chunk. Produces the most coherent chunks but is slower (requires embedding every sentence during ingestion).&lt;/p&gt;

&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%2Fkt3abefi2ps8o68zskdr.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%2Fkt3abefi2ps8o68zskdr.png" alt="Semantic Chunking" width="800" height="1371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Chunking strategy selection — recursive approach, falling back from paragraphs to lines to words.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Embeddings
&lt;/h2&gt;

&lt;p&gt;An embedding model converts text into a dense vector — a list of floats (e.g., 384 dimensions for &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;) where similar texts produce similar vectors. Semantic similarity becomes geometric proximity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Embeddings: &lt;code&gt;sentence-transformers&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sentence-transformers&lt;/code&gt; runs entirely locally — no API key, no per-token cost, no latency from network calls.&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;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;texts&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;Python is a high-level programming language.&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;Guido van Rossum created Python in 1991.&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;The weather is sunny today.&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="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# shape: (3, 384)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# (3, 384)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; is 80MB on disk, encodes ~14,000 sentences per second on CPU, and produces 384-dimensional vectors. It is the right default for local development and prototyping.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Embeddings
&lt;/h3&gt;

&lt;p&gt;For production, Anthropic's and OpenAI's embedding APIs trade local cost for higher-quality vectors at scale. Use them when your retrieval accuracy on domain-specific content drops below acceptable thresholds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Dims&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sentence-transformers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;Free (local)&lt;/td&gt;
&lt;td&gt;Best for dev/prototyping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sentence-transformers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;all-mpnet-base-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;Free (local)&lt;/td&gt;
&lt;td&gt;Higher quality, 3× slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;$0.02 / 1M tokens&lt;/td&gt;
&lt;td&gt;Good price/quality tradeoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic API&lt;/td&gt;
&lt;td&gt;Voyage-3 (via Voyage AI)&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;$0.06 / 1M tokens&lt;/td&gt;
&lt;td&gt;SOTA quality for RAG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep your embedding model consistent between ingestion and query time. If you embed documents with &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; and query with &lt;code&gt;text-embedding-3-small&lt;/code&gt;, your similarity scores will be meaningless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vector Storage with ChromaDB
&lt;/h2&gt;

&lt;p&gt;ChromaDB is an in-process vector database for Python. It requires no server, no Docker container, no cloud account — import it and use 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;chromadb&lt;/span&gt;

&lt;span class="c1"&gt;# In-memory client — resets between runs (good for unit tests)
&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;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Persistent client — stores to disk (good for development)
&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;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PersistentClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;./chroma_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add documents with pre-computed embeddings
&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ids&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;chunk_0&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;chunk_1&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;chunk_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]],&lt;/span&gt;
    &lt;span class="n"&gt;documents&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;Python is...&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;Guido van Rossum...&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;Weather is...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;metadatas&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;source&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;python_intro.txt&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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;python_intro.txt&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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;weather.txt&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="c1"&gt;# Query — returns top-3 nearest chunks
&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_embeddings&lt;/span&gt;&lt;span class="o"&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="mf"&gt;0.18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]],&lt;/span&gt;
    &lt;span class="n"&gt;n_results&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;include&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;documents&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;metadatas&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;distances&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;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&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="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadatas&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="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distances&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ChromaDB uses cosine similarity by default. Lower distance = more similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing a Vector Database
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ChromaDB&lt;/th&gt;
&lt;th&gt;Pinecone&lt;/th&gt;
&lt;th&gt;pgvector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Zero (in-process)&lt;/td&gt;
&lt;td&gt;Managed cloud&lt;/td&gt;
&lt;td&gt;Add extension to Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;Millions of vectors&lt;/td&gt;
&lt;td&gt;Billions&lt;/td&gt;
&lt;td&gt;Millions (with tuning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$70/month+ (managed)&lt;/td&gt;
&lt;td&gt;Postgres hosting cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtering&lt;/td&gt;
&lt;td&gt;Metadata filters&lt;/td&gt;
&lt;td&gt;Metadata filters&lt;/td&gt;
&lt;td&gt;Full SQL WHERE clauses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;Local disk&lt;/td&gt;
&lt;td&gt;Cloud-managed&lt;/td&gt;
&lt;td&gt;Postgres storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Local dev, prototypes&lt;/td&gt;
&lt;td&gt;Production at scale&lt;/td&gt;
&lt;td&gt;Teams already on Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with ChromaDB locally — it removes all infrastructure friction during development, which is where you need to iterate fastest. I reach for pgvector over Pinecone when the team is already on Postgres: the operational overhead is near-zero and full SQL filtering eliminates an entire class of retrieval bugs that metadata-only filters can't handle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retrieval Strategies
&lt;/h2&gt;

&lt;p&gt;Retrieval is not just "find the nearest vectors." Three strategies matter in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cosine Similarity Search
&lt;/h3&gt;

&lt;p&gt;The default. Compute the cosine similarity between the query vector and every stored vector; return the top-k most similar chunks. Fast, well-understood, and works well when the query language matches the document language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maximal Marginal Relevance (MMR)
&lt;/h3&gt;

&lt;p&gt;Standard top-k retrieval can return five chunks that all say the same thing — high similarity, low diversity. MMR trades some similarity for diversity: each additional chunk is selected to be both similar to the query &lt;em&gt;and&lt;/em&gt; different from already-selected chunks.&lt;/p&gt;

&lt;p&gt;ChromaDB does not implement MMR natively. Implement it post-retrieval:&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;_mmr_rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;candidate_embeddings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&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;candidate_docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="n"&gt;lambda_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Select top-k chunks using Maximal Marginal Relevance.

    Args:
        query_embedding: Embedded user query.
        candidate_embeddings: Embeddings of candidate chunks (over-fetch, e.g., top-20).
        candidate_docs: Text of each candidate chunk.
        k: Number of chunks to return.
        lambda_: Trade-off between relevance (1.0) and diversity (0.0).

    Returns:
        k chunks selected for both relevance and diversity.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Cosine similarity: query vs candidates
&lt;/span&gt;    &lt;span class="n"&gt;sim_to_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;selected_indices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&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;remaining&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="nf"&gt;range&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;candidate_docs&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;_&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="nf"&gt;min&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;selected_indices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# First pick: highest similarity to query
&lt;/span&gt;            &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sim_to_query&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Subsequent picks: balance relevance vs redundancy
&lt;/span&gt;            &lt;span class="n"&gt;selected_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;selected_indices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;sim_q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sim_to_query&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="n"&gt;sim_selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands&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="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;selected_vecs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cands&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="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selected_vecs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-9&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;j&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selected_vecs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lambda_&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sim_q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lambda_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;sim_selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&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="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

        &lt;span class="n"&gt;selected_indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate_docs&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;selected_indices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hybrid Search (BM25 + Semantic)
&lt;/h3&gt;

&lt;p&gt;Semantic search handles paraphrase and synonymy well. BM25 (keyword search) handles exact terms — product codes, names, technical identifiers — better. Hybrid search runs both and combines scores (typically via Reciprocal Rank Fusion). Use hybrid when your documents contain precise identifiers that semantic search might miss. I default to MMR over top-k similarity the moment a domain has redundant content — policy documentation, API reference pages, and anything generated from a template will poison straight similarity retrieval with near-duplicate chunks every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context Augmentation
&lt;/h2&gt;

&lt;p&gt;Retrieved chunks are useful only if the prompt tells Claude how to use them. A weak prompt ("here are some documents, answer the question") produces weak answers. A well-structured prompt produces grounded, citable answers.&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;_build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&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;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Format retrieved chunks and question into a grounded generation prompt.

    Args:
        question: The user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s original question.
        chunks: Retrieved document chunks, ordered by relevance (most relevant first).

    Returns:
        A formatted prompt that instructs Claude to cite sources and acknowledge gaps.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;context_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Source &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant. Answer the question below using only the provided sources.

Rules:
- Cite which source(s) support each claim (e.g., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;According to Source 2...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;).
- If the sources do not contain enough information to answer the question, say so explicitly.
- Do not use knowledge outside the provided sources.

Sources:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context_block&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Answer:&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three rules matter. Citing sources forces the model to ground claims in retrieved text rather than training data. Acknowledging gaps prevents confident-sounding hallucinations. Forbidding outside knowledge keeps the model from blending retrieved context with parametric knowledge unpredictably.&lt;/p&gt;




&lt;h2&gt;
  
  
  Generation with Claude
&lt;/h2&gt;

&lt;p&gt;The query method ties everything together: embed the question, retrieve chunks, build the prompt, call Claude, and return the answer.&lt;/p&gt;

&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%2Fzt2xkkavrboj8ao6kvgd.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%2Fzt2xkkavrboj8ao6kvgd.png" alt="Generation with Claude" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Full query path from user question to Claude-generated answer.&lt;/em&gt;&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;anthropic&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# reads ANTHROPIC_API_KEY from environment
&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Check why generation stopped
&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;text&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Response was cut off — increase max_tokens or reduce prompt length
&lt;/span&gt;        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;text&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;[Response truncated — max_tokens reached]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Generation stopped: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always check &lt;code&gt;stop_reason&lt;/code&gt;. &lt;code&gt;end_turn&lt;/code&gt; means the model finished naturally. &lt;code&gt;max_tokens&lt;/code&gt; means the answer was cut off — a common silent failure when retrieved chunks are large and the combined prompt + answer exceeds your token budget.&lt;/p&gt;

&lt;p&gt;For cheap classification tasks within a RAG system (e.g., query routing, intent detection, relevance filtering), use &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt; instead of &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;. Haiku is significantly faster and cheaper for short-context classification where generation quality is less critical.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;RAGPipeline&lt;/code&gt; Class
&lt;/h2&gt;

&lt;p&gt;The full implementation encapsulates all stages into a single class with three public methods: &lt;code&gt;ingest&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, and the private helpers.&lt;/p&gt;

&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%2Frp6rmf8y460i0uw0gyu1.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%2Frp6rmf8y460i0uw0gyu1.png" alt="RAGPipeline Class" width="726" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: &lt;code&gt;RAGPipeline&lt;/code&gt; class structure — three public methods, three private helpers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The constructor initializes all three dependencies. Chunk and embed are private because callers should not need to call them directly — they are implementation details of &lt;code&gt;ingest&lt;/code&gt; and &lt;code&gt;query&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Evaluation
&lt;/h2&gt;

&lt;p&gt;A RAG pipeline with no evaluation is a guess. Add measurement before shipping.&lt;/p&gt;

&lt;p&gt;Three metrics from the &lt;a href="https://docs.ragas.io/" rel="noopener noreferrer"&gt;RAGAS framework&lt;/a&gt; cover the core failure modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;How it catches failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Faithfulness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Is every claim in the answer supported by a retrieved chunk?&lt;/td&gt;
&lt;td&gt;Catches hallucinations — the model added facts not in context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Answer Relevancy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Does the answer actually address the question?&lt;/td&gt;
&lt;td&gt;Catches topic drift — the model answered a different question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context Recall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Did retrieval surface the chunks needed to answer?&lt;/td&gt;
&lt;td&gt;Catches retrieval failures — the right chunks weren't found&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run evaluation on a golden dataset — 20–50 question/answer pairs you've manually verified. Automate it in CI so a configuration change that breaks retrieval doesn't silently ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# RAGAS evaluation — requires: pip install ragas
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ragas.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;

&lt;span class="n"&gt;eval_data&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;question&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is Python?&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;Who created Python?&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;answer&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python is a programming language.&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;Guido van Rossum.&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;contexts&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python is a high-level language...&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Guido van Rossum created Python in 1991...&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ground_truth&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python is a high-level programming language.&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;Guido van Rossum.&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;faithfulness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer_relevancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_recall&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Chunks larger than 600 tokens drown the retrieval signal.&lt;/strong&gt;&lt;br&gt;
A 600-token chunk covers multiple topics. When you embed it, the vector averages over those topics and becomes less specific to any one of them. Similarity search returns chunks that are vaguely related to the query, not precisely relevant. Keep chunks under 400 tokens. If a passage requires more context, overlap consecutive chunks by 50–100 tokens rather than enlarging the chunk size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Not deduplicating similar chunks before storing.&lt;/strong&gt;&lt;br&gt;
If your corpus contains repeated passages (headers, boilerplate, legal disclaimers), those chunks will dominate retrieval — every query returns five variations of the same paragraph. Deduplicate before ingestion: compute a hash of each chunk's text and discard exact duplicates, then use a similarity threshold (cosine similarity &amp;gt; 0.95) to collapse near-duplicates. I've seen this sink a support-bot demo: the corpus was a product manual where the safety warning appeared verbatim on 40 of 200 pages — top-5 retrieval returned the warning for nearly every query, and the model dutifully answered "please refer to a qualified technician" regardless of what was asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Embedding the raw query without expansion.&lt;/strong&gt;&lt;br&gt;
A user asking "how does Python handle memory?" may not use the same words as your documentation ("garbage collection", "reference counting", "memory management"). Query expansion generates alternative phrasings before embedding: &lt;code&gt;original_query + hypothetical_answer_keywords&lt;/code&gt;. Hypothetical Document Embeddings (HyDE) generates a fake answer to the question and embeds that instead — the fake answer often matches document language better than the raw question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Skipping evaluation entirely.&lt;/strong&gt;&lt;br&gt;
Most RAG pipelines are shipped based on developer intuition ("the answers look right in testing"). Without a golden dataset and automated metrics, you won't know when a configuration change — a new chunk size, a different top-k, an updated embedding model — makes retrieval worse. Define your evaluation dataset before you tune parameters, not after. I've watched a team spend two weeks tuning chunk size upward because answers "felt" more complete — then discover via RAGAS that faithfulness had dropped from 0.89 to 0.71 because larger chunks were diluting the retrieved context with off-topic sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Using the same model for embedding and generation.&lt;/strong&gt;&lt;br&gt;
Anthropic's Claude models are generation models, not embedding models. Using a generation model's hidden states as embeddings produces inferior vectors compared to models trained specifically for semantic similarity (sentence-transformers, OpenAI's &lt;code&gt;text-embedding-3-*&lt;/code&gt;, Voyage AI). Keep embedding and generation as separate concerns: sentence-transformers (or a dedicated embedding API) for vectors, Claude for generation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;

&lt;p&gt;The complete implementation lives in two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/pipeline.py&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;RAGPipeline&lt;/code&gt; class with all methods documented and implemented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/main.py&lt;/code&gt;&lt;/strong&gt; — demo that ingests five documents about Python and runs three queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate        &lt;span class="c"&gt;# Windows: .venv\Scripts\activate&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Add your ANTHROPIC_API_KEY to .env&lt;/span&gt;

python src/main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ingested 5 documents → 12 chunks stored.

Q: What is Python's GIL?
A: According to Source 1, Python's Global Interpreter Lock (GIL) is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes simultaneously...

Q: Who created Python and when?
A: According to Source 3, Python was created by Guido van Rossum and first released in 1991...

Q: What is list comprehension?
A: According to Source 2, list comprehension is a concise syntax for creating lists based on existing iterables...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt; → &lt;code&gt;ai-integration/rag-pipeline/&lt;/code&gt; — see README for setup steps.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;RAG is the right architecture when your LLM needs access to private, large, or frequently updated data. The implementation is not what makes or breaks it — the parameters are: chunk size, overlap, retrieval strategy, and whether you bother to measure any of it. Most pipelines that fail in production fail because they were tuned by intuition and never measured. Add the RAGAS evaluation step before you ship anything to users, keep your chunks under 400 tokens, and you will avoid the failure modes that make RAG look unreliable. The architecture isn't fragile — the shortcuts are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/api/messages" rel="noopener noreferrer"&gt;Anthropic API Documentation — Messages&lt;/a&gt; — official reference for the &lt;code&gt;messages.create&lt;/code&gt; endpoint, &lt;code&gt;stop_reason&lt;/code&gt; values, and model IDs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.trychroma.com/" rel="noopener noreferrer"&gt;ChromaDB Documentation&lt;/a&gt; — official docs covering collections, embedding functions, metadata filtering, and persistence&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers Documentation&lt;/a&gt; — model selection guide, semantic similarity, and batch encoding&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.ragas.io/" rel="noopener noreferrer"&gt;RAGAS Documentation&lt;/a&gt; — RAG evaluation framework; faithfulness, answer relevancy, and context recall metrics&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.voyageai.com/docs/embeddings" rel="noopener noreferrer"&gt;Voyage AI Embeddings (Anthropic ecosystem)&lt;/a&gt; — production-quality embedding API recommended by Anthropic for RAG applications&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist. Building a RAG system? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Who Absorbs the Margin — Client, SIer, or the AI Vendor: Renegotiating SI Economics</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/who-absorbs-the-margin-client-sier-or-the-ai-vendor-renegotiating-si-economics-2ce2</link>
      <guid>https://dev.to/brywritescode/who-absorbs-the-margin-client-sier-or-the-ai-vendor-renegotiating-si-economics-2ce2</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every dollar an AI tool saves an SI on delivery cost is a dollar three different parties have a plausible claim to: the client (who's aware operating costs went down), the AI platform vendor (whose licensing terms increasingly shift toward capturing usage-based value), and the SI (whose judgment turned a raw capability into a delivered outcome).&lt;/li&gt;
&lt;li&gt;Procurement teams are learning to sort renewals into three buckets: renew and absorb, renegotiate to consumption, or replace and build. Expect more SIs to get pushed into the second and third buckets as clients understand AI has genuinely lowered the SI's own cost base.&lt;/li&gt;
&lt;li&gt;93% of sellers reported struggling to quantify and defend the value they'd actually added, which puts most SIs into these renegotiations without a real number to counter a client's discount demand.&lt;/li&gt;
&lt;li&gt;SIs that come through this pressure well are the ones that stop treating margin as something to defend by obscuring cost structure, and start treating it as something to defend by pricing a specific, demonstrable judgment contribution the client couldn't get from the AI platform directly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A client's CFO said something to me directly not long ago that a lot of SI account teams are starting to hear some version of: "I know what your AI licensing costs. I know what it used to cost you to deliver this. Explain to me why I'm still paying the old number." It's not an unreasonable question, and at this point in the AI-services transition, most SIs don't have a rehearsed answer for it. For a while, plenty of firms have been quietly pocketing the AI-driven cost reduction as pure margin, hoping nobody with enough visibility into the underlying economics would ask.&lt;/p&gt;

&lt;p&gt;The honest framing is that AI-driven delivery savings don't belong to any one party by default. They're contested, three ways. Clients have a real claim: operational costs genuinely came down, and clients increasingly know it, because AI tool pricing itself became public and metered rather than opaque. AI platform vendors have a claim too, since a growing share of enterprise software licensing is shifting toward capturing usage-based value directly, rather than just charging a flat seat fee and letting downstream margin sit wherever it lands. And the SI has a claim, the one most vulnerable to getting argued away in a renegotiation: the actual judgment, integration work, and delivery risk absorbed in turning a raw AI capability into something a client can actually rely on in production.&lt;/p&gt;

&lt;p&gt;Procurement organizations are getting sophisticated about this fast. A fairly standard renewal framework is already sorting vendor relationships into three buckets: renew and absorb (accept the current pricing, usually for low-volatility, well-understood work), renegotiate to consumption (move rigid seat- or hour-based pricing to a usage-based model that tracks real utilization), or replace and build (walk away from the vendor relationship entirely and bring the capability in-house, now that AI has made that a realistic option for more capabilities than it used to be). SIs that show up to these conversations without a defensible, quantified account of their own value contribution are getting sorted into renegotiate-to-consumption or replace-and-build far more often than SIs that can show precisely what they still add. A Holden Advisors study found 93% of sellers admitted they struggled to accurately quantify and defend their value, which means most SIs are walking into these renegotiations structurally unprepared for a client who's done their homework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Margin Actually Goes: Three Claims on the Same Savings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claimant&lt;/th&gt;
&lt;th&gt;Basis for the Claim&lt;/th&gt;
&lt;th&gt;What Wins the Argument in Practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Aware that AI genuinely lowered the vendor's delivery cost; expects some pass-through&lt;/td&gt;
&lt;td&gt;A vendor with a metered, transparent cost basis wins credibility; one hiding behind an unchanged invoice loses it fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI platform vendor&lt;/td&gt;
&lt;td&gt;Owns the underlying capability; increasingly prices on usage rather than flat seat fees&lt;/td&gt;
&lt;td&gt;Whatever margin the platform vendor's own licensing terms capture directly. This claim is largely settled by contract, not negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Systems integrator&lt;/td&gt;
&lt;td&gt;Provided judgment, integration, delivery risk absorption, and a working result the client couldn't have gotten by licensing the AI tool directly&lt;/td&gt;
&lt;td&gt;A quantified account of that judgment, specific defect rates avoided, integration complexity resolved, risk absorbed, wins. A vague "we add value" claim loses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; don't walk into a renewal conversation assuming your margin is safe because it always has been. Assume the client already has a rough estimate of your AI-driven cost reduction, and come with a specific, demonstrable account of what you add beyond the tool itself, or expect to get renegotiated into the consumption bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defending Margin With a Real Number Instead of a Vague Claim
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quantify your own AI-driven cost reduction honestly, before the client does it for you.&lt;/strong&gt; If you know the number, you control how it's framed. If the client calculates it independently and you're caught unprepared, you're negotiating from a defensive position for the rest of the relationship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate "what the AI platform did" from "what we did with it," in terms specific enough to price.&lt;/strong&gt; Defect rates avoided, integration issues resolved that the AI output alone would have missed, delivery risk actually absorbed: name the specific contribution, not a general appeal to expertise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer the renegotiation before the client demands it.&lt;/strong&gt; SIs that proactively bring a fair, transparent pricing update to a renewal conversation are keeping far more of their margin than SIs that wait to be confronted, because proactive framing reads as partnership and reactive framing reads as having been caught.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know which of your engagements are genuinely replace-and-build candidates, and don't fight that battle.&lt;/strong&gt; Some capabilities really have become cheap enough for a client to bring in-house. Contesting that reality burns credibility you'll need for the engagements where your judgment genuinely isn't replaceable.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do we know our own AI-driven delivery cost reduction, on our top client relationships, well enough to defend a number if the client brings one first?&lt;/li&gt;
&lt;li&gt;Can we name, specifically, what we add beyond the AI platform's raw output on our current engagements, or would we be making a vague appeal to "expertise" in a real renegotiation?&lt;/li&gt;
&lt;li&gt;Are we proactively bringing pricing conversations to clients as AI changes our cost base, or waiting for a CFO to ask the question first?&lt;/li&gt;
&lt;li&gt;Which of our current engagements are honest replace-and-build candidates for the client, and are we prepared for that conversation instead of resisting it?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;There's no default answer to who keeps the margin AI creates. It depends on who shows up to the renegotiation with a real number. Clients who understand the underlying cost shift will ask for a share of it, and they're not wrong to. AI platform vendors are already capturing their share through licensing terms most SIs don't control. What's actually contestable is the SI's own piece, and 93% of sellers walking into that conversation without a quantified value story is exactly why so much margin is likely to get renegotiated away from firms that could have kept more of it. The ones that hold their ground will be the ones treating their judgment as a line item with a number attached, not an assumption everyone keeps taking on faith.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.baytechconsulting.com/blog/saas-pricing-shift-negotiate-ai-driven-renewals" rel="noopener noreferrer"&gt;BayTech Consulting: SaaS Pricing Shift, How to Negotiate AI-Driven Renewals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hirefraction.com/blog/ai-is-killing-saas-margins-outcome-based-pricing-is-how-you-get-them-back/" rel="noopener noreferrer"&gt;Hire Fraction: AI Is Killing SaaS Margins. Outcome-Based Pricing Is How You Get Them Back&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.manufacturingtomorrow.com/article/2026/06/how-manufacturers-and-system-integrators-can-build-pricing-power-in-a-commodity-market/27634" rel="noopener noreferrer"&gt;Manufacturing Tomorrow: How Manufacturers and System Integrators Can Build Pricing Power in a Commodity Market&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Heading into a renewal conversation without a defensible value number? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>consulting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Amazon SQS vs SNS: Queues, Fan-Out, and Picking the Right One from the CLI</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 08 Sep 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/amazon-sqs-vs-sns-queues-fan-out-and-picking-the-right-one-from-the-cli-3f6m</link>
      <guid>https://dev.to/brywritescode/amazon-sqs-vs-sns-queues-fan-out-and-picking-the-right-one-from-the-cli-3f6m</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQS is a queue — one message, delivered to one consumer, held until processed. SNS is pub/sub — one message, fanned out to every subscriber. The fan-out pattern combining both is the standard, not an either/or choice.&lt;/li&gt;
&lt;li&gt;SNS-to-SQS delivery requires the queue's access policy to explicitly allow the topic to call &lt;code&gt;SendMessage&lt;/code&gt; — skip this and messages vanish with no error on the publish side.&lt;/li&gt;
&lt;li&gt;FIFO queues cap at 3,000 messages/second batched (300/s unbatched) unless you enable high-throughput mode, which raises that to 70,000/s. Both SQS and SNS also bill in 64 KB payload chunks, same mechanic as EventBridge.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;aws-cli/2.35.x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An IAM role with &lt;code&gt;sqs:*&lt;/code&gt; and &lt;code&gt;sns:*&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jq&lt;/code&gt; installed for parsing CLI JSON output in the examples below&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I still get asked "should I use SQS or SNS?" as though it's a fork in the road. Most of the time the honest answer is both, wired together — SNS decides who gets notified, SQS makes sure each of them actually gets to process the message at their own pace without losing it if they're briefly unavailable. Treating them as competing choices is how teams end up building a fan-out mechanism inside application code that SNS already does for free.&lt;/p&gt;

&lt;p&gt;The part that trips people up isn't the concept. It's the access policy. Subscribe an SQS queue to an SNS topic without granting the topic permission to send to that queue, and the subscription succeeds, the topic publish succeeds, and the message simply never arrives — no error surfaces anywhere in that chain. I've debugged this exact silent failure in a client's fan-out pipeline, and it's the single most common gap in CLI tutorials for this pattern.&lt;/p&gt;

&lt;p&gt;This article builds a standalone queue, a standalone topic, and the full fan-out pattern with the access policy step included, then covers where FIFO changes the throughput math.&lt;/p&gt;




&lt;h2&gt;
  
  
  Queue vs Topic, Conceptually
&lt;/h2&gt;

&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%2Fqpehoogg5nma3czt5vrc.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%2Fqpehoogg5nma3czt5vrc.png" alt="Queue vs Topic, Conceptually" width="631" height="864"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: SQS delivers each message to exactly one consumer; SNS delivers a copy to every subscriber.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A message sitting in an SQS queue waits until something polls for it. A message published to an SNS topic is pushed immediately to every current subscriber — there's no "waiting" concept on the topic itself, which is exactly why SNS alone is a poor fit for a subscriber that might be temporarily down. That's what the fan-out pattern fixes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Standalone SQS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Standard queue — no ordering guarantee, near-unlimited throughput.&lt;/span&gt;
aws sqs create-queue &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders-standard-queue

&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders-standard-queue &lt;span class="nt"&gt;--query&lt;/span&gt; QueueUrl &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws sqs send-message &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-body&lt;/span&gt; &lt;span class="s1"&gt;'{"orderId":"order-abc123","status":"pending"}'&lt;/span&gt;

aws sqs receive-message &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--max-number-of-messages&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# FIFO queue — strict ordering within a message group, capped throughput.&lt;/span&gt;
aws sqs create-queue &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders-fifo-queue.fifo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="s1"&gt;'{"FifoQueue":"true","ContentBasedDeduplication":"true"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queue type is permanent. There's no &lt;code&gt;update-queue-type&lt;/code&gt; command — you create a new queue and migrate, you don't convert an existing one. Decide standard vs FIFO before you have production traffic depending on the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Standalone SNS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sns create-topic &lt;span class="nt"&gt;--name&lt;/span&gt; orders-topic

&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sns list-topics &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Topics[?contains(TopicArn,'orders-topic')].TopicArn"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws sns publish &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s1"&gt;'{"orderId":"order-abc123","status":"pending"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"Order Created"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That publish immediately pushes to every current subscriber — email, SMS, HTTPS endpoint, Lambda, or SQS. Nothing is retained after delivery attempts complete. If you need durability — a subscriber that's slow or briefly offline shouldn't lose the message — that's the case for combining SNS with SQS, not using SNS alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fan-Out Pattern — Including the Step Everyone Skips
&lt;/h2&gt;

&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%2F76bu37p489hayovbos3g.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%2F76bu37p489hayovbos3g.png" alt="The Fan-Out Pattern — Including the Step Everyone Skips" width="784" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: fan-out delivery depends entirely on each queue's access policy explicitly trusting the topic — there's no other permission gate in this chain.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs create-queue &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders-notifications-queue
&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; orders-notifications-queue &lt;span class="nt"&gt;--query&lt;/span&gt; QueueUrl &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-attributes &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--attribute-names&lt;/span&gt; QueueArn &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Attributes.QueueArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# This is the step that gets skipped. Without it, the subscription&lt;/span&gt;
&lt;span class="c"&gt;# below will succeed and messages will vanish with zero errors.&lt;/span&gt;
aws sqs set-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Policy&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Version&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;2012-10-17&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Statement&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:[{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Effect&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Allow&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Principal&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Service&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;sns.amazonaws.com&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;},&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Action&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;sqs:SendMessage&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Resource&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;QUEUE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Condition&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;ArnEquals&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;aws:SourceArn&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;}}}]}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;

aws sns subscribe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; sqs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notification-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="nv"&gt;RawMessageDelivery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RawMessageDelivery=true&lt;/code&gt; matters as much as the policy step. Without it, the queue receives SNS's full JSON envelope — &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;MessageId&lt;/code&gt;, &lt;code&gt;TopicArn&lt;/code&gt;, and the actual message nested inside a &lt;code&gt;Message&lt;/code&gt; string field — instead of your original payload directly. Consumers that expect the raw body will parse the wrong structure and either error out or silently read &lt;code&gt;undefined&lt;/code&gt; fields.&lt;/p&gt;




&lt;h2&gt;
  
  
  FIFO Throughput Limits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Throughput ceiling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FIFO queue, batched&lt;/td&gt;
&lt;td&gt;3,000 messages/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIFO queue, unbatched&lt;/td&gt;
&lt;td&gt;300 messages/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIFO queue, high-throughput mode&lt;/td&gt;
&lt;td&gt;70,000 messages/second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNS FIFO topic&lt;/td&gt;
&lt;td&gt;3,000 messages/second or 20 MB/second, whichever is hit first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard queue/topic&lt;/td&gt;
&lt;td&gt;No published ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;High-throughput mode isn't automatic — it's an explicit setting (&lt;code&gt;DeduplicationScope&lt;/code&gt; and &lt;code&gt;FifoThroughputLimit&lt;/code&gt; attributes set to &lt;code&gt;messageGroup&lt;/code&gt; and &lt;code&gt;perMessageGroupId&lt;/code&gt; respectively) that trades a small amount of strict cross-group ordering guarantee for the throughput increase. If you need strict global ordering across all message groups, don't enable it. If your ordering requirement is per-customer or per-order (a common case), high-throughput mode is almost always the right call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Subscribing SQS to SNS without a queue access policy&lt;/strong&gt;&lt;br&gt;
The subscription API call succeeds regardless. The failure is invisible until someone notices messages aren't arriving — which could be minutes or weeks later depending on traffic patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Forgetting &lt;code&gt;RawMessageDelivery&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Consumers built against the raw payload shape break silently or throw confusing parsing errors when they receive SNS's wrapped envelope instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Choosing FIFO by default "to be safe"&lt;/strong&gt;&lt;br&gt;
FIFO's throughput ceiling and stricter deduplication requirements are a real cost. Most systems don't actually need strict ordering — verify the requirement before paying for it in complexity and throughput headroom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Trying to convert a standard queue to FIFO&lt;/strong&gt;&lt;br&gt;
There's no such command. You create a new FIFO queue and migrate producers and consumers to it — plan for that as a deploy, not a config change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Long polling (&lt;code&gt;--wait-time-seconds 20&lt;/code&gt; on &lt;code&gt;receive-message&lt;/code&gt;) eliminates the empty-receive charges that pile up from aggressive short polling — this alone is one of the two biggest SQS cost levers, the other being batching sends and receives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Scope queue access policies to the specific topic ARN, not a wildcard principal. The AWS Tip source in this article's research log describes a real incident where a queue policy pointed at a stale topic ARN after a topic recreation — silent failure, same root cause as skipping the policy entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Both services bill in 64 KB payload chunks. A consistently large message body (nested JSON, embedded metadata) multiplies your bill the same way it does on EventBridge — trim payloads, pass references where you can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; Alarm on &lt;code&gt;ApproximateAgeOfOldestMessage&lt;/code&gt; for SQS queues — a rising value means consumers aren't keeping up, well before the queue depth itself looks alarming.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Fan-Out Setup Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;TOPIC_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;-topic&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;QUEUE_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;-notifications-queue&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

aws sns create-topic &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sns list-topics &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Topics[?contains(TopicArn,'&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')].TopicArn"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws sqs create-queue &lt;span class="nt"&gt;--queue-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; QueueUrl &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-attributes &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--attribute-names&lt;/span&gt; QueueArn &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Attributes.QueueArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;POLICY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sns.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;QUEUE_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;","Condition":{"ArnEquals":{"aws:SourceArn":"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"}}}]}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

aws sqs set-queue-attributes &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Policy&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POLICY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-Rs&lt;/span&gt; .&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
aws sns subscribe &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; sqs &lt;span class="nt"&gt;--notification-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="nv"&gt;RawMessageDelivery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true

echo&lt;/span&gt; &lt;span class="s2"&gt;"Fan-out ready: &lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_NAME&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source including a batched consumer with long polling: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/amazon-sqs-vs-sns-cli/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Stop framing SQS and SNS as competing choices — SNS decides who hears about something, SQS makes sure each listener actually gets to act on it without losing the message if they're briefly unavailable. The fan-out pattern combining both is the default for a reason. The one step that will actually cost you debugging time if skipped is the queue access policy: get it wrong and everything upstream reports success while the message goes nowhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/" rel="noopener noreferrer"&gt;Amazon SQS Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/subscribe-sqs-queue-to-sns-topic.html" rel="noopener noreferrer"&gt;Subscribing an Amazon SQS queue to an Amazon SNS topic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/sqs/create-queue.html" rel="noopener noreferrer"&gt;create-queue — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/fifo-topic-code-examples.html" rel="noopener noreferrer"&gt;Amazon SNS code examples for FIFO topics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Designing a messaging or fan-out architecture on AWS? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Multi-Tier Subcontracting Pyramid Under Pressure: What Happens to Body-Shop SIers When AI Writes the Code</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/the-multi-tier-subcontracting-pyramid-under-pressure-what-happens-to-body-shop-siers-when-ai-1ocp</link>
      <guid>https://dev.to/brywritescode/the-multi-tier-subcontracting-pyramid-under-pressure-what-happens-to-body-shop-siers-when-ai-1ocp</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Japan's &lt;em&gt;tajuu-shitauke&lt;/em&gt; (multi-layer subcontracting) structure routes enterprise IT work from a prime contractor (&lt;em&gt;motoke&lt;/em&gt;) down through one or more subcontractor tiers to the engineers who actually write and test the code, a pattern that's persisted, largely unchanged, since the mainframe era.&lt;/li&gt;
&lt;li&gt;Each tier historically takes its margin off the top before passing reduced-price work downward. A client-paid ¥1,000,000 monthly rate can leave the engineer actually doing the work with roughly ¥350,000 to ¥400,000 after cascading through two or three intermediary layers.&lt;/li&gt;
&lt;li&gt;AI-assisted coding tools compress exactly the layer this structure depends on most: routine implementation, testing, and documentation work performed by second- and third-tier subcontractors and individual SES engineers.&lt;/li&gt;
&lt;li&gt;The pyramid's top (client relationships, architecture, governance) looks likely to hold up. Its middle and bottom, firms and engineers whose entire value proposition is executing defined tasks at a lower price than the tier above, are facing the sharpest margin compression in the industry.&lt;/li&gt;
&lt;li&gt;Japan's 2026 Subcontract Act reform, which expanded transparency requirements to creative and consulting services, gives lower-tier firms a real legal lever to push back on unfair pass-through pricing right as AI is already reshaping what "fair" pricing even means.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A subcontractor relationship I've watched come under real strain recently is a fairly typical version of what's starting to happen across the industry. This is a second-tier firm that built a stable, unglamorous business supplying detailed-design and coding-and-testing engineers to a prime contractor's enterprise projects, at a rate that undercuts the tier above it. That's the entire business model: execute defined tasks, reliably, for less than the next tier up would charge. As the prime contractor's own AI-assisted tooling starts producing that same detailed-design and coding output directly, at a fraction of the cost and turnaround time, there's no lower price the second-tier firm can offer to stay competitive. It isn't underpriced. It's becoming structurally obsolete.&lt;/p&gt;

&lt;p&gt;The pyramid this firm sits inside is one of the most distinctive features of Japan's IT industry. The prime contractor, or &lt;em&gt;motoke&lt;/em&gt;, wins the client relationship and the overall project, then delegates substantial portions of detailed design, implementation, integration testing, and commissioning to one or more subcontractor tiers below it. Prime contractors capture the largest margins, commonly cited around 30-40%. First-tier subcontractors run 20-30%. Second-tier and below drop to 10-15%. Individual engineers, often working under System Engineering Service (SES) staffing arrangements, sit at the bottom, with compensation shrinking at each layer above them. A documented rate cascade shows the mechanism concretely: a client paying roughly ¥1,000,000 a month for an engineer's time can leave that engineer with something in the range of ¥350,000 to ¥400,000 after the intermediary tiers each take their cut.&lt;/p&gt;

&lt;p&gt;This structure has survived for decades because it solved a real coordination problem: large enterprise projects needed a way to scale headcount up and down without every firm in the chain carrying the client relationship or the delivery risk directly. AI-assisted coding isn't attacking that coordination function. It's attacking the thing the lower tiers are actually selling, person-hours of implementation and testing labor, priced progressively lower the further down the pyramid you look. As a prime contractor, or increasingly the client itself, can generate a larger share of that same output directly, the lower tiers' entire value proposition, execute cheaply, stops being a viable business on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pyramid Layer Economics Under AI Compression
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TD
    Client[Client] --&amp;gt;|pays full rate| Prime[Prime Contractor Motoke]
    Prime --&amp;gt;|passes down reduced rate, keeps 30 to 40 percent margin| Tier1[First-Tier Subcontractor]
    Tier1 --&amp;gt;|passes down reduced rate, keeps 20 to 30 percent margin| Tier2[Second-Tier Subcontractor]
    Tier2 --&amp;gt;|passes down reduced rate, keeps 10 to 15 percent margin| SES[Individual Engineer SES]
    AI[AI-Assisted Coding Tooling] -.-&amp;gt;|replaces routine output of| Tier2
    AI -.-&amp;gt;|replaces routine output of| SES&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Historical Role&lt;/th&gt;
&lt;th&gt;Historical Margin&lt;/th&gt;
&lt;th&gt;Effect of AI-Assisted Coding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Pays full contracted rate&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Increasingly aware of the gap between what AI can produce and what's still being billed for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prime contractor (motoke)&lt;/td&gt;
&lt;td&gt;Client relationship, architecture, governance, overall delivery risk&lt;/td&gt;
&lt;td&gt;~30-40%&lt;/td&gt;
&lt;td&gt;Largely intact so far. Judgment and relationship work AI doesn't replace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-tier subcontractor&lt;/td&gt;
&lt;td&gt;Mid-scale delivery management, some architecture&lt;/td&gt;
&lt;td&gt;~20-30%&lt;/td&gt;
&lt;td&gt;Under pressure, but retains value where it manages integration complexity AI can't own alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second-tier subcontractor and below&lt;/td&gt;
&lt;td&gt;Detailed design, coding, testing execution&lt;/td&gt;
&lt;td&gt;~10-15%&lt;/td&gt;
&lt;td&gt;Hardest hit. This is exactly the work AI-assisted tooling compresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Individual engineer (SES)&lt;/td&gt;
&lt;td&gt;Task-level implementation&lt;/td&gt;
&lt;td&gt;Remainder after cascading cuts&lt;/td&gt;
&lt;td&gt;Displaced where the task is routine; in higher demand where verification and AI-output review are the actual job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; if your firm's position in this chain has always been "we execute the same task the tier above us does, for less," that position won't survive AI-assisted coding regardless of how thin you cut your margin further. The layers most likely to survive are the ones pricing judgment, integration complexity, or client trust, not marginal labor cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Likely to Survive, and What a Lower-Tier Firm Should Change Now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firms moving up the value chain, not just down on price.&lt;/strong&gt; The subcontractors most likely to survive won't compete on being cheaper than the tier above. They're taking on integration and governance responsibility that used to sit with the prime contractor, becoming harder to replace with AI output alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firms specializing in verifying AI-generated output, not just producing more of it.&lt;/strong&gt; Reviewing and validating AI-generated code against a client's actual production constraints is turning out to require exactly the kind of experienced-engineer judgment that routine implementation work didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firms using Japan's 2026 Subcontract Act reform actively.&lt;/strong&gt; The reform's expanded transparency requirements give lower-tier firms real standing to contest unfair pass-through pricing. Firms that understand and use this leverage should renegotiate from a stronger position than firms that don't know the reform applies to them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firms consolidating instead of competing on the same shrinking margin.&lt;/strong&gt; Some second- and third-tier firms are merging capabilities, combining a compressed coding-and-testing practice with a smaller firm's domain expertise, to offer something closer to the first-tier's integration value than to commodity execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firms that change nothing.&lt;/strong&gt; These are the most exposed to winding down, getting acquired for their client relationships and remaining engineers, or shrinking into a much smaller commodity-execution niche with correspondingly thinner margins.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Where does our firm actually sit in our clients' delivery chains, and is our value proposition still "we execute this cheaper than the tier above," or has it genuinely moved to something AI-assisted tooling can't produce directly?&lt;/li&gt;
&lt;li&gt;If a prime contractor above us started generating our layer's output directly with AI tooling, what would we still have left to sell them?&lt;/li&gt;
&lt;li&gt;Are we aware of what Japan's 2026 Subcontract Act reform actually changed for transparency and fair pricing in multi-tier arrangements, and have we used it in a renegotiation?&lt;/li&gt;
&lt;li&gt;Have we tested whether our engineers are more valuable reviewing and validating AI-generated output than producing new output themselves, and are we pricing that shift yet?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The multi-tier subcontracting pyramid isn't collapsing, and it's not going to. What it built its lower tiers on, the assumption that there's always a cheaper price for the same routine task one layer further down, is what's breaking. Firms most likely to weather the pressure are moving toward judgment, integration, and verification work. Firms trying to survive by cutting their price further into an already-thin margin are the most exposed. The prime-contractor layer looks likely to hold up fine, because it was never really selling person-hours in the first place. Everyone underneath it needs to figure out, quickly, whether they've been selling person-hours the whole time, and if so, what else they actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youngju.dev/transcribe/culture/2026-03-19-japan-it-industry-structure-subcontracting.en" rel="noopener noreferrer"&gt;youngju.dev: Japan IT Industry Subcontracting Structure, SIer, SES, and the Multi-Layer Pyramid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abe-legal.jp/en/news/subcontract-act-reform-2026" rel="noopener noreferrer"&gt;Abe Legal: Japan Subcontract Act Reform 2026, Expanded Scope to Creative &amp;amp; Consulting Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dualbootpartners.com/insights/the-talent-pyramid/" rel="noopener noreferrer"&gt;Dual Boot Partners: The Talent Pyramid Is Crumbling, Why Traditional IT Services Can't Survive AI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Trying to figure out where your firm's real value sits in a compressed delivery chain? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>consulting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>AI Cost Optimization: A Business Guide to LLM API Budgeting</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 01 Sep 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/ai-cost-optimization-a-business-guide-to-llm-api-budgeting-gdl</link>
      <guid>https://dev.to/brywritescode/ai-cost-optimization-a-business-guide-to-llm-api-budgeting-gdl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Key Points&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM API costs double every six months for most growing organizations — model selection alone can reduce your bill by 50–80% without changing what you build.&lt;/li&gt;
&lt;li&gt;Prompt caching and response caching together eliminate the majority of repeated compute costs, saving 60–90% on input tokens for the right workloads.&lt;/li&gt;
&lt;li&gt;Batch processing cuts per-token prices by 50% for any task that doesn't require a real-time answer.&lt;/li&gt;
&lt;li&gt;Visibility comes first: you cannot optimize spend you cannot see — budget alerts, cost attribution by feature, and a 4-phase adoption roadmap make savings stick.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The AI Bill Nobody Expected
&lt;/h2&gt;

&lt;p&gt;The CFO opens the cloud invoice. Line 37: LLM API usage — $48,000. Last month it was $22,000. Three months ago it was $4,000.&lt;/p&gt;

&lt;p&gt;This is not a hypothetical. Enterprise LLM API spending doubled in under six months as teams moved from experiments to production workloads, and most organizations had no framework in place to manage the acceleration. Unlike compute or storage — where costs scale predictably with users — AI API costs are shaped by choices made at the code level: which model you pick, how you structure requests, whether you cache responses, and whether you batch non-urgent jobs. Those choices are invisible to finance and often undiscussed with engineering leadership.&lt;/p&gt;

&lt;p&gt;I've watched this play out across organizations at different scales — from startups where one engineer's prototype quietly became a $30K/month production workload, to larger teams where cost attribution was a year-long engineering project after the fact. This guide gives business leaders — CTOs, product managers, and anyone signing cloud invoices — a clear, non-technical framework for getting AI spend under control without sacrificing the capabilities your teams depend on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why AI Costs Spiral
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent across organizations of every size: costs spiral because of three compounding defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teams default to the most powerful model.&lt;/strong&gt; When engineers prototype an AI feature, they reach for the frontier model — it produces the best output, reduces debugging time, and avoids internal debates about quality. That default rarely gets revisited after launch. The same flagship model handling nuanced legal analysis ends up powering an FAQ bot that answers "What are your business hours?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody tracks token usage by feature.&lt;/strong&gt; Unlike server costs that map neatly to infrastructure, LLM costs are buried in a single API line item. Without attribution — which feature consumed how many tokens — there is no feedback loop. Expensive patterns persist indefinitely because nobody can see them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users trigger expensive chains without limits.&lt;/strong&gt; In production, one user action can silently trigger a cascade of API calls: an initial query, a summarization step, a classification call, a response synthesis step. Each hop multiplies the token count. Without circuit breakers or cost caps, a single power user can consume more than the entire intended monthly budget in a week. I've seen a five-step document analysis pipeline — perfectly reasonable in isolation — go uncapped in production and rack up more in a weekend than the team had budgeted for the entire month, because one user stress-tested it with 400-page PDFs.&lt;/p&gt;




&lt;h2&gt;
  
  
  How LLM Pricing Works
&lt;/h2&gt;

&lt;p&gt;AI APIs charge by the token — a chunk of text roughly equivalent to 0.75 words in English. A 100-word paragraph is approximately 130 tokens. Two separate charges apply to every request: input tokens (what you send to the model) and output tokens (what the model generates in response).&lt;/p&gt;

&lt;p&gt;Output tokens are significantly more expensive. Across all major providers, output pricing runs 4–5 times higher per token than input pricing. This matters because it means that asking a model to write a long report costs far more than asking it to classify a sentence — even if both requests contain the same amount of input text.&lt;/p&gt;

&lt;p&gt;Here is the current pricing landscape for the major models your engineering team is most likely using:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input $/1M tokens&lt;/th&gt;
&lt;th&gt;Output $/1M tokens&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.8&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;td&gt;200K&lt;/td&gt;
&lt;td&gt;Complex reasoning, multi-step analysis, legal/financial review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;td&gt;200K&lt;/td&gt;
&lt;td&gt;Balanced quality and cost; most production workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;200K&lt;/td&gt;
&lt;td&gt;High-volume, simple tasks: classification, extraction, FAQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;General-purpose; strong tool-use and structured output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 2.5 Pro&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;Very long documents; high-context summarization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Pricing as of June 2026. Verify current rates at official provider pricing pages before committing to budget projections.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Context window refers to the maximum amount of text a model can consider in a single request. Larger context windows are critical for processing lengthy contracts, codebases, or conversation histories — but they also increase token costs proportionally.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Lever: Model Selection
&lt;/h2&gt;

&lt;p&gt;If you take one action from this guide, make it this: match task complexity to model tier.&lt;/p&gt;

&lt;p&gt;The cost gap between tiers is not incremental — it is multiplicative. Using Claude Opus for a simple classification task costs roughly 20 times more per request than using Claude Haiku for the same task. At scale, that ratio becomes a six-figure annual line item for a mid-sized product.&lt;/p&gt;

&lt;p&gt;The business principle is straightforward: the model's capability should match the task's demand. Sending a routine FAQ to your most powerful model is the equivalent of deploying a senior partner to answer reception phone calls.&lt;/p&gt;

&lt;p&gt;Use this decision framework when evaluating which model tier a task belongs in:&lt;/p&gt;

&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%2Furrje63wqp1xuxhqkrm6.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%2Furrje63wqp1xuxhqkrm6.png" alt="Model Selection" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My recommendation: start the audit with your highest-volume features, not your most complex ones. FAQ responses, form classification, data extraction, and templated summaries almost always belong in the Haiku tier — and those tend to be your volume leaders. Complex document analysis, strategic synthesis, and nuanced content generation belong in the Sonnet or Opus tier, but they're rarely the source of the runaway bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prompt Caching: Pay Once, Reuse Many Times
&lt;/h2&gt;

&lt;p&gt;Every AI application sends instructions to the model with every request. A customer service bot might include a 2,000-word system prompt describing the company's products, policies, and tone guidelines — and that prompt gets sent, and charged, with every single user message.&lt;/p&gt;

&lt;p&gt;Prompt caching solves this. When you send the same instructions repeatedly, the provider stores that content in a temporary cache. Subsequent requests that use the same cached prefix are charged at 10% of the normal input price — a 90% discount on those tokens.&lt;/p&gt;

&lt;p&gt;For applications with long, stable system prompts — support bots, document processors, product assistants — prompt caching typically reduces input token costs by 60–90%. The cached content stays valid for a configurable duration (5 minutes or 1 hour on Anthropic's API) and is automatically refreshed when accessed.&lt;/p&gt;

&lt;p&gt;From a business perspective: if your engineering team is not using prompt caching on any application that includes a system prompt longer than a few hundred words, you are paying full price for tokens you have already paid for. In practice, this is the first thing I check when a team tells me their AI costs feel out of control — it is almost always uncached, and enabling it is usually a one-day engineering task that pays for itself within the first billing cycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Response Caching: Skip the API Call Entirely
&lt;/h2&gt;

&lt;p&gt;Prompt caching operates at the provider level and reduces the cost of repeated inputs. Response caching operates at your application level and eliminates the API call entirely for repeated questions.&lt;/p&gt;

&lt;p&gt;The concept is simple: when a user asks a question, your application checks a local database before calling the AI API. If the same question has been asked before and the answer is still valid, return the stored answer. No tokens consumed, no API cost, no latency.&lt;/p&gt;

&lt;p&gt;This approach works well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FAQ bots:&lt;/strong&gt; The same 200 questions account for 80% of support volume in most businesses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product descriptions:&lt;/strong&gt; Thousands of users view the same product content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templated reports:&lt;/strong&gt; Weekly summaries generated from the same data schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not work well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time data requests:&lt;/strong&gt; Questions about live inventory, current prices, or today's metrics require fresh API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-specific personalization:&lt;/strong&gt; Responses that incorporate individual user history or preferences cannot be safely reused across users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure investment is modest — a simple key-value store or database table — and the return on high-repetition workloads is significant. I use response caching as the first conversation to have with any team running a support bot or product FAQ: the ROI calculation is fast, the engineering effort is low, and approval is easy when you can show finance that 60% of calls will cost nothing after day three. On FAQ-heavy applications, organizations typically see 40–70% of requests served from cache after the first few days of production traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Batch Processing: Trade Speed for 50% Off
&lt;/h2&gt;

&lt;p&gt;Real-time API calls — where your application waits for an immediate response — carry a premium price. For tasks where a response is not needed within seconds, every major provider offers a batch processing API at 50% off standard pricing.&lt;/p&gt;

&lt;p&gt;Anthropic's Batch API and OpenAI's Batch API both accept large volumes of requests submitted at once and return results within hours, typically overnight. The same models, the same quality — at half the cost.&lt;/p&gt;

&lt;p&gt;Batch processing is well-suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overnight report generation&lt;/li&gt;
&lt;li&gt;Bulk document classification or extraction&lt;/li&gt;
&lt;li&gt;Weekly content summarization jobs&lt;/li&gt;
&lt;li&gt;Training data generation or quality review&lt;/li&gt;
&lt;li&gt;Large-scale sentiment analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key trade-off is latency. Batch jobs are asynchronous — you submit the work and retrieve results later. For any workflow where a user is waiting for a response, batch processing is not appropriate. For workflows that run on a schedule or process accumulated data, it is one of the simplest cost reductions available.&lt;/p&gt;

&lt;p&gt;A team spending $10,000 per month on overnight AI processing jobs that currently use the real-time API can reduce that line item to $5,000 with a single architectural change. If your organization runs any scheduled AI jobs — weekly digests, monthly classification sweeps, overnight data enrichment — batch processing should be a standing agenda item in your next budget review: the savings are predictable, the risk is low, and the approval case is straightforward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Budget Controls and Monitoring
&lt;/h2&gt;

&lt;p&gt;Cost optimization techniques only work if you can see whether they are working. Visibility is the prerequisite for everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set spend alerts in provider dashboards.&lt;/strong&gt; Both the Anthropic Console and the OpenAI usage dashboard allow you to configure email alerts when monthly spend crosses a threshold. Set alerts at 50%, 75%, and 100% of your planned monthly budget — not just at the limit. Early warning gives engineering time to investigate before costs become a crisis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implement soft limits in application code.&lt;/strong&gt; Provider-level alerts fire after costs have already accumulated. Application-level limits stop the accumulation. Work with your engineering team to implement per-user, per-feature, and per-workflow token budgets. When a workflow hits its limit, it either degrades gracefully (using a cheaper model) or queues the request for batch processing rather than failing loudly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track cost per feature, per user, per workflow.&lt;/strong&gt; A single API line item tells you nothing actionable. Attribution — which feature consumed which tokens — is what makes optimization possible. Organizations that implement cost attribution consistently report identifying two or three "cost sink" features that account for the majority of spend, often features that had never been considered high-cost during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adoption Roadmap: 4 Phases to Controlled AI Spend
&lt;/h2&gt;

&lt;p&gt;Cost optimization is not a one-time project. It is an ongoing practice. Organizations that achieve and sustain 50–80% cost reductions follow a consistent phased approach.&lt;/p&gt;

&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%2F85w15bir670g4utyh0zc.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%2F85w15bir670g4utyh0zc.png" alt="Adoption Roadmap" width="799" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Measure your baseline.&lt;/strong&gt; Before changing anything, establish what you are spending, by feature and model. This takes two to four weeks and requires engineering effort to add cost attribution to your existing AI calls. Without this data, you are optimizing blind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Identify expensive patterns.&lt;/strong&gt; With attribution in place, surface the top cost drivers. Typical findings: a high-volume feature using the wrong model tier, a long system prompt without caching, a batch-eligible workflow running in real-time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Apply targeted optimizations.&lt;/strong&gt; Address findings in order of impact. Model right-sizing is usually first — it requires minimal engineering effort and delivers immediate, compounding returns. Prompt caching is second. Response caching and batch processing follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Monitor and iterate.&lt;/strong&gt; New features introduce new cost patterns. Set a recurring monthly review cadence where engineering and finance align on spend vs. budget and flag new anomalies. The loop between Phase 4 and Phase 2 is what prevents costs from spiraling again after the initial optimization effort.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions to Ask Your Engineering Team
&lt;/h2&gt;

&lt;p&gt;Before your next budget review or AI project kickoff, bring these questions to your engineering leadership:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Which AI features are using which models?&lt;/strong&gt; Can you show me a list of every production AI feature and the model tier it currently runs on?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do we have cost attribution?&lt;/strong&gt; Can we see our monthly AI spend broken down by feature, workflow, or user segment — not just as a single total?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are we using prompt caching on any feature with a long system prompt?&lt;/strong&gt; If a feature sends the same instructions with every request and is not using prompt caching, what is the estimated monthly savings from enabling it?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Which AI workflows run in real-time that could run overnight?&lt;/strong&gt; Is there a list of report generation, bulk processing, or classification jobs that currently use the real-time API but don't need to?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do we have spend alerts configured?&lt;/strong&gt; At what thresholds do we receive notifications, and who receives them?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are there application-level rate limits or cost caps per user?&lt;/strong&gt; What prevents a single user or workflow from consuming an outsized share of our monthly token budget?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When did we last review whether each feature is on the right model tier?&lt;/strong&gt; Has any feature been moved from a flagship model to a lower-cost tier after initial development — or do we still run the same models we used during prototyping?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;AI API costs are not a fixed overhead — they are a function of architectural decisions your engineering team makes every day. The organizations I've seen get this right share one thing: they treated visibility as non-negotiable from the start, not as a cleanup project after the bill became alarming. Once you can see where the money goes, the optimizations follow naturally — and they tend to be faster and cheaper to implement than anyone expected. The goal is not to spend less on AI. It is to stop funding waste so the budget can go toward the AI capabilities that actually move your business forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic API Pricing — Official rates for Claude models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Anthropic Prompt Caching Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/batch-processing" rel="noopener noreferrer"&gt;Anthropic Batch API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;OpenAI API Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/pricing" rel="noopener noreferrer"&gt;Google Gemini API Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist. Managing AI infrastructure costs? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Billing Hours to Billing Outcomes: New Contract Structures for AI-Assisted SI Projects</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/from-billing-hours-to-billing-outcomes-new-contract-structures-for-ai-assisted-si-projects-2m53</link>
      <guid>https://dev.to/brywritescode/from-billing-hours-to-billing-outcomes-new-contract-structures-for-ai-assisted-si-projects-2m53</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Time-and-materials billing gives the vendor no financial incentive to finish faster. Every extra hour is another invoice line. AI-accelerated delivery is making that misalignment obvious to clients who can see the work speeding up while the invoice logic stays the same.&lt;/li&gt;
&lt;li&gt;73% of consulting clients already tell researchers they prefer value-based or outcome-driven pricing over hourly rates. That preference is likely to harden into the default contract structure across most of the systems-integration market within the next several years.&lt;/li&gt;
&lt;li&gt;Outcome-based contracts don't eliminate risk. They move it. Vendors absorb schedule risk on defined deliverables, which is forcing real changes to scoping discipline, delivery governance, and which projects an SI will even accept.&lt;/li&gt;
&lt;li&gt;Time-and-materials isn't disappearing. It survives, correctly, for genuinely exploratory work where the scope can't be defined upfront. The mistake to watch for is applying it to well-defined work out of habit, not because T&amp;amp;M is inherently wrong.&lt;/li&gt;
&lt;li&gt;IDC's forecast, 30% of IT services contracts outcome-based by 2029, is starting to look conservative. If AI-driven delivery speed keeps compounding, outcome-based and hybrid subscription-plus-usage structures could cover a majority of new systems-integration engagements within the decade, in markets where clients have any real pricing leverage.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medium.com/@brywritescode/why-the-man-month-is-dying-how-ai-broke-it-services-oldest-pricing-unit-d8121156c937" rel="noopener noreferrer"&gt;Why the Man-Month Is Dying: How AI Broke IT Services' Oldest Pricing Unit&lt;/a&gt; in this thread covered why the man-month is dying as a pricing unit. This one covers what's actually starting to get written into contracts once firms stop defaulting to it, because "bill outcomes, not hours" is a slogan, and slogans don't survive contact with a real statement of work.&lt;/p&gt;

&lt;p&gt;The economic case against pure time-and-materials was already documented before AI made it urgent. PMI's Pulse of the Profession data shows T&amp;amp;M engagements running 23% over budget on average, which on a $50,000 project is $11,500 of unplanned client spend, with no penalty to the vendor for the overrun. AI-assisted delivery hasn't fixed that structural misalignment. If anything, it's making it worse in the near term, because a vendor billing by the hour has an active disincentive to let AI tools cut delivery time, and clients increasingly can tell. Futurum Research already found 73% of consulting clients favor value-based or outcome-driven pricing over hourly rates, largely because AI's delivery-speed gains have made time-based billing look indefensible rather than merely inefficient.&lt;/p&gt;

&lt;p&gt;What's replacing it won't be a single template. Outcome-based contracts, payment tied to specific, measurable deliverables with acceptance criteria, are taking over the well-defined end of the market: data migrations, defined integrations, modernization projects with a clear "done" state. Hybrid structures, pairing a base subscription for fixed costs with a usage or outcome-based component above a baseline, are showing up in ongoing managed-service relationships. Time-and-materials should survive where it always made sense: genuinely exploratory work, early-stage product discovery, engagements where the client themselves doesn't yet know the final scope. The mistake to avoid over the next few years isn't choosing outcome-based pricing. It's applying whichever model a firm already knows how to bill, regardless of which one actually fits the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract Model Fit by Project Type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project Type&lt;/th&gt;
&lt;th&gt;Best-Fit Model&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Defined data migration or system integration&lt;/td&gt;
&lt;td&gt;Outcome-based&lt;/td&gt;
&lt;td&gt;Scope and success criteria are specifiable in advance; AI-accelerated delivery becomes vendor margin, not client discount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early-stage product discovery, undefined scope&lt;/td&gt;
&lt;td&gt;Time-and-materials&lt;/td&gt;
&lt;td&gt;Scope genuinely can't be fixed upfront; forcing outcome pricing here just relabels the guesswork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing managed services / maintenance&lt;/td&gt;
&lt;td&gt;Hybrid subscription + usage&lt;/td&gt;
&lt;td&gt;Base cost stays predictable for the client; usage component tracks real variability instead of headcount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed-scope modernization with a hard deadline&lt;/td&gt;
&lt;td&gt;Fixed-price with milestone acceptance&lt;/td&gt;
&lt;td&gt;Client needs cost certainty; vendor absorbs schedule risk in exchange for a defined, unchanging scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulatory-driven compliance rebuild&lt;/td&gt;
&lt;td&gt;Outcome-based, tied to audit-passable state&lt;/td&gt;
&lt;td&gt;Client cares about a certifiable end state, not hours logged getting there&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; match the contract model to how well-defined the scope actually is, not to which model your firm is most comfortable billing. An SI that only offers T&amp;amp;M today is telling well-defined-scope clients, correctly, that it hasn't done the scoping work outcome-based pricing requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Switch: A Phased Approach
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the measurement infrastructure before changing the invoice template.&lt;/strong&gt; Outcome-based billing requires a checkable definition of "done": acceptance criteria, a metering system for usage, or an audit standard. Firms that flip their contract language before building this lose money on undefined "outcomes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with your most well-understood, most frequently repeated project type.&lt;/strong&gt; A data migration you've delivered fifty times is far easier to price on outcome than a novel integration you've never scoped before. Don't lead the transition with your hardest, most novel work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renegotiate existing T&amp;amp;M relationships in the open, not by stealth.&lt;/strong&gt; Clients who discover a vendor quietly pocketing AI-driven speed gains under an unchanged T&amp;amp;M invoice react far worse than clients told directly that pricing is moving to outcomes and shown why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a genuine T&amp;amp;M option for genuinely exploratory engagements.&lt;/strong&gt; Retiring T&amp;amp;M entirely just pushes clients with undefined scope toward vendors willing to be honest that some work can't be priced by outcome yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revisit pricing on every engagement renewal, not just new business.&lt;/strong&gt; The real lesson behind IDC's forecast isn't the 30% number. It's that firms which wait for contract renewal cycles to force the pricing conversation, rather than initiating it, cede the framing to whichever competitor gets there first.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For our current book of business, how many engagements are still billed by the hour purely out of habit, on work that's actually well-defined enough to price on outcome?&lt;/li&gt;
&lt;li&gt;If a client asked us directly why our pricing model doesn't reward them for AI-driven delivery speed, do we have an honest answer, or an evasive one?&lt;/li&gt;
&lt;li&gt;Do we have real acceptance criteria and measurement infrastructure in place before we've committed to an outcome-based number, or are we guessing at "outcomes" the same way we used to guess at hours?&lt;/li&gt;
&lt;li&gt;Are we keeping time-and-materials available for the engagements that genuinely need it, or treating it as a legacy model to be phased out everywhere regardless of fit?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The contract structures most likely to survive the man-month's decline won't be chosen because they sound better in a sales deck. Outcome-based pricing is winning the well-defined end of the market because it aligns vendor incentive with AI-driven speed instead of punishing it. Time-and-materials should survive at the genuinely exploratory end because forcing a false "outcome" definition onto undefined scope just relabels the same uncertainty. The firms most likely to struggle through this transition aren't the ones that pick the wrong model. They're the ones that pick one model and apply it everywhere, regardless of whether the work in front of them actually fits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.wednesday.is/writing-articles/time-and-materials-vs-fixed-price-vs-outcome-based-contracts" rel="noopener noreferrer"&gt;Wednesday Solutions: Time and Materials vs Fixed Price vs Outcome-Based Contracts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://birdviewpsa.com/blog/outcome-based-contracts/" rel="noopener noreferrer"&gt;Birdview PSA: Outcome-Based Contracts, KPIs, Milestones, and Margin Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hig.com/news/it-services-in-the-age-of-agentic-ai-underwriting-through-a-structural-shift/" rel="noopener noreferrer"&gt;H.I.G. Capital: IT Services in the Age of Agentic AI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Deciding which of your engagements are actually ready for outcome-based pricing? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>consulting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Azure Event Grid: Push-Based Events Without the Kafka Overhead</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/azure-event-grid-push-based-events-without-the-kafka-overhead-1nhm</link>
      <guid>https://dev.to/brywritescode/azure-event-grid-push-based-events-without-the-kafka-overhead-1nhm</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Event Grid is a push-based event router, not a streaming log. Azure's actual Kafka-equivalent is Event Hubs — conflating the two leads teams to pick the wrong service for high-throughput streaming.&lt;/li&gt;
&lt;li&gt;Pricing is $0.60 per million operations after the first 100,000 free per month, and delivery is at-least-once, never exactly-once — every subscriber you build must be idempotent, full stop.&lt;/li&gt;
&lt;li&gt;A custom topic, a webhook subscription, and dead-letter configuration take four CLI commands — dead-lettering specifically is the one step almost every quickstart skips.&lt;/li&gt;
&lt;li&gt;Choose Event Grid when you need to react to discrete events (a blob uploaded, an order placed). Choose Event Hubs when you need to process a continuous stream and replay it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;az-cli 2.6x.x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An Azure resource group and a storage account for dead-letter destination (&lt;code&gt;az storage account create&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A publicly reachable HTTPS webhook endpoint to receive events — the examples use a placeholder Azure Function URL&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've had the same conversation with two different teams: "we need event-driven architecture on Azure, so we need Kafka." Neither actually needed Kafka. Both needed to react when something happened — a file landed in storage, an order got placed — not to process an unbounded stream of records with replay and consumer-group semantics. That's Event Grid's job, and it does it in far less setup than standing up Event Hubs or a managed Kafka cluster would require.&lt;/p&gt;

&lt;p&gt;The confusion is understandable. Azure has three services that all get called "event-driven": Event Grid, Event Hubs, and Service Bus. Only Event Hubs is the Kafka-equivalent. Event Grid is closer to a fan-out push notification system — publish an event, Event Grid delivers it via HTTPS webhook (or a handful of other supported destinations) to every matching subscriber, with retries and dead-lettering built in.&lt;/p&gt;

&lt;p&gt;This will explain how to build a real topic and subscription from the CLI, including the dead-letter setup nearly every quickstart skips, and draw the line clearly between what Event Grid is for and what it isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Event Grid Sits
&lt;/h2&gt;

&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%2Fw7brors1lwqv9py6f1yn.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%2Fw7brors1lwqv9py6f1yn.png" alt="Where Event Grid Sits" width="549" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: one topic, multiple independent subscriptions, each with its own delivery and dead-letter configuration.&lt;/em&gt;&lt;/p&gt;

&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%2Fv4kjmp9ppzoga2wufnxg.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%2Fv4kjmp9ppzoga2wufnxg.png" alt="Where Event Grid Sits 2" width="483" height="888"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Event Grid pushes discrete events to subscribers; Event Hubs is the pull-based, replayable stream for continuous data — they solve different problems.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Building It From the CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. A custom topic — the entry point events get published to.&lt;/span&gt;
az eventgrid topic create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; orders-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-eventgrid-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

&lt;span class="nv"&gt;TOPIC_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az eventgrid topic show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; orders-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-eventgrid-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;TOPIC_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az eventgrid topic show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; orders-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-eventgrid-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; endpoint &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;TOPIC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az eventgrid topic key list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; orders-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-eventgrid-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; key1 &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. A storage container for dead-lettered events — set this up before&lt;/span&gt;
&lt;span class="c"&gt;#    the subscription, not after something starts failing silently.&lt;/span&gt;
az storage container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; eventgrid-deadletter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account-name&lt;/span&gt; eventgriddemostorage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. A webhook subscription with retry policy and dead-letter destination.&lt;/span&gt;
az eventgrid event-subscription create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; order-notifications &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-resource-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint&lt;/span&gt; &lt;span class="s2"&gt;"https://order-processor.azurewebsites.net/api/handle-order-event"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-delivery-attempts&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-ttl&lt;/span&gt; 1440 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--deadletter-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resourceGroups/rg-eventgrid-demo/providers/Microsoft.Storage/storageAccounts/eventgriddemostorage/blobServices/default/containers/eventgrid-deadletter"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 4. Publish a test event to confirm the wiring.&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_ENDPOINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"aeg-sas-key: &lt;/span&gt;&lt;span class="nv"&gt;$TOPIC_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "id": "evt-001",
    "eventType": "OrderCreated",
    "subject": "orders/order-abc123",
    "eventTime": "2026-08-01T12:00:00Z",
    "data": { "orderId": "order-abc123", "status": "pending" },
    "dataVersion": "1.0"
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands, and unlike the EventBridge build in AWS EventBridge, publishing here goes over plain HTTPS with a SAS key header rather than through a dedicated CLI verb — Event Grid's publish path is the topic's own HTTPS endpoint, not an &lt;code&gt;az eventgrid publish&lt;/code&gt; command.&lt;/p&gt;




&lt;h2&gt;
  
  
  At-Least-Once Delivery Is Not Optional Reading
&lt;/h2&gt;

&lt;p&gt;Event Grid retries failed deliveries — up to &lt;code&gt;--max-delivery-attempts&lt;/code&gt; times, with exponential backoff, before giving up and routing to the dead-letter destination. That retry behavior means your webhook handler will, eventually, receive the same event more than once. Not as an edge case — as the documented contract.&lt;/p&gt;

&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%2Fh18lmg8fr26rtvqjod97.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%2Fh18lmg8fr26rtvqjod97.png" alt="At-Least-Once Delivery Is Not Optional Reading" width="688" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Event Grid's retry-then-dead-letter sequence. A handler that isn't idempotent will process the same logical event twice on any successful retry after a prior partial failure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Design the handler to key off &lt;code&gt;id&lt;/code&gt; (the event ID) and treat re-delivery of a seen ID as a no-op. This is the single most common production bug I've seen with Event Grid consumers — not a delivery failure, but a handler that wasn't written to expect its own retries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event Grid vs Event Hubs vs Service Bus
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Event Grid&lt;/th&gt;
&lt;th&gt;Event Hubs&lt;/th&gt;
&lt;th&gt;Service Bus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Push (webhook/fan-out)&lt;/td&gt;
&lt;td&gt;Pull (streaming log, consumer groups)&lt;/td&gt;
&lt;td&gt;Pull (queue/topic, message broker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka-equivalent&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (retention window)&lt;/td&gt;
&lt;td&gt;No (messages consumed once, unless using sessions/dead-letter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery guarantee&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;At-least-once (consumer-managed offsets)&lt;/td&gt;
&lt;td&gt;At-least-once, FIFO available with sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;React to discrete events (blob created, order placed)&lt;/td&gt;
&lt;td&gt;High-throughput continuous streams, analytics pipelines&lt;/td&gt;
&lt;td&gt;Ordered work queues, transactional messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;Per-operation ($0.60/M after free tier)&lt;/td&gt;
&lt;td&gt;Per-throughput-unit + per-million-events&lt;/td&gt;
&lt;td&gt;Per-tier flat + per-operation (Premium)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; If your use case is "when X happens, notify Y," that's Event Grid. If it's "process an unbounded stream and be able to replay from three days ago," that's Event Hubs. If it's "guarantee this work item gets processed exactly once, in order, by exactly one worker," that's Service Bus.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Reaching for Event Grid to replace Kafka&lt;/strong&gt;&lt;br&gt;
Event Grid has no replay and no consumer-group semantics. If a stakeholder says "we need Kafka on Azure," the answer is Event Hubs, not Event Grid — don't let the word "events" in both names cause a wrong pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Building webhook handlers that assume single delivery&lt;/strong&gt;&lt;br&gt;
At-least-once means exactly what it says. A handler that isn't idempotent against the event &lt;code&gt;id&lt;/code&gt; will eventually double-process something, usually during a transient network blip that has nothing to do with your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Skipping dead-letter configuration&lt;/strong&gt;&lt;br&gt;
Without &lt;code&gt;--deadletter-endpoint&lt;/code&gt;, an event that exhausts all delivery attempts is simply gone. No default fallback storage exists. Configure it at subscription creation, not after the first unexplained missing event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Setting &lt;code&gt;--event-ttl&lt;/code&gt; too short for slow downstream recovery&lt;/strong&gt;&lt;br&gt;
The default and commonly-used TTL is 1440 minutes (24 hours). If your webhook endpoint can plausibly be down longer than that during an incident, either extend the TTL or accept that events older than it are dead-lettered, and build your recovery runbook around checking that container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Event Grid's push model means latency is dominated by your subscriber's response time, not Event Grid itself — a slow webhook handler triggers Event Grid's own retry logic, which compounds load on an already-struggling endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Validate the &lt;code&gt;aeg-event-type&lt;/code&gt; header for the initial &lt;code&gt;SubscriptionValidation&lt;/code&gt; handshake event, and verify the event signature or use Azure AD-based authentication on the webhook endpoint rather than relying solely on the SAS key being unguessable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; At $0.60/million operations after 100K free per month, Event Grid is inexpensive even at meaningful scale — the cost driver worth watching is retry volume from a flaky subscriber, not the base event rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; Track &lt;code&gt;PublishSuccessCount&lt;/code&gt;, &lt;code&gt;DeliveryAttemptFailCount&lt;/code&gt;, and &lt;code&gt;DeadLetteredCount&lt;/code&gt; metrics per subscription. A rising &lt;code&gt;DeadLetteredCount&lt;/code&gt; with no corresponding alert is how "the event system is just quietly dropping stuff" becomes a support ticket weeks later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Idempotent Webhook Handler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="cm"&gt;/** Tracks processed event ids to make delivery idempotent — swap for Redis/DB in production. */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processedEventIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * POST /api/handle-order-event — Event Grid webhook target.
 *
 * @remarks
 * Handles the SubscriptionValidation handshake on first subscribe, then
 * processes OrderCreated events. Re-delivered events (same `id`) are
 * acknowledged with 200 but not reprocessed — required given Event Grid's
 * at-least-once delivery guarantee.
 *
 * @returns 200 on successful handling or validation; 400 on malformed payload.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/handle-order-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Microsoft.EventGrid.SubscriptionValidationEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validationCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;validationCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;validationCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;validationResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validationCode&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processedEventIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Already handled — Event Grid retried a prior delivery.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;processedEventIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processing order event &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with the teardown script: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/azure-event-grid-cli/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Event Grid is the right tool when you need to react to something that happened, delivered via push with retries and dead-lettering handled for you — not when you need a replayable, high-throughput stream, which is Event Hubs' job. Build every subscriber assuming at-least-once delivery from day one, configure dead-lettering before you need it rather than after an event goes missing, and the four-command CLI setup above will get you further, faster, than most teams expect from an "enterprise event routing" service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://azure.microsoft.com/en-us/pricing/details/event-grid/" rel="noopener noreferrer"&gt;Pricing – Event Grid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/cli/azure/eventgrid/event-subscription?view=azure-cli-latest" rel="noopener noreferrer"&gt;az eventgrid event-subscription — Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/event-grid/custom-event-quickstart" rel="noopener noreferrer"&gt;Quickstart: Send custom events with Event Grid and Azure CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.azure.cn/en-us/event-grid/overview" rel="noopener noreferrer"&gt;Introduction to Azure Event Grid&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Deciding between Event Grid, Event Hubs, and Service Bus for your next project? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>azure</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Japanese Enterprises Buy Differently: The RFP-to-Contract Cycle for Tech Vendors</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Thu, 20 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/why-japanese-enterprises-buy-differently-the-rfp-to-contract-cycle-for-tech-vendors-1pnm</link>
      <guid>https://dev.to/brywritescode/why-japanese-enterprises-buy-differently-the-rfp-to-contract-cycle-for-tech-vendors-1pnm</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Japanese enterprise sales cycles for foreign vendors without local presence run 6–18 months, against 3–6 months in the US — a structural difference, not a sign the deal is stalling.&lt;/li&gt;
&lt;li&gt;Two named mechanisms drive the length: nemawashi (informal, one-on-one consensus building before anything is formal) and ringi (the formal document that circulates for sign-off once consensus already exists).&lt;/li&gt;
&lt;li&gt;By the time a ringi-sho actually starts circulating, the real decision is usually already made — the formal approval stage is closer to ratification than to open deliberation.&lt;/li&gt;
&lt;li&gt;Pushing for a faster close during nemawashi doesn't speed up the deal. It signals you don't understand the process you're actually in, which is a worse outcome than a slow yes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A first-time-in-Japan sales lead I worked with was two months into a promising deal, had presented three times, and felt like the deal had gone quiet. His read: the champion had lost interest, or a competitor had swooped in. Neither was true. He was watching nemawashi happen and reading it as silence.&lt;/p&gt;

&lt;p&gt;Japan's enterprise buying process runs through two distinct, named phases most foreign vendors have never heard of by name even when they've lived through them: nemawashi, the informal groundwork that happens before anything is written down, and ringi, the formal document that circulates for sign-off once that groundwork is done. &lt;a href="https://medium.com/@brywritescode/japan-market-entry-for-cloud-saas-vendors-what-localization-actually-requires-beyond-translation-8fae855cc65b" rel="noopener noreferrer"&gt;Japan Market Entry for Cloud &amp;amp; SaaS Vendors&lt;/a&gt; in this series introduced why localization has to go deeper than translation to clear a Japanese buying process at all. This article is the buying process itself — what actually happens between your first meeting and a signed contract, and why the timeline that looks like inefficiency from the outside is closer to due diligence than delay.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nemawashi and Ringi: The Two Mechanisms
&lt;/h2&gt;

&lt;p&gt;Think of nemawashi like a gardener preparing a tree's root system for months before a transplant, which is literally where the word comes from ("going around the roots"). You don't just dig up the tree and move it; you prepare every root individually so the move, when it happens, doesn't shock the plant. Nemawashi is the same idea applied to a business decision: your champion has a private conversation with every stakeholder who'll eventually need to sign off, works through their specific objection, adjusts the proposal in response, and keeps going until everyone who matters has quietly signaled they won't block it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ringi&lt;/strong&gt; is what happens after that groundwork is done. A formal document, the ringi-sho, circulates through the stakeholders nemawashi already aligned, and each one adds their hanko as a recorded sign-off. The critical detail most foreign vendors miss: by the time the ringi-sho starts moving, the real decision was already made during nemawashi. The formal circulation is ratification of a consensus that already exists, not an open debate where your deal could still be won or lost.&lt;/p&gt;

&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%2Fvj0lc0tx1y1ssytfa16m.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%2Fvj0lc0tx1y1ssytfa16m.png" alt="Nemawashi and Ringi: The Two Mechanisms" width="784" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves:&lt;/strong&gt; without knowing these two phases exist, a foreign sales team reads the quiet nemawashi period as a stalled deal, pushes for a decision meeting to "move things forward," and in doing so signals to the champion that the vendor doesn't understand or respect the process — which is a specific kind of damage nemawashi, done well, is supposed to prevent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four-Phase Cycle in Practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Introduction and relationship building (1–3 months).&lt;/strong&gt; Initial meetings, meishi (business card) exchange, company overview presentations. Western sales instinct is to get to a needs-assessment conversation fast; in Japan, skipping straight past relationship-building reads as presumptuous, not efficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Needs assessment and technical evaluation (2–4 months).&lt;/strong&gt; Detailed requirements gathering across multiple departments, security and compliance review, reference checks against your existing Japanese customers if you have any (and a real handicap if you don't).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Nemawashi (2–4 months).&lt;/strong&gt; Your champion works the stakeholder list one conversation at a time. Questions come back to you filtered through the champion, not directly — a structural reason "just get us in a room with the decision-maker" doesn't work the way it might elsewhere. This is the quiet period that reads as radio silence to a vendor who doesn't know what it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Formal proposal, ringi, and contract (1–3 months).&lt;/strong&gt; Japanese-language pricing proposal, contract terms, hanko-sealed execution. By this point the deal is close to done; this phase is largely mechanical.&lt;/p&gt;

&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%2Fbyauym2bduzbctwfxvqm.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%2Fbyauym2bduzbctwfxvqm.png" alt="The Four-Phase Cycle in Practice" width="784" height="57"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add it up and a deal that would close in six months against a US buyer routinely runs 6–18 months against a Japanese enterprise without existing local presence — a JETRO-cited comparison puts US cycles at 3–6 months and Western Europe at 4–9 months against that same 6–18 month Japan range.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: Pushing the Timeline vs Working the Process vs Skipping the Groundwork
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Pushing for a Faster Close&lt;/th&gt;
&lt;th&gt;Working the Nemawashi Process&lt;/th&gt;
&lt;th&gt;Skipping Straight to Formal Proposal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Champion relationship&lt;/td&gt;
&lt;td&gt;Damaged — reads as impatience or disrespect for process&lt;/td&gt;
&lt;td&gt;Strengthened — champion sees you as easy to work with&lt;/td&gt;
&lt;td&gt;Champion put in an awkward position, unprepared stakeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual deal velocity&lt;/td&gt;
&lt;td&gt;Often slower — objections surface late and derail the ringi stage&lt;/td&gt;
&lt;td&gt;Fastest realistic path — objections resolved before they can block&lt;/td&gt;
&lt;td&gt;Frequently rejected outright at the ringi stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stakeholder buy-in&lt;/td&gt;
&lt;td&gt;Shallow — sign-offs given reluctantly, deal at risk post-signature&lt;/td&gt;
&lt;td&gt;Deep — sign-offs are ratifying a decision people already support&lt;/td&gt;
&lt;td&gt;Absent — stakeholders haven't had their concerns heard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Never — this consistently backfires in Japan specifically&lt;/td&gt;
&lt;td&gt;Any enterprise deal without an existing fast-track relationship&lt;/td&gt;
&lt;td&gt;Only viable for the rare vendor with deep, established local trust already&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; work the nemawashi process deliberately rather than trying to compress it. The vendors who push for speed lose more deals to a mishandled Phase 3 than they gain from the weeks they saved trying to skip it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages of Understanding This Process
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Higher close rates once you're in Phase 3:&lt;/strong&gt; a deal that reaches active nemawashi with a real champion has already cleared most of the risk — the objections get surfaced and handled before the formal stage, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable post-signature relationships:&lt;/strong&gt; because ringi ratifies genuine, not coerced, consensus, contracts signed this way tend to expand rather than churn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real moat against less patient competitors:&lt;/strong&gt; many foreign vendors give up or mishandle Phase 3, which is exactly why the vendors who work it properly face less competitive pressure at the point that matters most.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages and Risks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real opportunity cost:&lt;/strong&gt; 12–18 months is a long cash-flow assumption to build a go-to-market plan around, and a board expecting Western-market velocity needs to be told this explicitly, early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited visibility during Phase 3:&lt;/strong&gt; you genuinely don't get to see the internal conversation — you have to trust your champion is representing you well, which makes champion selection itself a critical, and easy to underweight, decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shortcut exists for a vendor without a champion:&lt;/strong&gt; if you can't identify and cultivate someone internally who'll do the nemawashi work on your behalf, the deal has a structural ceiling regardless of product quality.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Is This Right for You?
&lt;/h2&gt;

&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%2F4itu6idw2wjn06i4hyjf.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%2F4itu6idw2wjn06i4hyjf.png" alt="Is This Right for You?" width="784" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for the full cycle if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're entering without an existing Japan presence or reference customers.&lt;/li&gt;
&lt;li&gt;Your target account is a large enterprise with a multi-department approval structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You may see a faster cycle if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have a strong local reference customer in the same industry.&lt;/li&gt;
&lt;li&gt;Your champion has run this process for a similar tool before and can move faster because the stakeholder map is already known to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reconsider the target account if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have no path to an internal champion at all — no product, however good, closes a deal that has nobody running nemawashi on your behalf.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Realistic First Engagement
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify your champion early and invest in them specifically&lt;/strong&gt; — not just the person who took your first meeting, the person who'll actually walk the ringi-sho through stakeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare materials for nemawashi, not just for your pitch&lt;/strong&gt; — one-pagers your champion can hand to a stakeholder in a hallway conversation matter more here than your deck does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect and plan for the quiet period&lt;/strong&gt; — brief your own leadership in advance so a two-month silence in Phase 3 doesn't get misread internally as a dying deal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have Japanese-language, yen-denominated materials ready before Phase 4 starts&lt;/strong&gt;, not scrambled together once the ringi-sho is already circulating.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cost Considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Type&lt;/th&gt;
&lt;th&gt;What to Budget For&lt;/th&gt;
&lt;th&gt;Typical Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extended sales cycle carrying cost&lt;/td&gt;
&lt;td&gt;Sales team time and CAC amortized over 6–18 months instead of 3–6&lt;/td&gt;
&lt;td&gt;2–3x the carrying cost of an equivalent US deal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local champion cultivation&lt;/td&gt;
&lt;td&gt;Travel, relationship-building trips, in-person meetings (4–5 typical before pricing is even discussed)&lt;/td&gt;
&lt;td&gt;3–5 in-person visits over the cycle if not locally based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Materials localization for nemawashi&lt;/td&gt;
&lt;td&gt;One-pagers, technical briefs your champion can circulate informally&lt;/td&gt;
&lt;td&gt;$5,000–$15,000 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formal proposal and contract stage&lt;/td&gt;
&lt;td&gt;Japanese-language pricing proposal, dual-language contract&lt;/td&gt;
&lt;td&gt;Covered under &lt;a href="https://medium.com/@brywritescode/japan-market-entry-for-cloud-saas-vendors-what-localization-actually-requires-beyond-translation-8fae855cc65b" rel="noopener noreferrer"&gt;Japan Market Entry for Cloud &amp;amp; SaaS Vendors&lt;/a&gt; commercial localization estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ROI signal:&lt;/strong&gt; treat the extended cycle as a cost of entry, not a red flag mid-deal — a Japanese enterprise contract that clears ringi tends to expand and renew more predictably than an equivalent Western deal, because the consensus behind it is real, not a single champion's individual sign-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;"Do we have an identified internal champion, and have we invested time specifically in them, not just the stakeholder group broadly?"&lt;/li&gt;
&lt;li&gt;"Is our sales forecast modeling a 6–18 month cycle, or did we copy our US timeline into this account's projection?"&lt;/li&gt;
&lt;li&gt;"When the deal goes quiet, do we have a plan to check in appropriately, or will we default to pushing for a decision meeting?"&lt;/li&gt;
&lt;li&gt;"Are our materials ready for a champion to hand to a stakeholder informally, not just for a formal pitch meeting?"&lt;/li&gt;
&lt;li&gt;"Have we told our own leadership what nemawashi is, so a quiet Phase 3 doesn't get misread as a dying deal internally?"&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;The length of a Japanese enterprise sales cycle isn't inefficiency waiting to be optimized away — it's nemawashi and ringi doing exactly what they're designed to do: surface every objection before it can kill the deal, and turn a formal sign-off into ratification instead of a fresh vote. Plan your cash flow and your patience around 6–18 months, invest specifically in your champion, and resist the instinct to push for speed during the quiet period. The vendors who understand this close more deals, not fewer, than the ones treating Japan like a slower version of their home market.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://globis.eu/nemawashi-in-japanese-culture/" rel="noopener noreferrer"&gt;GLOBIS Europe — The Invisible Hand: How Nemawashi Shapes Every Decision in Japan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://understanding-japan.com/nemawashi-ringi-how-japanese-companies-decide/" rel="noopener noreferrer"&gt;Understanding Japan — Nemawashi &amp;amp; Ringi: How Japanese Companies Decide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetro.go.jp/en/invest/setting_up/" rel="noopener noreferrer"&gt;JETRO — Setting Up a Business in Japan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist, 15 years in IT, based in Tokyo. Navigating a long Japanese enterprise sales cycle right now? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Estimating a Project When AI Can Prototype the Whole Thing in a Day</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 19 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/estimating-a-project-when-ai-can-prototype-the-whole-thing-in-a-day-2c5m</link>
      <guid>https://dev.to/brywritescode/estimating-a-project-when-ai-can-prototype-the-whole-thing-in-a-day-2c5m</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Traditional estimation assumed the biggest unknown was implementation time. AI-assisted prototyping is collapsing that unknown for a large class of projects. A working, demoable version of "the whole thing" is increasingly a same-day artifact, not a milestone.&lt;/li&gt;
&lt;li&gt;This isn't making estimation easier. It's moving the hard part upstream, from "how long will this take to build" to "what does this client actually need, and which parts of a fast prototype are load-bearing versus disposable."&lt;/li&gt;
&lt;li&gt;Clients who watch a prototype appear in a day reasonably ask why the full engagement still takes months. An SI that can't answer that clearly, precisely, and early loses the deal before pricing even comes up.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I watched a prospective client's face change during a discovery call recently, the moment our team screen-shared a working prototype of their requested feature, built that morning, in response to the brief they'd sent the night before. The reaction wasn't delight. It was suspicion. If that took a morning, the client asked, what exactly is the six-week, six-figure proposal for?&lt;/p&gt;

&lt;p&gt;It's a fair question, and estimation as a discipline has to answer it honestly or lose credibility entirely. For most of the industry's history, the dominant uncertainty in a project estimate was implementation time: how long would it actually take to write, wire up, and test the thing being asked for. AI-assisted prototyping tools are attacking that uncertainty directly. What took three to five days of estimation and scaffolding work now often takes twenty minutes to produce a workable first pass, and a demoable version of an entire application can be a same-day artifact rather than a weeks-away milestone. That's not marginal. It's inverting which part of a project is actually hard to estimate.&lt;/p&gt;

&lt;p&gt;What isn't getting faster is figuring out what the client actually needs versus what they asked for, which requirements are genuinely load-bearing versus assumed, and which parts of a fast prototype are production-viable versus a convincing façade over unhandled edge cases, missing data governance, and untested failure modes. A prototype answers "can this exist." It says almost nothing about "should this exist in production, serving real traffic, under this client's compliance obligations." Conflating the two is the single most common estimation failure I'm seeing since prototyping got fast, and it burns trust exactly when trust is what the estimate needs most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old Estimation Bottleneck vs. Post-AI Estimation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Pre-AI Estimation&lt;/th&gt;
&lt;th&gt;Post-AI Estimation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dominant source of uncertainty&lt;/td&gt;
&lt;td&gt;Implementation time: how long to actually build it&lt;/td&gt;
&lt;td&gt;Requirements clarity: what the client actually needs versus what they described&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role of a prototype in the sales cycle&lt;/td&gt;
&lt;td&gt;Proof delivered weeks into the engagement, after signing&lt;/td&gt;
&lt;td&gt;Increasingly delivered same-day, before or during the pitch itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the client is really paying for&lt;/td&gt;
&lt;td&gt;Labor to produce a working system&lt;/td&gt;
&lt;td&gt;Judgment separating prototype-viable from production-viable, and scoping the gap between them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biggest estimation risk today&lt;/td&gt;
&lt;td&gt;Underestimating build time&lt;/td&gt;
&lt;td&gt;Underestimating the gap between "it demos" and "it survives production"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a fast prototype proves&lt;/td&gt;
&lt;td&gt;Very little on its own&lt;/td&gt;
&lt;td&gt;Real value, once paired with an honest gap analysis against production requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; stop treating a same-day prototype as evidence the whole engagement should be same-day priced. Treat it as the starting artifact for the estimate, and spend the actual estimation effort on the gap between what's on screen and what a production system serving that client's real constraints requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Fast-Prototype Estimation Process That Actually Holds Up
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the prototype first, in front of the client if possible.&lt;/strong&gt; It's the fastest way to surface misunderstood requirements. Clients correct a prototype far more precisely than they answer an abstract discovery question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate "demoed" from "load-bearing" explicitly, in writing.&lt;/strong&gt; For every feature the prototype shows working, state whether it's handling real data volumes, real failure modes, real auth, and real compliance requirements, or whether it's a scripted happy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price the gap, not the prototype.&lt;/strong&gt; The estimate should account for hardening, integration with existing systems, data migration, and the client's actual compliance and scale requirements: the parts a prototype is specifically designed to skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the client the gap analysis, not just the number.&lt;/strong&gt; A client who's seen a prototype built in a day will not accept a six-figure quote without an explanation. Show them exactly what's between "it demos" and "it ships," and the number stops looking arbitrary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revisit the estimate at the first production-scale test, not at the end.&lt;/strong&gt; AI-assisted prototypes routinely hide scale and integration problems that only surface once real data and real load hit the system. Build a checkpoint for that into the estimate itself, not as a change-order surprise.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When we show a client a fast prototype, do we also show them, explicitly, what's not yet handled, or do we let the demo imply more readiness than exists?&lt;/li&gt;
&lt;li&gt;Is our estimation effort actually going toward requirements clarity now, or are we still budgeting most of our estimation time for implementation guesswork that AI has already answered?&lt;/li&gt;
&lt;li&gt;Have we ever lost a client's trust because a fast prototype made our full-engagement timeline look inflated, and did we have a clear answer ready when they asked why?&lt;/li&gt;
&lt;li&gt;Do our estimates get revisited at a real production-scale checkpoint, or only at the end when a scale problem becomes an expensive surprise?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A same-day prototype is a genuinely useful artifact and a genuinely dangerous one, depending on what an SI does with it. Used as evidence that judgment is unnecessary, it wrecks client trust the moment the full quote lands. Used as the starting point for an honest, explicit gap analysis between what demos and what ships, it's the fastest way to have the requirements conversation that used to take weeks. The estimation skill that mattered a few years ago, guessing implementation time, is fading fast. The one replacing it, telling a client precisely what stands between a convincing demo and a production system they can bet their business on, is harder. It's also the one clients are increasingly paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devtimate.com/ai-project-estimation/" rel="noopener noreferrer"&gt;devtimate: How to Estimate a Software Project Step by Step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.taskade.com/blog/ai-project-estimation-tools" rel="noopener noreferrer"&gt;Taskade: 7 Best AI Project Estimation Tools in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rapidinnovation.io/service-development/ai-project-estimation-company" rel="noopener noreferrer"&gt;Rapid Innovation: AI Project Estimation Company&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Need help separating what a prototype proves from what a client needs in production? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>consulting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>API Gateway Patterns: AWS vs Azure vs GCP</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 18 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/api-gateway-patterns-aws-vs-azure-vs-gcp-4g1l</link>
      <guid>https://dev.to/brywritescode/api-gateway-patterns-aws-vs-azure-vs-gcp-4g1l</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS API Gateway&lt;/strong&gt; comes in two flavors: HTTP API (cheaper, faster, 90% of use cases) and REST API (full feature set, higher cost). Use HTTP API unless you need REST API's request/response transformation or private integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure API Management (APIM)&lt;/strong&gt; is the only managed gateway with a built-in developer portal and a policy engine powerful enough to handle enterprise API governance without custom code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP offers three overlapping products&lt;/strong&gt;: API Gateway (serverless, OpenAPI-driven), Cloud Endpoints (gRPC and self-managed), and Apigee (enterprise-grade). Don't pick randomly — they are not interchangeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common patterns&lt;/strong&gt; — BFF (Backend for Frontend), API aggregation, and protocol translation — are cloud-agnostic in design but differ significantly in implementation cost across providers.&lt;/li&gt;
&lt;li&gt;Cold starts on Lambda-backed gateways, APIM's XML policy syntax, and GCP's per-project quota model are the three gotchas that will cost you time in production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Picking an API gateway is a three-to-five year decision. Migrating between providers requires rewriting auth flows, rate-limiting config, and deployment pipelines — not just updating a URL. I've evaluated all three in production environments — migrating a client from AWS REST API to HTTP API, standing up APIM for an enterprise API program, and routing Cloud Run services through GCP API Gateway — and the differences that matter are rarely the ones in the marketing comparison tables. Most teams pick the gateway that matches their existing cloud provider without comparing what they're getting.&lt;/p&gt;

&lt;p&gt;That's often the right call. But it helps to know what you're trading. AWS, Azure, and GCP have different design philosophies: AWS optimizes for serverless-native integration, Azure optimizes for enterprise API governance, and GCP gives you a choice between a lightweight managed service and a full API platform.&lt;/p&gt;

&lt;p&gt;This article explains what each gateway does architecturally, compares them on the features that matter in production, covers the three patterns you'll implement on any of them, and flags the gotchas that each provider's documentation glosses over.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an API Gateway Does
&lt;/h2&gt;

&lt;p&gt;Before comparing providers, align on the job a gateway performs. An API gateway sits between clients and backend services and handles cross-cutting concerns so your services don't have to.&lt;/p&gt;

&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%2Ffay1pjl2t07w6hfyfygi.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%2Ffay1pjl2t07w6hfyfygi.png" alt="What an API Gateway Does" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Client traffic enters the gateway, which handles auth and routing before forwarding to backend services.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The five jobs a gateway performs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt; — match request paths to backend services, including path rewriting and load balancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and authorization&lt;/strong&gt; — validate JWT tokens, API keys, or OAuth flows before traffic reaches your service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — enforce per-client or per-route request quotas; protect backends from traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request/response transformation&lt;/strong&gt; — rewrite headers, translate between protocols (REST ↔ gRPC), strip or inject fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — emit access logs, request metrics, and distributed traces without instrumenting each service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every provider covers these five jobs. How they expose them — and at what cost — is what differentiates them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Request Flow Through a Gateway
&lt;/h2&gt;

&lt;p&gt;Before choosing a provider, understand the sequence every request traverses. The auth, rate-limit, and routing steps happen in this order regardless of which gateway you use.&lt;/p&gt;

&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%2Fo4ddfzxae8wqsn4c20uk.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%2Fo4ddfzxae8wqsn4c20uk.png" alt="Request Flow Through a Gateway" width="799" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Request flow through a gateway — auth and rate limiting happen before the backend ever receives the request.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AWS API Gateway
&lt;/h2&gt;

&lt;p&gt;AWS offers three API products under the API Gateway brand: &lt;strong&gt;HTTP API&lt;/strong&gt;, &lt;strong&gt;REST API&lt;/strong&gt;, and &lt;strong&gt;WebSocket API&lt;/strong&gt;. HTTP API and REST API are the ones backend engineers choose between daily.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP API vs REST API
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;HTTP API&lt;/th&gt;
&lt;th&gt;REST API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$1.00 / million requests&lt;/td&gt;
&lt;td&gt;$3.50 / million requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency overhead&lt;/td&gt;
&lt;td&gt;~6 ms&lt;/td&gt;
&lt;td&gt;~11 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT authorizer (native)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — Lambda authorizer required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request/response transforms&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (mapping templates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage plans / API keys&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private integrations (VPC Link)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;No — separate product&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS WAF integration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use HTTP API&lt;/strong&gt; for Lambda-backed services, JWT-authenticated endpoints, and any new API where you don't need request/response mapping templates. It costs 71% less than REST API and imposes half the latency overhead. I default to HTTP API for every new AWS project — the only time I've reached for REST API in the last two years was when a client needed usage plans for billing third-party API consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use REST API&lt;/strong&gt; when you need usage plans and API keys for third-party developer access, request/response transformation via mapping templates, or Cognito user pool authorizers without a Lambda function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda Integration
&lt;/h3&gt;

&lt;p&gt;The typical AWS pattern connects API Gateway directly to Lambda. The gateway acts as the event source; Lambda handles the business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AWS Lambda handler — receives API Gateway proxy event&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResultV2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResultV2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/errors/400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bad Request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId path parameter is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Business logic here&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HTTP API route configuration (AWS SAM / CloudFormation)&lt;/span&gt;
&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GetUserFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist/handler.handler&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs22.x&lt;/span&gt;
      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GetUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HttpApi&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&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;/users/{userId}&lt;/span&gt;
            &lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Authorizer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JWTAuthorizer&lt;/span&gt;

  &lt;span class="na"&gt;MyHttpApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::HttpApi&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Authorizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;JWTAuthorizer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;JwtConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
              &lt;span class="na"&gt;Issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://auth.example.com/"&lt;/span&gt;
            &lt;span class="na"&gt;IdentitySource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$request.header.Authorization"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Cold Start Gotcha
&lt;/h3&gt;

&lt;p&gt;Lambda-backed HTTP APIs incur cold start latency when a function instance is not warm. For a Node.js 22.x Lambda with a small bundle, cold starts add 200–800 ms. For a Java or .NET Lambda with a heavy runtime, expect 1–5 seconds.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable &lt;strong&gt;Provisioned Concurrency&lt;/strong&gt; on the Lambda — eliminates cold starts for pre-warmed instances, billed hourly even when idle.&lt;/li&gt;
&lt;li&gt;Keep Lambda bundle size under 5 MB (smaller = faster cold start).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;SnapStart&lt;/strong&gt; for Java Lambda functions (zero cold start after initial activation).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cold starts do not affect AWS ECS or EKS backends connected via VPC Link — they only affect Lambda integrations. I've seen this burn teams who built their entire p99 latency budget around gateway benchmarks, then went live and got paged at 3 AM because a burst of new connections spiked p99 to 2.8 seconds — the Lambda was a .NET 6 function with a 40 MB bundle and no Provisioned Concurrency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azure API Management (APIM)
&lt;/h2&gt;

&lt;p&gt;Azure APIM is architecturally different from AWS API Gateway. It is not a simple proxy — it is a full API management platform with three components:&lt;/p&gt;

&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%2Fuln3kes78xb5n5zodc8y.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%2Fuln3kes78xb5n5zodc8y.png" alt="Azure API Management (APIM)" width="799" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: APIM's three-component architecture — the gateway, the management plane, and the developer portal are separate concerns.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Policy Engine
&lt;/h3&gt;

&lt;p&gt;APIM's differentiating feature is its XML-based policy engine. Policies apply at four scopes: global, product, API, and operation. They execute in order: inbound → backend → outbound → on-error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- APIM policy: validate JWT, rate-limit, inject backend header --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Validate JWT from Authorization header --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;validate-jwt&lt;/span&gt; &lt;span class="na"&gt;header-name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;failed-validation-httpcode=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt;
                  &lt;span class="na"&gt;failed-validation-error-message=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;openid-config&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://auth.example.com/.well-known/openid-configuration"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;audiences&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;audience&amp;gt;&lt;/span&gt;https://api.example.com&lt;span class="nt"&gt;&amp;lt;/audience&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/audiences&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/validate-jwt&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Rate limit: 100 calls per 60 seconds per subscription key --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;rate-limit-by-key&lt;/span&gt; &lt;span class="na"&gt;calls=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;renewal-period=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt;
                       &lt;span class="na"&gt;counter-key=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription.Id)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Inject caller identity into backend header --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-User-Id"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.Request.Headers.GetValueOrDefault("Authorization","")
               .Replace("Bearer ",""))&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Strip internal headers before returning to client --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Internal-Trace-Id"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"delete"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy engine handles authentication, rate limiting, caching, transformation, and error handling — without custom code. For an enterprise that manages 50+ APIs across multiple teams, that centralization is the primary value proposition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer Portal
&lt;/h3&gt;

&lt;p&gt;APIM includes a fully customizable developer portal out of the box. Developers browse APIs, subscribe to products, generate API keys, and test endpoints — all without involving the API team. AWS and GCP's native gateways require third-party tooling (e.g., Backstage, Stoplight) to achieve the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing Tiers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price (approx.)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;$3.50 / million calls&lt;/td&gt;
&lt;td&gt;Pay-per-use. No SLA for developer portal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;~$50/month&lt;/td&gt;
&lt;td&gt;Dev/test only. No production SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;~$150/month&lt;/td&gt;
&lt;td&gt;Up to 1M calls/month included.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;~$750/month&lt;/td&gt;
&lt;td&gt;Full features, 99.9% SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;~$3,800+/month&lt;/td&gt;
&lt;td&gt;Multi-region, VNET integration, 99.95% SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Consumption tier looks attractive but lacks the developer portal SLA and several advanced policy features. Standard is the minimum for production workloads with enterprise requirements. In practice, I recommend clients skip Consumption entirely for anything customer-facing — the cost delta between Consumption and Standard is irrelevant next to the engineering hours you'll spend working around its limitations.&lt;/p&gt;




&lt;h2&gt;
  
  
  GCP: API Gateway, Cloud Endpoints, and Apigee
&lt;/h2&gt;

&lt;p&gt;GCP's three API products have overlapping names and confusingly similar descriptions. Choose the wrong one and you'll hit feature walls or pay for enterprise features you don't need.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;OpenAPI&lt;/th&gt;
&lt;th&gt;gRPC&lt;/th&gt;
&lt;th&gt;Developer portal&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless backends (Cloud Run, Functions)&lt;/td&gt;
&lt;td&gt;Yes (OAS v2)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Endpoints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gRPC services, self-managed gateway&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apigee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise, partner APIs, monetization&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use API Gateway&lt;/strong&gt; when your backend runs on Cloud Run, Cloud Functions, or App Engine and you want a fully managed gateway with no infrastructure to operate. Import your OpenAPI spec and Google provisions the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Cloud Endpoints&lt;/strong&gt; when your services speak gRPC, or when you need to host the gateway proxy (ESPv2) on your own runtime for private networking. Cloud Endpoints is also the right choice for local development with gRPC tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Apigee&lt;/strong&gt; when you need a developer portal, API monetization, advanced bot detection, or multi-cloud/hybrid deployment. Apigee is the only GCP option that competes directly with Azure APIM.&lt;/p&gt;

&lt;h3&gt;
  
  
  GCP API Gateway — OpenAPI-Driven Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# openapi.yaml — GCP API Gateway configuration&lt;/span&gt;
&lt;span class="na"&gt;swagger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0"&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User Service API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
&lt;span class="na"&gt;schemes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;span class="na"&gt;produces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="na"&gt;x-google-backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://user-service-xyz-uc.a.run.app&lt;/span&gt;
  &lt;span class="na"&gt;deadline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30.0&lt;/span&gt;

&lt;span class="na"&gt;securityDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;authorizationUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;implicit&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oauth2&lt;/span&gt;
    &lt;span class="na"&gt;x-google-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://securetoken.google.com/my-project"&lt;/span&gt;
    &lt;span class="na"&gt;x-google-jwks_uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"&lt;/span&gt;
    &lt;span class="na"&gt;x-google-audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-project"&lt;/span&gt;

&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;/users/{userId}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get user by ID&lt;/span&gt;
      &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getUser&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;userId&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User found&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;401"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unauthorized&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;404"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy to GCP API Gateway&lt;/span&gt;
gcloud api-gateway api-configs create user-service-v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--openapi-spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openapi.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backend-auth-service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api-gateway-sa@my-project.iam.gserviceaccount.com

gcloud api-gateway gateways create user-service-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service-v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The GCP Quota Gotcha
&lt;/h3&gt;

&lt;p&gt;GCP API Gateway enforces quotas at the project level. If you run multiple APIs or environments in the same GCP project, they share quota limits. A traffic spike on one API eats into the quota headroom for every other API in that project. I've seen this catch teams when a batch job saturated API quotas during an off-hours data migration, which cascaded into 429s on the customer-facing API living in the same project — a production incident that had nothing to do with the customer-facing service itself.&lt;/p&gt;

&lt;p&gt;Structure your GCP projects to isolate production APIs. One project per environment (dev, staging, prod) is the minimum. One project per service in production is safer for high-traffic APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;AWS HTTP API&lt;/th&gt;
&lt;th&gt;AWS REST API&lt;/th&gt;
&lt;th&gt;Azure APIM (Standard)&lt;/th&gt;
&lt;th&gt;GCP API Gateway&lt;/th&gt;
&lt;th&gt;GCP Apigee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: JWT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Lambda authorizer&lt;/td&gt;
&lt;td&gt;Policy (validate-jwt)&lt;/td&gt;
&lt;td&gt;OpenAPI extension&lt;/td&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: API keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: OAuth 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial (JWT)&lt;/td&gt;
&lt;td&gt;Lambda authorizer&lt;/td&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (usage plans)&lt;/td&gt;
&lt;td&gt;Yes (per subscription)&lt;/td&gt;
&lt;td&gt;Yes (per consumer)&lt;/td&gt;
&lt;td&gt;Yes (advanced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request transform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (mapping templates)&lt;/td&gt;
&lt;td&gt;Yes (policy engine)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Response transform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol translation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (REST ↔ SOAP)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer portal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (built-in)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gRPC support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Via Endpoints&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate product&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-region active-active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (regional)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Premium tier&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Tier (monthly)&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold starts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lambda-dependent&lt;/td&gt;
&lt;td&gt;Lambda-dependent&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Common Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend for Frontend (BFF)
&lt;/h3&gt;

&lt;p&gt;The BFF pattern uses a dedicated gateway layer per client type — one for the web app, one for the mobile app — to avoid forcing different clients to negotiate a single general-purpose API.&lt;/p&gt;

&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%2Fm9iuc8lwxhqcg66ccjba.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%2Fm9iuc8lwxhqcg66ccjba.png" alt="Backend for Frontend (BFF)" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: BFF pattern — each client type gets a dedicated aggregation layer tailored to its data needs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A BFF does data aggregation and transformation — it is not where business logic lives. Business rules belong in the downstream services.&lt;/p&gt;

&lt;p&gt;On AWS, implement each BFF as a separate Lambda function or ECS service behind its own HTTP API route. On Azure, use APIM products to route mobile vs web clients to different backends. On GCP, deploy separate Cloud Run services and separate API Gateway configurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Aggregation
&lt;/h3&gt;

&lt;p&gt;Aggregation collapses multiple downstream service calls into a single response for the client. The gateway (or a BFF) fans out the requests in parallel and merges the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API aggregation pattern — call multiple services in parallel&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * GET /orders/:id/summary — returns a merged view of order, inventory, and shipping data.
 *
 * @remarks
 * Fans out to three downstream services in parallel. If any service fails,
 * the endpoint returns 502 with the name of the failing service.
 *
 * @returns Merged order summary or 502 with failing service name.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventoryRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shippingRes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://order-service/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://inventory-service/stock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://shipping-service/shipments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventoryRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shippingRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`upstream_failure`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;service&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderSummary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inventoryRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shippingRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protocol Translation
&lt;/h3&gt;

&lt;p&gt;Protocol translation converts between API protocols at the gateway layer. The most common case in enterprise environments is REST-to-SOAP, where a new REST API sits in front of a legacy SOAP backend.&lt;/p&gt;

&lt;p&gt;Azure APIM handles this natively with the &lt;code&gt;soap-to-rest&lt;/code&gt; policy. On AWS or GCP, you write a Lambda or Cloud Run adapter that performs the translation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Azure APIM: expose a REST endpoint backed by a SOAP service --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Convert REST JSON body to SOAP XML envelope --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;
      @{
        var body = context.Request.Body.As&lt;span class="nt"&gt;&amp;lt;JObject&amp;gt;&lt;/span&gt;();
        return string.Format(
          @"&lt;span class="nt"&gt;&amp;lt;soapenv:Envelope&lt;/span&gt; &lt;span class="na"&gt;xmlns:soapenv=&lt;/span&gt;&lt;span class="s"&gt;'http://schemas.xmlsoap.org/soap/envelope/'&lt;/span&gt;
                              &lt;span class="na"&gt;xmlns:usr=&lt;/span&gt;&lt;span class="s"&gt;'http://legacy.example.com/user'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;soapenv:Body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;usr:GetUser&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;usr:UserId&amp;gt;&lt;/span&gt;{0}&lt;span class="nt"&gt;&amp;lt;/usr:UserId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/usr:GetUser&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/soapenv:Body&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/soapenv:Envelope&amp;gt;&lt;/span&gt;",
          body["userId"]
        );
      }
    &lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;text/xml; charset=utf-8&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"SOAPAction"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;http://legacy.example.com/user/GetUser&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Parse SOAP response and return JSON --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;xml-to-json&lt;/span&gt; &lt;span class="na"&gt;kind=&lt;/span&gt;&lt;span class="s"&gt;"direct"&lt;/span&gt; &lt;span class="na"&gt;apply=&lt;/span&gt;&lt;span class="s"&gt;"always"&lt;/span&gt; &lt;span class="na"&gt;consider-accept-header=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protocol translation is one of the strongest arguments for Azure APIM. Implementing the same pattern on AWS requires a Lambda adapter and custom XML parsing logic. If your organization has legacy SOAP services that need a REST facade, I would pick Azure APIM over any other provider specifically for this — the AWS alternative is a Lambda that becomes a maintenance liability the moment the original SOAP developer leaves.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Every gateway manages API versions moving through environments. The state diagram below represents the lifecycle that applies regardless of provider.&lt;/p&gt;

&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%2Fmchmulyiuz02egzpv1d2.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%2Fmchmulyiuz02egzpv1d2.png" alt="API Lifecycle Management" width="464" height="1568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: API lifecycle from development through retirement. Breaking changes are only safe before the API reaches Production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On AWS, implement environment promotion by deploying to separate API Gateway stages (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;). On Azure, use APIM's revision system for non-breaking changes and a new API version for breaking changes. On GCP API Gateway, deploy to separate gateway instances per environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right Gateway
&lt;/h2&gt;

&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%2Fpg7uq8ndlr168oro2zou.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%2Fpg7uq8ndlr168oro2zou.png" alt="Choosing the Right Gateway" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Decision flowchart for selecting a gateway.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Using AWS REST API when HTTP API covers your needs&lt;/strong&gt;&lt;br&gt;
REST API costs 3.5× more per million requests and adds ~5 ms of latency over HTTP API. The only reasons to choose REST API over HTTP API are mapping templates for request/response transformation, usage plans with API keys, or Cognito user pool authorizers. If none of those apply, HTTP API is the correct choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Ignoring Lambda cold starts in latency budgets&lt;/strong&gt;&lt;br&gt;
Teams measure gateway latency and see 6 ms. They assume their API responds in under 100 ms. Then they deploy to production and discover p99 latency is 700 ms because the Lambda behind the gateway cold-starts on every new burst. Cold starts are not a gateway problem — they are a Lambda problem — but they manifest as gateway latency and are often debugged at the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Choosing Azure APIM Consumption for production without reading the tier limitations&lt;/strong&gt;&lt;br&gt;
The Consumption tier has no SLA for the developer portal, limits on policy features, and no built-in cache. Teams select it for cost reasons and hit limitations mid-project. Standard is the correct production tier for most workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Mixing multiple APIs in a single GCP project&lt;/strong&gt;&lt;br&gt;
GCP API Gateway enforces quotas at the project level. Multiple APIs in one project share quota. A traffic spike on one API reduces the available quota for all others in the same project. Separate projects per environment (and per service in production for high-traffic APIs) is the safe default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Building complex API aggregation in gateway mapping templates&lt;/strong&gt;&lt;br&gt;
AWS REST API mapping templates (Velocity Template Language) and Azure APIM policies can both perform data transformation. Use them for simple field renaming or header injection. Do not use them to aggregate multiple backend calls, implement business logic, or transform complex nested structures — that complexity belongs in a BFF service where it is testable and maintainable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS HTTP API adds ~6 ms of median gateway overhead. REST API adds ~11 ms. Set your latency budget accordingly.&lt;/li&gt;
&lt;li&gt;Azure APIM Standard tier median overhead is 5–15 ms depending on policy complexity. A policy chain with JWT validation, rate limiting, and transformation adds up.&lt;/li&gt;
&lt;li&gt;GCP API Gateway overhead is 5–20 ms. Apigee overhead is higher due to the full policy engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On AWS, attach a WAF WebACL to your HTTP API or REST API. The gateway itself validates JWT signatures but does not inspect payloads for SQL injection or XSS patterns.&lt;/li&gt;
&lt;li&gt;On Azure APIM, use the &lt;code&gt;ip-filter&lt;/code&gt; policy to block known-bad IP ranges and the &lt;code&gt;validate-content&lt;/code&gt; policy for schema validation. APIM integrates with Azure DDoS Protection at the Premium tier.&lt;/li&gt;
&lt;li&gt;On GCP, pair API Gateway with Cloud Armor for DDoS protection and request filtering. Apigee has built-in bot detection and threat protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS HTTP API at 1 billion requests/month: ~$1,000. Lambda execution costs are separate.&lt;/li&gt;
&lt;li&gt;Azure APIM Standard: ~$750/month flat plus overage. Standard includes 1M calls/month; additional calls are $3.50/million.&lt;/li&gt;
&lt;li&gt;GCP API Gateway at 1 billion requests/month: ~$2,000 (calls over the free tier at $3.00/million above 2M).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On AWS, enable CloudWatch detailed metrics for your API Gateway and set alarms on &lt;code&gt;4XXError&lt;/code&gt;, &lt;code&gt;5XXError&lt;/code&gt;, &lt;code&gt;Latency&lt;/code&gt;, and &lt;code&gt;IntegrationLatency&lt;/code&gt;. &lt;code&gt;IntegrationLatency&lt;/code&gt; isolates backend latency from gateway latency — critical for cold start diagnosis.&lt;/li&gt;
&lt;li&gt;On Azure, use APIM's built-in Application Insights integration. Track &lt;code&gt;Backend Duration&lt;/code&gt; separately from &lt;code&gt;Gateway Duration&lt;/code&gt; to identify whether slowness is in the gateway policies or the backend.&lt;/li&gt;
&lt;li&gt;On GCP, Cloud Logging and Cloud Trace integrate automatically. Set a budget alert on API call costs in the GCP Console — GCP quotas can run up quickly on traffic spikes.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Full Example: API Aggregation Service (TypeScript)
&lt;/h2&gt;

&lt;p&gt;This aggregation service sits behind an API Gateway (on any provider) and fans out to three downstream services in parallel. It returns a merged response or reports which upstream failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Types ───────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="cm"&gt;/** Represents the shape of a downstream service response for an order. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lineItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Inventory availability per product. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;InventoryData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Shipment tracking details. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ShipmentData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trackingNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;estimatedDelivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Merged summary returned to the client. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderSummaryResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShipmentData&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** RFC 7807 problem detail shape for error responses. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProblemDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Helpers ─────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProblemDetail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://api.example.com/errors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Fetch a downstream service and parse the JSON response.
 *
 * @remarks Throws with a structured error if the upstream returns non-2xx.
 * Caller is responsible for catching via Promise.allSettled.
 *
 * @param url - Full URL of the downstream service endpoint.
 * @returns Parsed JSON response body.
 * @throws {Error} when upstream returns a non-2xx status.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Request-Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 5 s timeout per upstream call&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Upstream &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Routes ──────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * GET /orders/:id/summary — fan-out aggregation across order, inventory, and shipping services.
 *
 * @remarks
 * **Path parameters:**
 * - `id` — Order UUID. Must exist in the order service.
 *
 * **Responses:**
 * - `200 OK` — All three upstreams responded successfully.
 * - `404 Not Found` — Order service returned 404.
 * - `502 Bad Gateway` — One or more upstreams failed or timed out; body identifies which.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ORDER_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ORDER_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://order-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INV_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INVENTORY_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://inventory-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHIP_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHIPPING_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://shipping-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrderData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ORDER_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShipmentData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SHIP_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/shipments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upstream Failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Order service failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch inventory for each line item in parallel&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;INV_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/stock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventoryFailure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inventoryFailure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upstream Failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inventory service failed for one or more products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderSummaryResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Error handler ───────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unhandled error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal Server Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An unexpected error occurred.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Start ───────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Aggregation service running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;ORDER_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3001 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;INVENTORY_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3002 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;SHIPPING_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3003 &lt;span class="se"&gt;\&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the aggregation endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Aggregated response from three services in one call&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/orders/order-abc123/summary | jq

&lt;span class="c"&gt;# Expected shape:&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "requestId": "uuid",&lt;/span&gt;
&lt;span class="c"&gt;#   "order": { "id": "order-abc123", "status": "confirmed", ... },&lt;/span&gt;
&lt;span class="c"&gt;#   "inventory": [{ "productId": "...", "available": 10 }, ...],&lt;/span&gt;
&lt;span class="c"&gt;#   "shipment": { "trackingNumber": "...", "status": "in_transit" }&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with tests: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt; → &lt;code&gt;cloud-apis/api-gateway-patterns/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;AWS HTTP API is the right default for Lambda-backed services — it is faster and costs less than REST API, and covers 90% of use cases. Azure APIM wins when your organization needs a developer portal, enterprise API governance, or REST-to-SOAP translation without custom code. GCP's three options serve different tiers: API Gateway for serverless, Cloud Endpoints for gRPC, and Apigee for enterprise. The BFF pattern, API aggregation, and protocol translation are cloud-agnostic in concept but differ significantly in implementation cost — Azure APIM handles protocol translation natively, while AWS and GCP require adapter services. The decision you should walk away from this article having made: stop treating gateway selection as a checkbox in your cloud provider's onboarding wizard. The wrong choice costs you months of workarounds; the right one disappears into your infrastructure and you stop thinking about it entirely — which is exactly what a good gateway should do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" rel="noopener noreferrer"&gt;AWS API Gateway Developer Guide — HTTP API vs REST API&lt;/a&gt; — official AWS documentation on choosing between the two products&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/" rel="noopener noreferrer"&gt;Azure API Management documentation&lt;/a&gt; — official Microsoft docs covering policies, tiers, and the developer portal&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/blog/products/application-modernization/choosing-between-apigee-api-gateway-and-cloud-endpoints" rel="noopener noreferrer"&gt;Choosing between Apigee, API Gateway, and Cloud Endpoints — Google Cloud Blog&lt;/a&gt; — official Google guidance on selecting the right GCP product&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/api-gateway/docs" rel="noopener noreferrer"&gt;GCP API Gateway documentation&lt;/a&gt; — OpenAPI configuration, authentication, and quota management&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Evaluating or designing your API gateway architecture? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Story Points After AI: When Velocity Metrics Stop Meaning What They Used To</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/story-points-after-ai-when-velocity-metrics-stop-meaning-what-they-used-to-1jkf</link>
      <guid>https://dev.to/brywritescode/story-points-after-ai-when-velocity-metrics-stop-meaning-what-they-used-to-1jkf</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Story points and velocity were built on an assumption: that the effort to draft a solution roughly tracked the effort to deliver it. AI is breaking that link by collapsing drafting effort toward zero while leaving verification effort largely where it was.&lt;/li&gt;
&lt;li&gt;A team whose velocity jumps from 50 to 5,000 in a sprint hasn't gotten 100x better. It's produced a number that no longer measures the thing anyone cares about, and more engineering leaders are quietly admitting this every quarter.&lt;/li&gt;
&lt;li&gt;What's replacing velocity isn't a single metric. It's a shift toward outcome-oriented measures: defect density in AI-generated code, rework percentage, lead time to value, and issue resolution time, none of which can be inflated just by generating more draft code faster.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@brywritescode/why-the-man-month-is-dying-how-ai-broke-it-services-oldest-pricing-unit-d8121156c937" rel="noopener noreferrer"&gt;Why the Man-Month Is Dying: How AI Broke IT Services' Oldest Pricing Unit&lt;/a&gt; covered why the man-month is dying as a pricing unit. This one covers the parallel death of story points as a delivery-measurement unit. The two are breaking for the same underlying reason, on different sides of the same contract.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A client-side engineering director told me recently that her team's velocity chart looked like a hockey stick after they rolled out agentic coding tools, and that she'd stopped showing it to her own leadership because it made her look like either a liar or an idiot. Neither was true. Her team had actually improved. The chart just couldn't say by how much, or at what, because the number it produced no longer meant what it used to mean.&lt;/p&gt;

&lt;p&gt;Story points measure estimated effort, and velocity is story points delivered per sprint, a proxy that worked reasonably well as long as the bottleneck in software delivery was drafting: writing the code, wiring up the boilerplate, working through the mechanical parts of a ticket. AI-assisted development attacks exactly that bottleneck and leaves the other one, verification, largely untouched. When drafting effort collapses toward zero and verification effort doesn't move much, a velocity number built on the old ratio between the two stops correlating with anything real. Scrum.org's own framing of the problem is blunt: if a team's velocity jumps from 50 to 5,000 in a single sprint, they haven't gotten 100x better. They've broken the metric, because it was always a proxy for effort, and AI reduces the drafting effort to near-zero while leaving verification complexity high.&lt;/p&gt;

&lt;p&gt;The industry data backs up how fast this is becoming unignorable. Teams running agentic AI tools report 10-20x velocity gains alongside 40-78% fewer bugs, numbers that sound like unambiguous good news until you try to use them for the same planning and comparison purposes velocity was always used for. You cannot forecast a roadmap, compare two teams, or set a hiring plan against a metric that swings by two orders of magnitude depending on which AI tool got rolled out last quarter. Teams that haven't found a replacement are flying blind with better instruments than they've ever had, which is its own kind of dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old Velocity Metrics vs. Post-AI Delivery Metrics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Story Points / Velocity&lt;/th&gt;
&lt;th&gt;Post-AI Delivery Metrics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it actually measures&lt;/td&gt;
&lt;td&gt;Estimated drafting effort per sprint&lt;/td&gt;
&lt;td&gt;Defect density, rework rate, lead time to value, issue resolution time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inflatable by AI tooling alone&lt;/td&gt;
&lt;td&gt;Yes, trivially, without any real gain&lt;/td&gt;
&lt;td&gt;No, each metric requires an actual outcome to move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful for cross-team comparison&lt;/td&gt;
&lt;td&gt;Only if both teams use identical tooling and estimation habits&lt;/td&gt;
&lt;td&gt;Yes, outcomes are tooling-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful for client-facing reporting&lt;/td&gt;
&lt;td&gt;Increasingly not, since AI adoption accelerated in 2026&lt;/td&gt;
&lt;td&gt;Yes, clients can verify a defect rate or a resolution time against their own experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a spike in the number tells you&lt;/td&gt;
&lt;td&gt;Nothing reliable&lt;/td&gt;
&lt;td&gt;That verification-adjusted output actually improved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; if your organization is still running sprint retros around a velocity chart, check whether anyone on the team can explain what a 10x jump in that number would actually mean. If the honest answer is "the tooling changed," the metric isn't tracking delivery anymore. It's tracking tool adoption, and you should be measuring that separately from outcome quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Is Likely Headed
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Right now: denial and dashboards for most teams.&lt;/strong&gt; Most teams are keeping the velocity chart because the tooling to replace it (defect-density tracking, rework attribution, lead-time-to-value instrumentation) isn't built yet, and nobody wants to be first to admit their headline metric is cosmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect a credibility break within the next year or two.&lt;/strong&gt; As more teams get publicly embarrassed by an unexplainable 50-to-5,000 velocity swing, engineering leadership will stop defending the metric in front of executives and clients. "Agent efficiency" language is already starting to replace "velocity" in serious planning conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome metrics should go from experimental to standard within a few years.&lt;/strong&gt; Rework percentage and AI-attributed defect rate are becoming normal line items on engineering dashboards, not because they're perfect, but because they can't be gamed by pointing an agent at a backlog overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The end state: metrics become a client-facing artifact, not just an internal one.&lt;/strong&gt; Firms moving toward outcome-based contracts are starting to share rework and defect data directly with clients, because the metric is finally honest enough to survive that exposure.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If our velocity doubled next sprint, could anyone on this team explain exactly why, in terms a client would find credible?&lt;/li&gt;
&lt;li&gt;Are we tracking rework and defect rate on AI-generated code specifically, or lumping it in with everything else and losing the signal?&lt;/li&gt;
&lt;li&gt;Would we be comfortable sharing our current delivery metrics directly with a client, or are they really an internal-only number dressed up as evidence of progress?&lt;/li&gt;
&lt;li&gt;Is any part of a team's evaluation or bonus still tied to raw velocity, in a way that quietly rewards inflating a number everyone privately agrees is broken?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Story points and velocity aren't failing because someone calculated them wrong. They're failing because the thing they measured, effort, is no longer correlating with the thing everyone actually wanted to know, which was progress. The teams adapting fastest aren't the ones with the flashiest AI tooling. They're the ones willing to admit their dashboard is lying to them and replace it with metrics, defect density, rework rate, lead time to value, that can't be inflated by the same tools that broke the old ones. The man-month is dying on the pricing side of the contract. Velocity is dying on the delivery side. Neither death is really about AI being too good. It's about a proxy finally getting exposed as a proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.scrum.org/resources/blog/velocity-agent-efficiency-evidence-based-management-ai-era" rel="noopener noreferrer"&gt;Scrum.org: From Velocity to "Agent Efficiency", Evidence-Based Management for the AI Era&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oobeya.io/blog/engineering-metrics-in-the-ai-era" rel="noopener noreferrer"&gt;Oobeya: Engineering Metrics in the AI Era, A Complete Guide for 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitvelocity.dev/blog/measuring-engineering-ai-era" rel="noopener noreferrer"&gt;GitVelocity: AI Broke Your Engineering Metrics. Here's What Works Now&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Still reporting velocity to a client and not sure it means anything anymore? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>agile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>AWS EventBridge: Event Buses and Rules You Can Script from Day One</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/aws-eventbridge-event-buses-and-rules-you-can-script-from-day-one-106f</link>
      <guid>https://dev.to/brywritescode/aws-eventbridge-event-buses-and-rules-you-can-script-from-day-one-106f</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge charges $1.00 per million custom events published; rule evaluation itself is free, up to 300 rules per bus. The real billing surprise isn't rule count — it's payload size, since events bill in 64 KB chunks and the January 2026 limit increase to 1 MB means one large event can now bill as up to 16.&lt;/li&gt;
&lt;li&gt;A custom event bus, a rule with a JSON event pattern, and a Lambda target take four CLI commands total. Use &lt;code&gt;InputTransformer&lt;/code&gt; on targets to trim events down to only the fields a target needs, rather than forwarding the full body downstream.&lt;/li&gt;
&lt;li&gt;Archive and replay are cheap to enable and expensive to forget about — archived data bills monthly storage whether or not you ever replay it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;aws-cli/2.35.x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An IAM role with &lt;code&gt;events:*&lt;/code&gt; and &lt;code&gt;lambda:InvokeFunction&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;A deployed Lambda function to act as a target — the examples use &lt;code&gt;order-processor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;EventBridge gets pitched as "SNS but smarter" often enough that people underrate how little code it takes to get real value from it. I've stood up event-driven order processing for two different clients using nothing but a custom bus, a handful of pattern-matched rules, and existing Lambda functions — no message broker to run, no queue infrastructure to patch.&lt;/p&gt;

&lt;p&gt;The part that doesn't show up in the marketing is the cost mechanic that changed in January 2026: AWS raised the maximum event payload from 256 KB to 1 MB, which sounds like a pure win until you notice events still bill in 64 KB chunks. A single 1 MB event can now bill as 16 separate chunked events at the custom-event rate. That's not a reason to avoid EventBridge — it's a reason to design your event payloads deliberately instead of dumping an entire domain object into &lt;code&gt;detail&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article builds a working bus, rule, and target from the terminal, and treats the payload mechanic as a design input, not an afterthought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bus, Rules, Targets: The Three Pieces
&lt;/h2&gt;

&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%2F7br94gvx2akygpucqlo0.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%2F7br94gvx2akygpucqlo0.png" alt="Bus, Rules, Targets: The Three Pieces" width="784" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: one bus, multiple rules each matching a different event pattern, each routing to its own target.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A rule without a target does nothing but sit there for free. A rule with a matching event pattern and a wired-up target is what actually moves data. Rules and targets are separate API calls on purpose — one rule can fan out to up to five targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building It From the CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. A custom bus — separate from the default bus so your rules&lt;/span&gt;
&lt;span class="c"&gt;#    only ever see events your own services publish.&lt;/span&gt;
aws events create-event-bus &lt;span class="nt"&gt;--name&lt;/span&gt; orders-bus

&lt;span class="c"&gt;# 2. A rule matching a specific event pattern on that bus.&lt;/span&gt;
aws events put-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-pattern&lt;/span&gt; &lt;span class="s1"&gt;'{
    "source": ["orders-service"],
    "detail-type": ["OrderCreated"],
    "detail": {
      "status": ["pending"]
    }
  }'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Grant the rule permission to invoke the Lambda target.&lt;/span&gt;
aws lambda add-permission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; order-processor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statement-id&lt;/span&gt; eventbridge-invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; lambda:InvokeFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; events.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:events:us-east-1:123456789012:rule/orders-bus/order-created-rule"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Wire the rule to the Lambda target.&lt;/span&gt;
aws events put-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rule&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "Id": "order-processor-target",
    "Arn": "arn:aws:lambda:us-east-1:123456789012:function:order-processor"
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands, and the bus is live. Publish a test event to confirm the wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events put-events &lt;span class="nt"&gt;--entries&lt;/span&gt; &lt;span class="s1"&gt;'[{
  "Source": "orders-service",
  "DetailType": "OrderCreated",
  "Detail": "{\"orderId\":\"order-abc123\",\"status\":\"pending\",\"total\":49.99}",
  "EventBusName": "orders-bus"
}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response returns &lt;code&gt;"FailedEntryCount": 0&lt;/code&gt;. If it's &lt;code&gt;1&lt;/code&gt;, check &lt;code&gt;Errors&lt;/code&gt; in the same response — the two most common causes are a malformed &lt;code&gt;Detail&lt;/code&gt; JSON string or a &lt;code&gt;detail-type&lt;/code&gt;/&lt;code&gt;source&lt;/code&gt; combination that doesn't match any rule, which fails silently rather than erroring (the event is simply dropped with no matching rule, and EventBridge does not treat that as a failure).&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Around the Payload-Chunking Rule
&lt;/h2&gt;

&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%2Fp4wwnkw3c7c9x2y98vcf.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%2Fp4wwnkw3c7c9x2y98vcf.png" alt="Designing Around the Payload-Chunking Rule" width="494" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: EventBridge's per-64KB chunking mechanic, effective with the January 2026 payload limit increase to 1 MB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix isn't "never send large events." It's "don't send more than the target needs." Two concrete patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a reference, not the full object.&lt;/strong&gt; If &lt;code&gt;order-created-rule&lt;/code&gt;'s target only needs the order ID to go fetch full details itself, publish &lt;code&gt;{"orderId": "order-abc123"}&lt;/code&gt; in &lt;code&gt;detail&lt;/code&gt;, not the entire order document with line items, customer data, and shipping addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;InputTransformer&lt;/code&gt; to trim before delivery.&lt;/strong&gt; When the target genuinely needs specific fields from a larger event, extract them at the rule level instead of forwarding everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events put-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rule&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "Id": "order-processor-target",
    "Arn": "arn:aws:lambda:us-east-1:123456789012:function:order-processor",
    "InputTransformer": {
      "InputPathsMap": {
        "orderId": "$.detail.orderId",
        "status": "$.detail.status"
      },
      "InputTemplate": "{\"orderId\": &amp;lt;orderId&amp;gt;, \"status\": &amp;lt;status&amp;gt;}"
    }
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;InputTransformer&lt;/code&gt; runs on EventBridge's side before delivery — it doesn't reduce what you're billed for publishing the original event, but it does mean your Lambda isn't parsing (and your logs aren't storing) a payload larger than it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Archive and Replay
&lt;/h2&gt;

&lt;p&gt;Archiving lets you replay events later — useful for reprocessing after a bug fix, or reconstructing state after a downstream outage. It's cheap per operation and easy to forget about entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events create-archive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--archive-name&lt;/span&gt; orders-archive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-source-arn&lt;/span&gt; arn:aws:events:us-east-1:123456789012:event-bus/orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retention-days&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Archive processing&lt;/td&gt;
&lt;td&gt;$0.10/GB archived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive storage&lt;/td&gt;
&lt;td&gt;$0.023/GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replayed events&lt;/td&gt;
&lt;td&gt;$1.00/million (same as custom event rate)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Storage bills every month an archive exists, whether or not anything ever gets replayed from it. Set &lt;code&gt;--retention-days&lt;/code&gt; deliberately — 30 days is a reasonable default for operational replay; longer retention should be a conscious compliance or audit decision, not the default you forgot to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Publishing to the default bus for everything&lt;/strong&gt;&lt;br&gt;
The default bus receives AWS service events too. Mixing your application events into it makes rule patterns harder to write precisely and makes the bus noisier to reason about. Use a custom bus per domain area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Assuming a failed match throws an error&lt;/strong&gt;&lt;br&gt;
It doesn't. An event with no matching rule is dropped silently — &lt;code&gt;put-events&lt;/code&gt; still returns success. Test your event patterns against real sample events before relying on them in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Forwarding entire event payloads to every target&lt;/strong&gt;&lt;br&gt;
This is both a cost issue (larger payloads, more chunked billing on any downstream re-publish) and a coupling issue — targets that receive more than they need tend to accumulate implicit dependencies on fields nobody documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Enabling archive without a retention policy decision&lt;/strong&gt;&lt;br&gt;
Storage costs accrue every month regardless of replay activity. An archive with no retention limit and no owner is exactly the kind of AWS bill line item nobody can explain six months later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Rule matching is fast and free regardless of rule count up to the 300-per-bus limit. If you're approaching that limit, it's usually a sign you need multiple buses segmented by domain, not one bus straining to model everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Use resource-based policies on the event bus (&lt;code&gt;put-permission&lt;/code&gt;) to control which accounts or services can publish, rather than relying solely on IAM policies at the producer side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Re-run the payload math whenever a producer's event shape grows. A field added "just in case" on a high-volume event source compounds fast at the chunked billing rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; CloudWatch metrics on &lt;code&gt;TriggeredRules&lt;/code&gt;, &lt;code&gt;FailedInvocations&lt;/code&gt;, and &lt;code&gt;ThrottledRules&lt;/code&gt; are free and on by default — set an alarm on &lt;code&gt;FailedInvocations&lt;/code&gt; per target, since a misconfigured target fails silently from the publisher's perspective.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Teardown Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BUS_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUS_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;-bus&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;RULE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RULE_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;order&lt;/span&gt;&lt;span class="p"&gt;-created-rule&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

aws events remove-targets &lt;span class="nt"&gt;--rule&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RULE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--ids&lt;/span&gt; order-processor-target
aws events delete-rule &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RULE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
aws events delete-archive &lt;span class="nt"&gt;--archive-name&lt;/span&gt; orders-archive &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;aws events delete-event-bus &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Torn down &lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt; and its rules."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source including the sample Lambda target and pattern-matching tests: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/aws-eventbridge-cli/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;EventBridge is one of the fastest AWS services to script from zero — a working bus, rule, and target is four commands. The part worth designing around deliberately is payload size: with the 2026 increase to a 1 MB event limit, it's easy to publish something large enough to bill as multiple events without noticing. Send references and trim payloads with &lt;code&gt;InputTransformer&lt;/code&gt; rather than forwarding full objects downstream, and treat archive retention as a decision you make on purpose, not a default you forgot existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;Amazon EventBridge pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/events/" rel="noopener noreferrer"&gt;events — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;Event bus targets in Amazon EventBridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://repost.aws/knowledge-center/eventbridge-reduce-charges" rel="noopener noreferrer"&gt;Understand EventBridge charges and reduce future charges — AWS re:Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Designing an event-driven architecture on AWS? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
