<?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: Stéphane Derosiaux</title>
    <description>The latest articles on DEV Community by Stéphane Derosiaux (@sderosiaux).</description>
    <link>https://dev.to/sderosiaux</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%2F1137771%2F8ec57b19-4d29-4f61-9e8c-e5d27f821c04.jpg</url>
      <title>DEV Community: Stéphane Derosiaux</title>
      <link>https://dev.to/sderosiaux</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sderosiaux"/>
    <language>en</language>
    <item>
      <title>Chrome Shrunk a 30,000 Token Web Page Into 300 Tokens.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Tue, 21 Jul 2026 18:52:07 +0000</pubDate>
      <link>https://dev.to/sderosiaux/chrome-agent-turn-any-llm-into-a-smart-web-browsing-agent-5gcj</link>
      <guid>https://dev.to/sderosiaux/chrome-agent-turn-any-llm-into-a-smart-web-browsing-agent-5gcj</guid>
      <description>&lt;p&gt;Ever handed an LLM a full web page and watched the amount of tokens being used?&lt;/p&gt;

&lt;p&gt;A single product listing is 20-30K tokens of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup before the model finds what it needs: wrapper divs, css class, SVG, JSON blobs etc. The agent needs maybe 300 tokens of that (the items and their prices). When you feed an LLM with the raw DOM, you pay for markup cleanup.&lt;/p&gt;

&lt;p&gt;This is why chrome-agent exists. It's one 3 MB binary that drives Chrome over the DevTools Protocol (CDP) and hands your agent the page in a shape a language model can cheaply read and act on. No MCP server, no Node runtime.&lt;/p&gt;

&lt;p&gt;For instance, ask an agent to: &lt;em&gt;find standing desks under $400 on a store, return the top five with prices&lt;/em&gt;. A boring job. Use a default agentic workflow with basic "WebSearch" and you're cooked.&lt;/p&gt;

&lt;p&gt;Let me show you why chrome-agent is the best to do the job here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Land on the page and look
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent goto &lt;span class="s2"&gt;"https://store.example.com/search?q=standing+desk"&lt;/span&gt; &lt;span class="nt"&gt;--inspect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;goto&lt;/code&gt; navigates. &lt;code&gt;--inspect&lt;/code&gt; grabs the useful content (no div soup), so the agent gets useful minimum content in one round trip. What comes back is &lt;em&gt;not&lt;/em&gt; HTML. It's the accessibility tree (the same roles-and-labels view a screen reader gets) flattened to compact text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uid=n1   heading "Standing Desks (48 results)"
uid=n14  searchbox "Search products"
uid=n22  button "Apply filters"
uid=n41  link "Uplift V2 Standing Desk"
uid=n47  link "Flexispot E7"
uid=n52  link "Vari Electric"
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's gone: no divs, no class names, no base64 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. useless HTML nodes are stripped, roughly a 70% cut against the raw accessibility tree (which is a 99% over raw HTML).&lt;/p&gt;

&lt;p&gt;chrome-agent has also magic command such as &lt;code&gt;inspect&lt;/code&gt; to extract deterministically what makes on the page, based on Reader modes from browser. It can returns like 50 tokens, against the 20-30K of raw HTML the same page would otherwise cost the model. That's insane!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A page becomes something the model can read in tokens it can afford, and act on by pointing at a uid.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why UID and not CSS selector? Because CSS selectors suck and cost way more tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why uids instead of CSS selectors?
&lt;/h2&gt;

&lt;p&gt;Because the agent needs a stable handle it can point at without inventing CSS. Those &lt;code&gt;n41&lt;/code&gt;, &lt;code&gt;n47&lt;/code&gt; values are Chrome's own internal IDs. They're stable across repeated inspects &lt;em&gt;on the same page&lt;/em&gt;, so the agent can inspect, reason, then act on &lt;code&gt;n47&lt;/code&gt; a few commands later and still hit the same element.&lt;/p&gt;

&lt;p&gt;chrome-agent has multiple targeting modes that the agent picks per situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uid&lt;/code&gt; (from inspect) =&amp;gt; the default, cheap, specific&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--selector "css"&lt;/code&gt; =&amp;gt; when you already know the DOM shape&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--xy 100,200&lt;/code&gt; =&amp;gt; canvas, maps, anything with no real DOM node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our example, the agent doesn't need every link, it needs the price control. So it narrows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent inspect &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"textbox,button"&lt;/span&gt; &lt;span class="nt"&gt;--urls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--filter&lt;/code&gt; keeps only the roles you name (with aliases, so &lt;code&gt;textbox&lt;/code&gt; also pulls in searchbox and combobox). Now the tree is a handful of lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uid=n14  searchbox "Search products"
uid=n62  textbox "Max price"
uid=n22  button "Apply filters"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's &lt;code&gt;n62&lt;/code&gt;. The agent saw it, so it can point at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act, then wait for the grid to settle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent fill &lt;span class="s2"&gt;"400"&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; n62
chrome-agent click n22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fill&lt;/code&gt; types into the max-price field, &lt;code&gt;click&lt;/code&gt; applies the filter. The full action set is there when the task needs it: &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;dblclick&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt; (matches by option value then visible text), &lt;code&gt;check&lt;/code&gt;/&lt;code&gt;uncheck&lt;/code&gt; (idempotent, so re-running a step never toggles you into the wrong state), &lt;code&gt;upload&lt;/code&gt;, &lt;code&gt;drag&lt;/code&gt;, and &lt;code&gt;press&lt;/code&gt; for raw keys. &lt;/p&gt;

&lt;p&gt;You have a SPA so there's no real navigation and the DOM keeps being replaced? That's also fine! Clicking the filter fires an XHR and re-renders the grid client-side. chrome-agent waits on the network, no need of pseudo-sleep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent &lt;span class="nb"&gt;wait &lt;/span&gt;network-idle &lt;span class="nt"&gt;--idle-ms&lt;/span&gt; 500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tracks in-flight requests and returns once the network has been quiet for 500ms (plenty enough to ensure all the Javascript calls are done).&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the data out is a decision, not a command
&lt;/h2&gt;

&lt;p&gt;The filtered grid is on screen and the agent needs the top five as structured data. There's a hierarchy, and the skill is matching the rung to the &lt;em&gt;shape of the data&lt;/em&gt;, not reaching for the same command every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;read&lt;/code&gt;&lt;/strong&gt; =&amp;gt; one article or long body of prose. Injects Mozilla's Readability, turns ~15K of HTML into ~500 clean tokens. Wrong tool here (this is a grid, not an essay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;extract&lt;/code&gt;&lt;/strong&gt; =&amp;gt; repeating records: product grids, search results, feeds, tables. MDR/DEPTA-style heuristics (sibling structural similarity, text-to-link ratio, hidden-element exclusion) find the repeating region on their own. This is our rung.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text --selector "main"&lt;/code&gt;&lt;/strong&gt; =&amp;gt; scoped visible copy when you just want words from one region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eval "..."&lt;/code&gt;&lt;/strong&gt; =&amp;gt; a single computed value the heuristics would miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;network&lt;/code&gt;&lt;/strong&gt; =&amp;gt; when the grid was painted from a JSON API, skip the DOM and read the response that fed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this task, &lt;code&gt;extract&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent extract &lt;span class="nt"&gt;--limit&lt;/span&gt; 5 &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"records"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Branch Duo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$349"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/p/branch-duo"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flexispot E7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$389"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/p/flexispot-e7"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vari Electric"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$395"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/p/vari-electric"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the store lazy-loads rows on scroll, &lt;code&gt;extract --scroll&lt;/code&gt; drives the page down and watches a &lt;code&gt;MutationObserver&lt;/code&gt; until new content lands.&lt;/p&gt;

&lt;p&gt;Sometimes the cheapest rung isn't the page at all. That grid was populated by a JSON call. The agent can grab it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent network &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"/api/search"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same prices, no scraping. &lt;/p&gt;

&lt;h2&gt;
  
  
  When it breaks, the error tells the agent what to try
&lt;/h2&gt;

&lt;p&gt;Agents fail constantly. What decides whether they recover is what a failure hands back. In &lt;code&gt;--json&lt;/code&gt; mode an error exits 1 but still prints a parseable object on stdout, and when there's an obvious next step it comes back with a &lt;code&gt;hint&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No element with uid=n47"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page may have changed. Re-run inspect to get current uids."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads &lt;code&gt;ok:false&lt;/code&gt;, reads the hint, re-inspects, retries. Self-healing without you hand-writing recovery logic for every command.&lt;/p&gt;

&lt;h2&gt;
  
  
  One connection instead of a hundred spawns
&lt;/h2&gt;

&lt;p&gt;Running the binary once per command pays process startup and a fresh Chrome connection every time. For a multi-step task that adds up. So there's pipe mode: one persistent connection, JSON commands in on stdin, JSON results out on stdout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chrome-agent pipe &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{"cmd":"goto","url":"https://store.example.com/search?q=standing+desk","inspect":true}
{"cmd":"fill","selector":"input[name=maxPrice]","value":"400"}
{"cmd":"click","selector":"button[type=submit]"}
{"cmd":"wait","what":"network-idle","idle_ms":500}
{"cmd":"extract","limit":5}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Playwright already exists. When do you reach for this?
&lt;/h2&gt;

&lt;p&gt;Different use-cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright / Puppeteer&lt;/strong&gt; =&amp;gt; deterministic end-to-end tests: assertions, fixtures, waiting on specific selectors, visual diffing. A full, mature automation API for humans writing test suites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chrome-agent&lt;/strong&gt; =&amp;gt; an LLM agent that has to read and act on pages it's never seen, in tokens it can afford, and recover from failure on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different jobs. If you're writing an assertion-heavy CI suite, use Playwright. If you do web researches and your agents need to browser the web, they are burning their context window (parsing HTML soup) and you are paying for it. Don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even more magic!
&lt;/h2&gt;

&lt;p&gt;chrome-agent supports even more things an LLM agent needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it can &lt;code&gt;drag&lt;/code&gt; elements&lt;/li&gt;
&lt;li&gt;it can &lt;code&gt;download&lt;/code&gt; elements&lt;/li&gt;
&lt;li&gt;it can handle &lt;code&gt;iframe&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;it can &lt;code&gt;--stealth&lt;/code&gt; to apply patches to bypass Cloudflare and Turnstile. It is &lt;strong&gt;not&lt;/strong&gt; guaranteed against DataDome or Kasada though; they fingerprint the bundled Chromium and ship detection updates constantly.&lt;/li&gt;
&lt;li&gt;it can access your logged-in site by using your real user cookies using &lt;code&gt;--copy-cookies&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Go with the skill to install and forget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add sderosiaux/chrome-agent
&lt;span class="c"&gt;# or&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;chrome-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ai</category>
      <category>rust</category>
      <category>showdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>Kafka Doesn't Know Who Owns Your Topics.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 20 Jul 2026 18:23:21 +0000</pubDate>
      <link>https://dev.to/sderosiaux/kafka-as-code-beyond-topics-3mbc</link>
      <guid>https://dev.to/sderosiaux/kafka-as-code-beyond-topics-3mbc</guid>
      <description>&lt;p&gt;Someone on your team asked "which Terraform provider do we use for Kafka?". Your answer is probably between Mongey/kafka and your vendor's provider like Aiven, MSK or Confluent.&lt;/p&gt;

&lt;p&gt;Good, but the question can actually be broader. There are three separate layers hiding in there:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating the cluster (brokers, VPC, MSK Serverless)&lt;/li&gt;
&lt;li&gt;Creating the topics (partitions, retention, ACLs)&lt;/li&gt;
&lt;li&gt;Governing the platform (who owns what, which rules apply, what a team can ship without asking)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both 1 and 2 have had providers for years. Layer 3 is just getting started with solutions like Conduktor. Let me show you why you should care and why it exists now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kafka protocol has no idea who owns what
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No provider that speaks the Kafka protocol can express ownership, because the protocol has no concept of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A topic in the Kafka protocol is a name, a partition count, a replication factor, and a config map. That's it. There is nowhere to put "the payments team owns this", "this contains PII," or "C1 criticality". Not because providers are lazy but because there's no field.&lt;/p&gt;

&lt;p&gt;So what happens instead? You build a central Terraform repo. The platform team owns it. Every topic request becomes a PR into a codebase the requester has never seen. And then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"They'll go in and have to make a PR to that repo. It's not super intuitive on how to do that. So they'll end up pinging our team anyway."&lt;/em&gt; — platform engineer at a consumer-lending company&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Self-service that routes through a human is just a ticket queue with extra YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two other things that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your CI becomes the most privileged Kafka client in the company.&lt;/strong&gt; A provider that dials brokers needs admin credentials and a network route to every broker of every cluster, with TLS/SASL/IAM wired up in the runner. Every new cluster is a new networking problem &lt;em&gt;and&lt;/em&gt; a new credentials problem in your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform will happily delete your data.&lt;/strong&gt; AWS's own &lt;a href="https://aws.amazon.com/blogs/big-data/automate-topic-provisioning-and-configuration-using-terraform-with-amazon-msk/" rel="noopener noreferrer"&gt;MSK Terraform tutorial&lt;/a&gt; walks you through changing a topic from 50 partitions to 10. Kafka can't shrink partitions. So Terraform destroys and recreates the topic, data included. The plan says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 1 to add, 1 to change, 1 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is technically accurate and completely fails to communicate "you are about to lose weeks of events." and make your consumers very unhappy. (without mentioning business impact)&lt;/p&gt;

&lt;p&gt;Notice the trend AWS itself is following: the newer &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/msk_topic" rel="noopener noreferrer"&gt;&lt;code&gt;aws_msk_topic&lt;/code&gt;&lt;/a&gt; resource takes a &lt;code&gt;cluster_arn&lt;/code&gt;, not bootstrap servers. Don't dial brokers from CI. Talk to a control plane over HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a control-plane provider looks like
&lt;/h2&gt;

&lt;p&gt;I've been using the &lt;a href="https://registry.terraform.io/providers/conduktor/conduktor/latest/docs" rel="noopener noreferrer"&gt;Conduktor provider&lt;/a&gt;. It doesn't create clusters, it registers ones that already exist and governs what runs on them. MSK, Confluent, Aiven, Redpanda, self-managed, Gateway virtual clusters: same model regardless.&lt;/p&gt;

&lt;p&gt;Registering an existing MSK cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"conduktor_console_kafka_cluster_v2"&lt;/span&gt; &lt;span class="s2"&gt;"msk"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-msk"&lt;/span&gt;
  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;display_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Payments (MSK, eu-west-1)"&lt;/span&gt;
    &lt;span class="nx"&gt;bootstrap_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"b-1.xxxxx.kafka.eu-west-1.amazonaws.com:9198"&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"sasl.jaas.config"&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"software.amazon.msk.auth.iam.IAMLoginModule required awsRoleArn='arn:aws:iam::123456789123:role/MSK-role';"&lt;/span&gt;
      &lt;span class="s2"&gt;"security.protocol"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SASL_SSL"&lt;/span&gt;
      &lt;span class="s2"&gt;"sasl.mechanism"&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS_MSK_IAM"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Gateway virtual cluster registers exactly the same way, so your non-prod isolation is governed by the same code path as prod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"conduktor_console_kafka_cluster_v2"&lt;/span&gt; &lt;span class="s2"&gt;"payments_vc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-vc"&lt;/span&gt;
  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;display_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Payments (virtual cluster)"&lt;/span&gt;
    &lt;span class="nx"&gt;bootstrap_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gateway.internal:6969"&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"security.protocol"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SASL_SSL"&lt;/span&gt;
      &lt;span class="s2"&gt;"sasl.mechanism"&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PLAIN"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;kafka_flavor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;gateway&lt;/span&gt; &lt;span class="p"&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="s2"&gt;"https://gateway.internal:8888"&lt;/span&gt;
        &lt;span class="nx"&gt;user&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;
        &lt;span class="nx"&gt;password&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateway_admin_password&lt;/span&gt;
        &lt;span class="nx"&gt;virtual_cluster&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The topic carries its own metadata
&lt;/h2&gt;

&lt;p&gt;Same topic you'd declare anywhere, except now the fields that matter to humans are first-class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"conduktor_console_topic_v2"&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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"click.orders.events"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;conduktor_console_kafka_cluster_v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"orders"&lt;/span&gt;
    &lt;span class="nx"&gt;criticality&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"C1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Order lifecycle events, one message per state change."&lt;/span&gt;

  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;partitions&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
    &lt;span class="nx"&gt;replication_factor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="nx"&gt;configs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"cleanup.policy"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"delete"&lt;/span&gt;
      &lt;span class="s2"&gt;"retention.ms"&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"604800000"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;prevent_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;prevent_destroy&lt;/code&gt; is not optional, see the partition-shrink story above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policies are Terraform resources
&lt;/h2&gt;

&lt;p&gt;The rules a topic must satisfy are themselves Terraform resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"conduktor_console_topic_policy_v1"&lt;/span&gt; &lt;span class="s2"&gt;"orders_rules"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"orders-topic-rules"&lt;/span&gt;
  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"metadata.name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"^click&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.(?&amp;lt;event&amp;gt;[a-z0-9-]+)&lt;/span&gt;&lt;span class="err"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.(avro|json)$"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="s2"&gt;"metadata.labels.criticality"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;one_of&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"C0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"C1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"C2"&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="s2"&gt;"spec.configs.retention.ms"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;604800000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare this to the usual approach: an OPA or Sentinel check on the Terraform plan. That check only fires for topics created &lt;em&gt;through that repo&lt;/em&gt;. Anyone who opens the UI, runs a CLI, or curls the API will just ignore it.&lt;/p&gt;

&lt;p&gt;A control-plane policy object is evaluated on &lt;strong&gt;every&lt;/strong&gt; creation path, because it lives where the creation is admitted, not where one particular pipeline runs.&lt;/p&gt;

&lt;p&gt;Attach the policy to an application, hand the team an API key scoped to their own application instance instead of an admin key, and the loop closes: the dev applies Terraform &lt;em&gt;in their own repo, with their own credentials&lt;/em&gt;, and gets&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: topic name "orders-events" does not match ^click\.(?&amp;lt;event&amp;gt;[a-z0-9-]+)\.(avro|json)$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...which they can fix in thirty seconds without pinging anyone. That's the difference between self-service and a ticket with YAML.&lt;/p&gt;

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

&lt;p&gt;To clarify, it's not one provider to rule them all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not create clusters.&lt;/strong&gt; No brokers, no VPC, no MSK Serverless. Keep &lt;code&gt;hashicorp/aws&lt;/code&gt; or &lt;code&gt;confluentinc/confluent&lt;/code&gt; for the infra layer. The pairing is: cloud provider creates the cluster, control-plane provider governs what runs on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It requires Conduktor Console.&lt;/strong&gt; It's the IaC surface of a platform, not a standalone Kafka client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns &lt;code&gt;click.orders.events&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What rules must it respect?&lt;/li&gt;
&lt;li&gt;What can the orders team create tomorrow without opening a ticket?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is "our wiki", that's the layer worth moving into code. &lt;/p&gt;

&lt;p&gt;Browse the &lt;a href="https://registry.terraform.io/providers/conduktor/conduktor/latest/docs" rel="noopener noreferrer"&gt;provider docs on the Terraform Registry&lt;/a&gt; if you want to poke at it.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>devops</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Every Application Could Delete Our Schemas.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:49:55 +0000</pubDate>
      <link>https://dev.to/conduktor/your-kafka-schema-registry-is-wide-open-1fl6</link>
      <guid>https://dev.to/conduktor/your-kafka-schema-registry-is-wide-open-1fl6</guid>
      <description>&lt;p&gt;Quick question about your Kafka setup: right now, which applications are allowed to change or delete which schemas?&lt;/p&gt;

&lt;p&gt;Sometimes, it's everyone (because the Schema Registry (SR) is not protected). Sometimes, it's an nginx/traefik in front of their SR to blacklist some routes (like global compatibility) and call it done. Sometimes, it's just to add basic authentication at least. Rarely, it's about authorization.&lt;/p&gt;

&lt;p&gt;Authorization is about "what are you allowed to change/break?"&lt;/p&gt;

&lt;p&gt;Let me show you where the gap actually is, because "just put a proxy in front of it" is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication isn't authorization
&lt;/h2&gt;

&lt;p&gt;A schema registry exists to solve a coordination problem. Producers and consumers ship on their own schedules, so they need a shared, versioned contract for the shape of each message. Producers register a schema, consumers resolve it by ID, and the registry enforces compatibility as things evolve so one side's change doesn't blow up the other.&lt;/p&gt;

&lt;p&gt;That makes it a very high-value target. &lt;em&gt;Everything&lt;/em&gt; producing or consuming structured data depends on it. So "who can change it" should be a first-class question and in the free Confluent Schema Registry, the answer is "anyone."&lt;/p&gt;

&lt;p&gt;The default listener is plain HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Schema Registry default — plaintext, every interface
&lt;/span&gt;&lt;span class="py"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://0.0.0.0:8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-subject access control isn't in the free version. It's a commercial add-on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Until either ACLs or Role-Based Access Control is also enabled for Schema Registry, any user can create, alter, and delete Schema Registry subjects."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create/alter/delete arê the controls you actually want is per-subject: &lt;em&gt;the payments team can write `payments-&lt;/em&gt;` schemas and nothing else.*&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just Confluent
&lt;/h2&gt;

&lt;p&gt;The free registry is the most obvious offender, but it's not alone. Every option handles per-subject authorization differently, and most of it is off by default:&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;Per-subject authz&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Uses your Kafka identity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Confluent SR (free)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;open, plain HTTP&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apicurio&lt;/td&gt;
&lt;td&gt;registry-wide roles only&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Karapace&lt;/td&gt;
&lt;td&gt;yes, regex ACL per subject&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confluent RBAC / plugin&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;off, commercial&lt;/td&gt;
&lt;td&gt;Confluent's plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic HTTP proxy&lt;/td&gt;
&lt;td&gt;coarse, path-prefix only&lt;/td&gt;
&lt;td&gt;you build it&lt;/td&gt;
&lt;td&gt;no — matches the URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per-subject control exists in a couple of places, but it's either a hand-edited auth file (Karapace) or a paid platform tier (Confluent RBAC). None of the free or open-source options tie the rule to the Kafka identities you already manage, or give you an audit trail that lines up with Kafka.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a generic HTTP proxy is a bad solution
&lt;/h2&gt;

&lt;p&gt;Since the registry speaks HTTP, the natural thinking is to set up an nginx or an API gateway and only allow the routes you want. For basic guardrails on a single registry, that's cheap and reasonable. Subject names even show up in the path, so you can gate writes by prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow writes only to payments-* subjects&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/subjects/payments-.*/versions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://schema-registry:8081&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;Looks fine. Then you need to maintain the nginx file, meet the edge cases, and you can only reason about URLs: not schemas, not identities. Here's the one that always gets people:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /schemas/ids/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fetches a schema by numeric ID. There's &lt;strong&gt;no subject in the path&lt;/strong&gt; to match on, so your careful &lt;code&gt;payments-*&lt;/code&gt; rule doesn't apply — and that call can return any schema, including another team's. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It authorizes the &lt;strong&gt;network path, not your Kafka principal&lt;/strong&gt;: so it's only as strong as the firewall around port 8081.&lt;/li&gt;
&lt;li&gt;It assumes subjects are named after topics. Switch to &lt;code&gt;RecordNameStrategy&lt;/code&gt; and the subject is a record name, so your prefix rules stop lining up.&lt;/li&gt;
&lt;li&gt;It can't tell a deliberate registration from a client silently auto-registering on first produce:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# One flag away from clients registering schemas you never reviewed
&lt;/span&gt;&lt;span class="py"&gt;auto.register.schemas&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to go down this path will create an abomination you'll need to maintain forever with your own model of subjects and owners inside your proxy. That's a lot of infrastructure to own just to answer "who can touch payments schemas."&lt;/p&gt;

&lt;h2&gt;
  
  
  What is "closed by default"?
&lt;/h2&gt;

&lt;p&gt;Per-subject authorization should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;By subject, prefix, and wildcard&lt;/strong&gt;, not by URL path that happens to contain a subject sometimes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tied to the Kafka identities you already use&lt;/strong&gt;, the same principals you manage for topic ACLs, not a second, parallel access system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged end to end&lt;/strong&gt;, every operation and every denial, in a trail that lines up with the rest of your Kafka audit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The way to get all three without a platform license or bespoke plumbing is a &lt;em&gt;schema-aware&lt;/em&gt; proxy, something that understands subjects and requests rather than URLs. That's the category &lt;a href="https://www.conduktor.io/schema-registry-proxy" rel="noopener noreferrer"&gt;Conduktor's Schema Registry Proxy&lt;/a&gt; sits in: it fronts the registry you already run, checks read and write permissions per subject, logs every call, and doesn't need any producer or consumer changes. You just point &lt;code&gt;schema.registry.url&lt;/code&gt; at the proxy and keep your current registry, Confluent or open source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clients don't change — they just talk to the proxy
&lt;/span&gt;&lt;span class="py"&gt;schema.registry.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://schema-registry-proxy:8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a protected schema registry, reads and writes, that can even be linked to a self-service framework where ownership is a first-class citizen.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>devops</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Every Kafka Cluster Eventually Hits This Networking Problem</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:42:50 +0000</pubDate>
      <link>https://dev.to/conduktor/kafka-addressing-vpc-peering-5f3j</link>
      <guid>https://dev.to/conduktor/kafka-addressing-vpc-peering-5f3j</guid>
      <description>&lt;p&gt;If you've ever tried to connect a Kafka client that lives in a different VPC than the cluster, you've probably hit this issue where there's a route between the two networks, telnet works, and yet the client still can't consume a single record. It feels like a networking bug. It isn't.&lt;/p&gt;

&lt;p&gt;The issue is that &lt;strong&gt;reaching Kafka across VPCs is an addressing problem, not a networking one.&lt;/strong&gt; Once that clicks, why peering, transit gateways, and per-broker load balancers don't work will make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your client actually does when it connects
&lt;/h2&gt;

&lt;p&gt;Most services are easy to reach across a networking boundary. Put a load balancer in front, give it one address, point clients at it. Done.&lt;/p&gt;

&lt;p&gt;Kafka doesn't work like that. A client connects in two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap.&lt;/strong&gt; The client talks to any broker and asks one question: who's in this cluster?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct connection.&lt;/strong&gt; The broker answers with metadata — a list of &lt;em&gt;every&lt;/em&gt; broker, each named by its own &lt;code&gt;advertised.listeners&lt;/code&gt; address. The client then opens direct connections to specific brokers: the leader for each partition it reads or writes (or the nearest replica, if you're using follower fetching, KIP-392, Kafka 2.4+).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second stage is the most important to remember. One shared address in front of the cluster is not working, because the client has to resolve and route to &lt;em&gt;each broker's advertised address, exactly as the cluster hands it back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So if a broker advertises &lt;code&gt;broker-1.cluster.internal:9092&lt;/code&gt;, a private name that only resolves inside the cluster's VPC, a remote client connects to the bootstrap fine, then fail when it tries to connect directly to one of the brokers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec kafka-consumer-a kcat -b kafka:9092 -L -m 5
# -&amp;gt; Failed to resolve 'kafka:9092'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A network path between two VPCs is not the same as Kafka reachability. The client still has to resolve and route to every broker's advertised address &lt;em&gt;from where it sits&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;"Then just make the broker advertise something reachable". Sure, brokers support multiple listeners, one internal and one external. But every listener is another port to open on every broker, and managed Kafka (MSK, Confluent Cloud, Aiven) won't let you touch them anyway. Listeners don't scale to a dozen independent networks across accounts and clouds. Which is exactly the situation you're in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why peering collapses the moment you have more than two networks
&lt;/h2&gt;

&lt;p&gt;VPC peering is genuinely great for connecting two VPCs. Past two, here be dragons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's point-to-point and non-transitive.&lt;/strong&gt; Peering A and B each to the cluster does &lt;em&gt;not&lt;/em&gt; let A talk to B. Every new client network needs its own peering straight into the cluster's VPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your cluster's VPC becomes an accidental hub.&lt;/strong&gt; Ten client networks means ten peerings, ten route-table entries, and ten security-group conversations on a VPC that was never designed to be a hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CIDRs have to be coordinated.&lt;/strong&gt; Peered VPCs can't overlap ranges, so every team negotiates address space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Kafka caps it.&lt;/strong&gt; Providers limit how many peerings or PrivateLink attachments you get, and none of them let you rewrite broker listeners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recurring pain is one line: &lt;strong&gt;every new client forces another change onto the cluster's VPC&lt;/strong&gt;, the one piece of infra you least want to keep editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: let a Kafka-aware proxy rewrite the addresses
&lt;/h2&gt;

&lt;p&gt;The solution: put something in the connection path that actually &lt;em&gt;understands the Kafka protocol&lt;/em&gt; instead of just shuffling packets, and have it rewrite the broker addresses in the metadata response before the client ever sees them.&lt;/p&gt;

&lt;p&gt;That's what &lt;a href="https://www.conduktor.io/gateway" rel="noopener noreferrer"&gt;Conduktor Gateway&lt;/a&gt; does. A broker advertises &lt;code&gt;kafka-internal:9092&lt;/code&gt;; the Gateway rewrites that to an address the client can reach. Because clients connect to specific brokers (the partition leaders), the Gateway maps each broker to its own port so traffic still routes deterministically to the right one behind it.&lt;/p&gt;

&lt;p&gt;From the client's side, the only change is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# before: pointing straight at a broker it can't actually reach
&lt;/span&gt;&lt;span class="py"&gt;bootstrap.servers&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;broker-1.cluster.internal:9092&lt;/span&gt;

&lt;span class="c"&gt;# after: pointing at the proxy, which hands back addresses it can
&lt;/span&gt;&lt;span class="py"&gt;bootstrap.servers&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;conduktor-gateway.hub:9092&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credentials don't change. The client presents the same SASL credentials it already has, the Gateway forwards them to the broker, and Kafka ACLs (or Confluent RBAC) still decide what it's allowed to do. The proxy is stateless, it stores no credentials and adds no new auth model.&lt;/p&gt;

&lt;p&gt;The nice architectural payoff: the Gateway lives in its own &lt;strong&gt;hub VPC&lt;/strong&gt;. Peerings or PrivateLink attachments land on the hub, never directly between a client and the cluster. New networks attach to the hub; the cluster keeps a single attachment no matter how many clients reach in, and its listeners are never touched.&lt;/p&gt;

&lt;p&gt;The Gateway isn't a substitute for a route as you still need connectivity between the hub and each VPC. What it removes is the per-client peering into the cluster, the broker reconfiguration, and the requirement that every advertised address be reachable from every client.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But what about a transit gateway? Or an LB per broker?"
&lt;/h2&gt;

&lt;p&gt;Won't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cloud transit gateway&lt;/strong&gt; is one hub many VPCs attach to for any-to-any IP connectivity. But it operates at L3 so it moves IP packets with zero awareness of Kafka. It doesn't change what brokers advertise, so you're back to sharing private hosted zones across accounts. Overlapping CIDRs still break it without NAT, it's confined to one cloud, and on managed Kafka you still can't touch the listeners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A load balancer per broker&lt;/strong&gt; can't be round-robined, because clients connect to specific leaders. So it's one NLB (or target group, or PrivateLink endpoint) &lt;em&gt;per broker&lt;/em&gt;, plus &lt;code&gt;advertised.listeners&lt;/code&gt; reconfigured: N pieces of plumbing to keep in sync, each with its own port, DNS record, and TLS SAN. Scale or replace a broker and the mapping churns, forcing a rolling restart. And on managed Kafka you usually can't set advertised listeners at all — so it's off the table before you begin.&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;Works at&lt;/th&gt;
&lt;th&gt;Fixes the advertised-address problem?&lt;/th&gt;
&lt;th&gt;Managed Kafka?&lt;/th&gt;
&lt;th&gt;Cross-cloud?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VPC peering&lt;/td&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Capped by limits&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transit gateway&lt;/td&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;On-prem yes, other cloud no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NLB per broker&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;Only if you reconfigure listeners&lt;/td&gt;
&lt;td&gt;Usually not&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka-aware proxy&lt;/td&gt;
&lt;td&gt;Kafka L7&lt;/td&gt;
&lt;td&gt;Yes, automatically&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;h2&gt;
  
  
  Reproduce the whole thing on one machine
&lt;/h2&gt;

&lt;p&gt;The free &lt;a href="https://www.conduktor.io/gateway/community-edition" rel="noopener noreferrer"&gt;Gateway Community&lt;/a&gt; quickstart stands up the exact scenario: a Kafka cluster in a private Docker network the clients can't reach, two consumers in two separate "VPC" networks, and the Gateway as the only container joined to all three.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash &amp;lt;(curl -fsSL https://releases.conduktor.io/gateway-community-quickstart)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumer A can't even resolve the broker directly — different network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec kafka-consumer-a kcat -b kafka:9092 -L -m 5
# -&amp;gt; Failed to resolve 'kafka:9092'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same consumer, same SASL credentials, one different bootstrap address — now it reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec kafka-consumer-a kcat -b conduktor-gateway:9092 -t customers -C -e -c 3 \
  -s value=avro -r http://karapace:8081 \
  -X security.protocol=SASL_PLAINTEXT -X sasl.mechanism=PLAIN \
  -X sasl.username=consumer-a -X sasl.password=consumer-a-secret
# -&amp;gt; readable records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the log line that proves &lt;em&gt;why&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker logs conduktor-gateway 2&amp;gt;&amp;amp;1 | grep "Rewriting METADATA"
# kafka:9092 -&amp;gt; conduktor-gateway:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole trick. The broker advertises an address the client can't reach; the proxy rewrites it to one it can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If cross-VPC Kafka has been a recurring issue on your platform team, try rethinking it: you don't have a networking problem, you have an addressing problem, and a Kafka-aware proxy is the one thing that fixes the address without you touching the cluster.&lt;/p&gt;

&lt;p&gt;If you later want to encrypt fields, enforce schemas, or isolate tenants on that same path, this same proxy will do it too.&lt;/p&gt;

&lt;p&gt;If you want to poke at it, the &lt;a href="https://www.conduktor.io/gateway/community-edition" rel="noopener noreferrer"&gt;Gateway Community quickstart&lt;/a&gt; is free and runs on one machine. Curious how you're solving this today, peering everything everywhere, transit gateways, something else? Let me know.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>datastreaming</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>We Measured Kafka Usage. The Results Surprised Us.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:32:43 +0000</pubDate>
      <link>https://dev.to/conduktor/kafka-cost-optimization-starts-with-usage-3lfb</link>
      <guid>https://dev.to/conduktor/kafka-cost-optimization-starts-with-usage-3lfb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfvlbwdkw5n53i3qecfi.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%2Fpfvlbwdkw5n53i3qecfi.png" alt="Kafka costs are about usage not just infra" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I sit in a lot of Kafka reviews. Vendors, instances, replication, tiered storage, advanced stuff like fetch-from-follower, networking, partitions, best practices etc. Most discussions are driven by tech only, instead of looking at the big picture: how this beautiful infra is being used.&lt;/p&gt;

&lt;p&gt;Unpopular opinion: &lt;strong&gt;most of your Kafka cost is not due to infrastructure, it's due to a usage problem.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the cost actually comes from
&lt;/h2&gt;

&lt;p&gt;Vendor calculators are hard to compare because of so many assumptions. Replication multipliers, disk class, compression ratio, tiered storage (billed at the replicated rate or the actual S3 rate). The price you see is almost never what you pay.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RF=3 multiplies the per-GB price by 3 everywhere.&lt;/strong&gt; And tiered storage is often &lt;em&gt;still&lt;/em&gt; billed at the replicated rate even though only one copy lives in S3. You're paying the RF=3 rate for data Kafka no longer replicates. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-VPC, in-region traffic&lt;/strong&gt; between your account and the vendor's lands on &lt;em&gt;your&lt;/em&gt; cloud bill, roughly 1c/GB each way depending on the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Without fetch-from-follower, most consumer fetches cross AZ boundaries.&lt;/strong&gt; With three balanced AZs, ~2/3 of consumer reads go cross-AZ, because the leader lives in one AZ and the other two reads come from elsewhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression is often just... off.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With zstd at sane batch sizes, JSON-ish logs and metrics commonly compress 8–10x:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;compression.type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;zstd&lt;/span&gt;
&lt;span class="py"&gt;batch.size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65536          # 64KB&lt;/span&gt;
&lt;span class="py"&gt;linger.ms&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Going from 5x to 10x halves your stored bytes &lt;em&gt;and&lt;/em&gt; halves the replication bytes flowing inside the cluster. You pay for that traffic three times over at RF=3, so the ratio matters.&lt;/p&gt;

&lt;p&gt;And fetch-from-follower, available since Kafka 2.4, is a broker + consumer config away. Same-AZ traffic inside your VPC is free on AWS, so no cross-AZ tax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# broker
&lt;/span&gt;&lt;span class="py"&gt;replica.selector.class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;org.apache.kafka.common.replica.RackAwareReplicaSelector&lt;/span&gt;
&lt;span class="py"&gt;broker.rack&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us-east-1a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# consumer — must match the broker's rack value
&lt;/span&gt;&lt;span class="py"&gt;client.rack&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us-east-1a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do all of it: fetch-from-follower, tiered storage, compression enforcement, partition right-sizing, BYOC to apply your existing cloud discount, single-AZ topics where you can tolerate it. But notice that &lt;em&gt;it's still infrastructure tuning.&lt;/em&gt; Let's go up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost is a stack, not a line item
&lt;/h2&gt;

&lt;p&gt;When you tune anything in Kafka, you think in layers, bottom-up: hardware, JVM, broker config, producer/consumer tuning, topic design, application code. Same for cost:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud infrastructure&lt;/strong&gt;: instance types, AZ placement, networking, BYOC negotiation. At big contract sizes, negotiated networking discounts can hit 90%, but only if traffic flows through &lt;em&gt;your&lt;/em&gt; account. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broker &amp;amp; protocol tuning&lt;/strong&gt;: compression, retention, RF, fetch-from-follower, tiered storage, partition count. Easy, they're config changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: diskless topics, Iceberg topics, single-AZ topics, proxies between clients and brokers, virtual clusters for multi-tenancy and non-prod consolidation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt;: fan-out, governance, discovery, self-service. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Often &lt;strong&gt;payoff goes up as you go higher (more system thinking)&lt;/strong&gt; Everyone's comfortable arguing about GP2 vs GP3 (volume types on AWS. Almost nobody thinks "why are 40% of these partitions doing nothing?"&lt;/p&gt;

&lt;p&gt;Speaking of which: most clusters carry &lt;a href="https://www.conduktor.io/blog/the-surprising-cost-of-kafka-partition-waste" rel="noopener noreferrer"&gt;40–70% partition waste&lt;/a&gt;, did you know that? On managed Kafka that's per-partition-hour billing. On self-managed, you hit the ~4,000–6,000 partition-replicas-per-broker ceiling (RF=3 turns 100k partitions into 300k replicas to host and track). KRaft raises the ceiling but it doesn't make the waste free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fan-out is the whole point of Kafka
&lt;/h2&gt;

&lt;p&gt;Kafka exists so that one byte written can be read by N independent consumers, decoupled in time, with zero coordination back to the producer. That's the log abstraction's reason to live.&lt;/p&gt;

&lt;p&gt;Do you measure your average fan-out? If it's 1, you probably shouldn't be running Kafka at all, you're paying for a distributed log to do a point-to-point queue's job. LinkedIn famously ran at ~5.4: the same bytes, written once, read by 5.4 independent teams.&lt;/p&gt;

&lt;p&gt;Cluster cost stays flat while consumers grow, so cost-per-business-outcome is decreasing the more we consume existing topics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost_per_use_case = cluster_cost / fan_out

fan-out 1  -&amp;gt;  $X      (one team carries the whole bill)
fan-out 3  -&amp;gt;  $X / 3
fan-out 5  -&amp;gt;  $X / 5  (same hardware, five outcomes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Is our Kafka usage growing?" is the wrong question. More business use-cases reading existing data is the best money you'll ever spend. Duplicated topics because nobody could find the existing one is pure waste, more storage, more replication, more pipelines, all because discovery and ownership are missing.&lt;/p&gt;

&lt;p&gt;The same goes for partitions: people over-provision because nobody knows how to size them, and you can't reduce partition count after the fact (breaks key ordering). The only way to surface that waste is &lt;a href="https://www.conduktor.io/blog/chargeback-attribute-map-kafka-costs-to-your-business" rel="noopener noreferrer"&gt;chargeback at the team-and-topic level&lt;/a&gt;. You can't optimize what you can't attribute.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A third of our traffic, we know what it has to do with, but we don't know exactly what they're doing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the usage layer leaking. It costs money, and nobody can fix it because nobody knows how to, where to look, or just own it. It's not an infra problem, it's governance, discovery, and self-service.&lt;/p&gt;

&lt;p&gt;Cost optimization is &lt;strong&gt;everybody's concern and nobody's objective.&lt;/strong&gt; Teams over-provision because &lt;em&gt;what if we need it later&lt;/em&gt; and &lt;em&gt;what if it breaks when we touch it&lt;/em&gt; are rational fears. "It's expensive" is not a business case. What works is showing the waste, the annual dollar number, and the effort to reclaim it, with a name next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026: Where to spend your effort
&lt;/h2&gt;

&lt;p&gt;Most deployments I see have way more headroom in the usage layer than the infra layer: topics nobody reads, partitions nobody needs, teams who'd benefit from streaming but find it too painful to onboard.&lt;/p&gt;

&lt;p&gt;There's a funny industry reflex here too. We chase the next architectural shiny thing, diskless, Iceberg topics, single-AZ, before we've answered the boring questions: who's using this, for what, and why aren't more teams using it?&lt;/p&gt;

&lt;p&gt;My actual recommendation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do the infrastructure pass once.&lt;/strong&gt; Instance types, AZ placement, BYOC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the config pass once.&lt;/strong&gt; Compression, retention, partition right-sizing, fetch-from-follower, tiered storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend the rest of the year on the usage layer.&lt;/strong&gt; Fan-out, ownership, discovery, chargeback, self-service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 are a sprint. Step 3 is the marathon.&lt;/p&gt;

&lt;p&gt;If you want to see where your usage layer is leaking, Conduktor's field engineering team does a &lt;a href="https://www.conduktor.io/contact/kafka-cost-analysis" rel="noopener noreferrer"&gt;free Kafka cost analysis&lt;/a&gt;: they'll map cost back to teams and topics and show you where the payoff sits. And if you just want to keep reading, &lt;a href="https://www.conduktor.io/blog/a-better-conversation-about-kafka-costs" rel="noopener noreferrer"&gt;Why Kafka Costs Keep Rising&lt;/a&gt; and &lt;a href="https://www.conduktor.io/blog/the-surprising-cost-of-kafka-partition-waste" rel="noopener noreferrer"&gt;the partition waste deep-dive&lt;/a&gt; are good next stops.&lt;/p&gt;

&lt;p&gt;What's your average fan-out? If you don't know it off the top of your head, that's probably where I'd start.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>We Built a Kafka Proxy. Here's Everything It Ended Up Doing.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:43:15 +0000</pubDate>
      <link>https://dev.to/conduktor/you-can-do-what-with-a-kafka-proxy-42b1</link>
      <guid>https://dev.to/conduktor/you-can-do-what-with-a-kafka-proxy-42b1</guid>
      <description>&lt;p&gt;At Current 2026, I realized that nobody knows exactly what a Kafka proxy can do.&lt;/p&gt;

&lt;p&gt;Most engineers and architects think it's just some kind of reverse-proxy for Kafka (think nginx) to do routing and used to bridge a legacy or non-native client to the cluster. &lt;/p&gt;

&lt;p&gt;That's not it. It's barely the start of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encryption
&lt;/h2&gt;

&lt;p&gt;For instance, an engineer at a UK building society had a hard requirement: encrypt personally identifiable fields before they ever hit Kafka: emails, national insurance numbers, that kind of data.&lt;/p&gt;

&lt;p&gt;His team built encryption into the application layer. Every producer that touched PII got encryption code. Every consumer got decryption code. Key handling, rotation, etc. to manage across services.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In every producer that touches PII...&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEmail&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;keyRef&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pii-key"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSsn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSsn&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;keyRef&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pii-key"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// And the mirror image in every consumer...&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEmail&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSsn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSsn&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiply that by all the micro-services to update and maintain now (cross languages, versioning, access to KMS etc.). That's quite expensive, at implementation time and to maintain.&lt;/p&gt;

&lt;p&gt;He didn't know a Kafka proxy could have done the whole thing at the record level, outside the apps. When we chat about it, he just realized he might have done a mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Kafka proxy does
&lt;/h2&gt;

&lt;p&gt;A Kafka proxy sits between your clients and your brokers and speaks the Kafka protocol. Clients connect to it exactly like they'd connect to a broker. No SDK, no app changes. It works for Kafka clients, Kafka Connect, Kafka Streams, Flink, Spark, etc. It's fully transparent to them.&lt;/p&gt;

&lt;p&gt;It makes it a natural place to put policy that doesn't belong inside your application and doesn't belong inside the cluster either.&lt;/p&gt;

&lt;p&gt;Encryption is the obvious one. Instead of touching dozens of applications, you declare the rule once. With &lt;a href="https://www.conduktor.io/gateway" rel="noopener noreferrer"&gt;Conduktor Gateway&lt;/a&gt; it's what we call an interceptor: a small piece of config applied to traffic matching a topic pattern. Roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Interceptor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gateway/v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"encrypt-customer-pii"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pluginClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.conduktor.gateway.interceptor.EncryptionPlugin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customers.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kmsConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VAULT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://vault:8200"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"recordValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AES256_GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"keySecretId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pii-key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fieldName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AES256_GCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"keySecretId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pii-key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy encrypts the fields on the way in and authorized consumers get them decrypted on the way out, everyone else gets ciphertext. The application code shrinks back to just... sending a record, not dealing with KMS and secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Same producer, after.&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProducerRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the rule lives in one declarative place (the proxy), you can do things that are painful at the app layer.&lt;/p&gt;

&lt;p&gt;For instance, crypto-shredding for GDPR. Delete the key, and every message encrypted with it becomes unreadable, instantly, across all your retention. You don't go hunting through topics for one person's data. You revoke a key. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Masking, validation, isolation: same one place
&lt;/h2&gt;

&lt;p&gt;Once the proxy is in the Kafka path, the same pattern opens a lot of doors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field-level masking&lt;/strong&gt;: show &lt;code&gt;j***@example.com&lt;/code&gt; to one team, the real value to another, same topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema and payload validation&lt;/strong&gt;: reject malformed records at the edge instead of poisoning a downstream consumer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic aliasing for migration&lt;/strong&gt;: point clients at a stable name while you move the real topic between clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual clusters&lt;/strong&gt;: carve one physical cluster into isolated tenants without standing up new infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit and policy enforcement&lt;/strong&gt;: log and gate access without patching the broker or the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that touches application code or broker config. It's policy, declared once, enforced in the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this connects to cost and self-service
&lt;/h2&gt;

&lt;p&gt;The other big topic at the conference was cost. Conduktor published a &lt;a href="https://www.conduktor.io/resources/ebooks/where-kafka-costs-hide-a-field-guide" rel="noopener noreferrer"&gt;field guide on where Kafka costs hide&lt;/a&gt; in April.&lt;/p&gt;

&lt;p&gt;Then the self-service conversation: teams want developers to create topics and request access in autonomy, but simple Topic on GitOps solution is just not enough, because self-service without guardrails easily turns Kafka into a mess:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If somebody goes onto the tool and adds in something ridiculous, like a thousand partitions, we need someone to have eyes on that. That's something we've learned we can't let go of."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Look at what encryption-at-the-proxy, cost guardrails, and self-service approval gates have in common. They're all policy that belongs &lt;em&gt;between&lt;/em&gt; your developers and your brokers, not baked into either one. Push it into the app and you copy-paste it dozens of time. Push it into the cluster and you can't change it without a migration. Put it in the layer in between, declare it once, and you can actually govern it.&lt;/p&gt;

&lt;p&gt;To build AI agents on top of streaming, the plumbing must come first: ownership, schema discipline, key custody, data quality before the data even enters Kafka, etc. A proxy that enforces structure and policy is a big chunk of that plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where does your policy live?
&lt;/h2&gt;

&lt;p&gt;What people think a proxy does is pass packets. What it really does is more of a safekeeper that holds the policy.&lt;/p&gt;

&lt;p&gt;If you've got encryption, masking, validation, or multi-tenant isolation scattered across your services right now, it's worth asking whether any of it should be living one layer down instead.&lt;/p&gt;

&lt;p&gt;Want to go deeper? The &lt;a href="https://www.conduktor.io/gateway" rel="noopener noreferrer"&gt;Gateway overview&lt;/a&gt; walks through the interceptor model, and the &lt;a href="https://www.conduktor.io/blog/what-we-learned-at-current-2026" rel="noopener noreferrer"&gt;original Current 2026 write-up&lt;/a&gt; has the rest of what we heard on the floor.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>architecture</category>
      <category>dataengineering</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Gave Claude Admin Access to My Kafka Cluster</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 08 Jun 2026 13:34:35 +0000</pubDate>
      <link>https://dev.to/conduktor/i-let-an-ai-agent-set-up-my-entire-kafka-platform-heres-what-actually-happened-220m</link>
      <guid>https://dev.to/conduktor/i-let-an-ai-agent-set-up-my-entire-kafka-platform-heres-what-actually-happened-220m</guid>
      <description>&lt;p&gt;Your AI coding assistant can explain consumer groups, rebalancing, and exactly-once semantics. Ask it to actually &lt;em&gt;set up&lt;/em&gt; a Kafka platform with governance, though, and it won't be able to do that on its own.&lt;/p&gt;

&lt;p&gt;Between hallucinations, misunderstanding, production impact (I really saw Claude messing up a rolling upgrade of Kafka brokers), and the lack of knowledge of the products your Kafka infra is relying on, there's a lot working against it&lt;/p&gt;

&lt;p&gt;The models, besides their training, have zero context about your infra. They've never seen your cluster, don't know your policies (technical, governance), and often have no way to check anything against your actual environment.&lt;/p&gt;

&lt;p&gt;You can give it the missing context using Conduktor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that was missing
&lt;/h2&gt;

&lt;p&gt;There is an open-source &lt;a href="https://github.com/conduktor/skills" rel="noopener noreferrer"&gt;Conduktor skill&lt;/a&gt; you install into your AI assistant. It works with Claude Code, Cursor, VS Code Copilot, Gemini CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add conduktor/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is teaching the agent the whole platform and how to run process against it: Console, Gateway, and the CLI, so it can be efficient and not hallucinate.&lt;/p&gt;

&lt;p&gt;After the install, the agent discovers your environment (Kafka clusters, Schema Registry, policies, etc.), asks questions based on what it finds, generates configs with &lt;em&gt;real&lt;/em&gt; values and best practices, and runs everything with dry-run validation before it touches anything.&lt;/p&gt;

&lt;p&gt;The CLI are really its "hands" as more deep than just MCP. The skill is the playbook where all the experience and practices from years of usage are written. This does a big difference VS "generate some YAML and cross fingers"&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting from absolutely nothing
&lt;/h2&gt;

&lt;p&gt;You can start from scratch with just Docker running and nothing else. No Kafka, no Conduktor, no config. When I just ask this (with the Conduktor skill setup): &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;install Conduktor and set it up so I can login&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It checked my environment, asked what I was trying to do, wrote a &lt;code&gt;docker-compose.yml&lt;/code&gt;, spun up the containers, hit one error along the way, self-corrected, and handed me a working platform, Kafka &amp;amp; Console perfectly configured.&lt;/p&gt;

&lt;p&gt;I could ask the same but on my production Kubernetes. It would follow best practices too, use Helm, discover my environment, etc., and in minutes everything would be wired perfectly, with policies already in place.&lt;/p&gt;

&lt;p&gt;This is much more powerful than a "human" quickstart, as the range of applications it covers is just wider and more production-ready already. The agent knows the Kafka domain, and with the skill it knows Conduktor, so the combination of both makes it ask me the right questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance, without becoming a Kafka lawyer
&lt;/h2&gt;

&lt;p&gt;Running Kafka isn't the hard part anymore. Making it &lt;em&gt;safe for a team to share&lt;/em&gt; is the hard part: naming conventions, ownership boundaries, policies. This is what prevent a Kafka cluster from turning into a wasteland of &lt;code&gt;test-topic-final-v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The beautiful thing is to be able to ask large prompts like this now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;set up governance for two teams, Payments and Analytics, with topic policies and cross-team permissions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It worked in stages and figured out the dependency ordering itself. When the API rejected something, it read the rejection, restructured the YAML, and retried, with minimal hand-holding from me (just asking what policies I want based on what's possible). It ended up creating the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TopicPolicy&lt;/code&gt; objects: locking down naming per team, enforcing safe defaults (retention, replication, required labels) across every topic. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Application&lt;/code&gt; objects with non-overlapping resource boundaries to define ownership of resources and teams.&lt;/li&gt;
&lt;li&gt;Topics with descriptions and labels in the catalog.&lt;/li&gt;
&lt;li&gt;Cross-team permission giving Analytics read access to &lt;code&gt;payments.orders.*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is federated ownership in practice: the platform team sets the boundaries, developers move freely inside them. Normally that knowledge takes months to accumulate and lives spreadsheet or Jira tickets. Here it lives in a skill file that every agent on the team can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now flip to the developer side
&lt;/h2&gt;

&lt;p&gt;Once those guardrails exist, a developer on the Payments team installs the &lt;em&gt;same skill&lt;/em&gt; and never has to know any of it happened. No &lt;code&gt;ApplicationInstance&lt;/code&gt;, no &lt;code&gt;TopicPolicy&lt;/code&gt;, no YAML. They just talk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What topics do we have?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent runs &lt;code&gt;conduktor get Topic&lt;/code&gt; and shows the catalog — descriptions, owners, labels, visibility. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I need a topic for my service."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent checks their &lt;code&gt;ApplicationInstance&lt;/code&gt;, reads the policy constraints (naming prefix &lt;code&gt;payments.*&lt;/code&gt;, retention one-to-seven days, a required &lt;code&gt;data-criticality&lt;/code&gt; label), asks what the topic is for, generates compliant YAML, dry-runs it, and applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Topic/payments.fulfillment.shipped: Created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer just got a topic that's compliant by default. Without the skill, that's a JIRA ticket most likely, and asking platform team what's the right shape and what to put.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I produce to my topic?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It reads the cluster config, grabs the real bootstrap server, and hands back working code:&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;confluent_kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Producer&lt;/span&gt;

&lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Producer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bootstrap.servers&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;localhost:19092&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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payments.fulfillment.shipped&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ord-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&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="s"&gt;orderId&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="s"&gt;ord-123&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="s"&gt;status&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="s"&gt;shipped&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="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy, paste, run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I need to read the Analytics team's clickstream."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent finds that &lt;code&gt;analytics.clickstream.pageviews&lt;/code&gt; belongs to the Analytics team, then writes a read-only permission scoped to exactly that topic, at both the Kafka and Console layers. The developer doesn't know what an ACL is or what &lt;code&gt;patternType: LITERAL&lt;/code&gt; means. They asked in English and got access. &lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually take away from this
&lt;/h2&gt;

&lt;p&gt;This walkthrough only touched governance and onboarding. The skill also covers Gateway (Kafka proxy) encryption, data quality rules, Terraform export, and CI/CD scaffolding.&lt;/p&gt;

&lt;p&gt;Try it, it's one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add conduktor/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's &lt;a href="https://github.com/conduktor/skills" rel="noopener noreferrer"&gt;open source&lt;/a&gt;, so if you hit a workflow it handles badly, open a PR. And if you're new to Conduktor, the &lt;a href="https://www.conduktor.io/community" rel="noopener noreferrer"&gt;Community Edition&lt;/a&gt; is free and self-hosted, the skill will do the install for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was adapted from the &lt;a href="https://www.conduktor.io/blog/set-up-a-kafka-platform-with-an-ai-agent" rel="noopener noreferrer"&gt;original on the Conduktor blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>dataengineering</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Kafka Engineers Actually Care About in 2026</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:23:50 +0000</pubDate>
      <link>https://dev.to/sderosiaux/5-things-kafka-practitioners-actually-said-at-current-2026-2l01</link>
      <guid>https://dev.to/sderosiaux/5-things-kafka-practitioners-actually-said-at-current-2026-2l01</guid>
      <description>&lt;p&gt;We were at Current 2026 in London two weeks ago. Keynotes about agentic AI and streaming agents but conferences conversations were about something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The IBM-Confluent acquisition?
&lt;/h2&gt;

&lt;p&gt;Six months after IBM acquired Confluent for $11B, I expected hot takes. What I got was mostly 'indifference'?&lt;/p&gt;

&lt;p&gt;A data engineering lead at a major US bank called himself a "doomer", Kafka and Cassandra are his two favorite open-source projects, and both now sit under IBM. A staff engineer at a streaming vendor was more optimistic, speculating about Confluent's AI team merging with Watson.&lt;/p&gt;

&lt;p&gt;The majority were more like "I try to concentrate on the tech side." "I don't know enough to comment." Moving on.&lt;/p&gt;

&lt;p&gt;The acquisition was in the air, but practitioners had more pressing problems to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Kafka costs are the third certainty (after death and taxes)
&lt;/h2&gt;

&lt;p&gt;Every cost conversation we had mapped to the same things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition overprovisioning&lt;/strong&gt;: inherited from not-knowing or templates tuned for a different workload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention mismatches&lt;/strong&gt;: defaults that outlive the use case they were set for&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster proliferation&lt;/strong&gt;: one-off clusters that never got consolidated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphan and duplicate topics&lt;/strong&gt;: experiments or dead projectsthat never got cleaned up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inefficient client patterns&lt;/strong&gt;: batching, compression, and serialization left on defaults + &lt;a href="https://www.conduktor.io/blog/librdkafka-vs-java-client" rel="noopener noreferrer"&gt;JVM vs librdkafka differences&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static capacity&lt;/strong&gt;: paying for peak when the load is 10% of that most of the time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some examples of scale where they had these problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;six years of unmanaged topics inherited during a Confluent Cloud migration&lt;/li&gt;
&lt;li&gt;one team had twenty MSK clusters running multi-regions&lt;/li&gt;
&lt;li&gt;MSK, Confluent, and Redpanda all running in parallel, looking for a single control plane as totally separated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A data engineer at a German energy consultancy put it well:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Kafka resource costs should not be taken for granted. Maybe I need sub-second latency, or maybe daily is enough. These are considerations you must make up front, before it's too late."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The solution is to change how to &lt;em&gt;use&lt;/em&gt; Kafka: guardrails at topic creation, retention defaults that reflect reality, and ownership at the source. We typically see 25-40% of infrastructure costs come back this way.&lt;/p&gt;

&lt;p&gt;We wrote up the full analysis in &lt;a href="https://www.conduktor.io/blog/your-platform-team-cant-fix-kafka-costs-alone" rel="noopener noreferrer"&gt;Your Platform Team Can't Fix Kafka Costs Alone&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Everyone wants self-service. Nobody has the operating model for it.
&lt;/h2&gt;

&lt;p&gt;Self-service provisioning is a common topic now. Developers want to create topics and request permissions without filing a Jira ticket (many still do). Platform teams want to provide it to not be a bottleneck. The &lt;em&gt;how&lt;/em&gt; is the question. Home-made is time consuming considering all variations, and using a vendor, it has to be flexible to adopt.&lt;/p&gt;

&lt;p&gt;A senior engineer at a Danish financial firm told us he was too busy managing tickets to build the system that would have made the tickets unnecessary. An architect at a UK telecom had no self-service at all, just Jira. A senior developer at a UK bank said her biggest frustration was waiting on the ticketing system.&lt;/p&gt;

&lt;p&gt;This is no joke. Self-service &lt;em&gt;resource provisioning&lt;/em&gt; is a capability and &lt;strong&gt;Federated ownership&lt;/strong&gt; is what produces it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform team sets standards and guardrails.&lt;/li&gt;
&lt;li&gt;Domain teams operate within them autonomously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The teams that have this working got the operating model right before building the tooling. You need to slow down to accelerate better. Everyone else stopped at "we can create topics, we have gitops", they think this is enough and are missing the whole point. This is like 5% of a real solution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The key thing for us is approval gates. Guardrails. If somebody adds a thousand partitions, we need someone to have eyes on that." — Lead streaming data engineer at a major African bank&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Self-service without federated ownership becomes either a free-for-all nobody can govern, or a tightly controlled environment nobody uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Nobody knows what a Kafka proxy actually does
&lt;/h2&gt;

&lt;p&gt;This was the surprise. Kafka proxies came up in more conversations than any other architectural topic, but most people still think a proxy does one thing: route traffic for legacy clients.&lt;/p&gt;

&lt;p&gt;An engineer at a European ISP runs one in production. It handles message routing. We asked about encryption, masking, or transformation. "We see a proxy as more of a routing tool."&lt;/p&gt;

&lt;p&gt;Here's the range of what serious Kafka proxies like Conduktor Gateway can do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt;: bridging legacy or non-native clients (most common)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DR and migration&lt;/strong&gt;: failover and provider switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: encryption and masking at the message level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full policy enforcement&lt;/strong&gt;: aliasing, schema validation, virtual clusters, field-level encryption, audit, and access control&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A VP at a global bank walked us through his checklist for a Kafka gateway: topic aliasing for migration, payload validation, virtual clusters for multi-tenant isolation, field-level encryption, data masking, and audit. It's a bank requirements doc.&lt;/p&gt;

&lt;p&gt;Another engineer at a UK building society had built application-layer encryption for PII. He didn't know a proxy could do it at the message level, saving hours of per-application work. Plenty of teams are paying the same tax right now, doing it client-side and it's a pain to manage at scale.&lt;/p&gt;

&lt;p&gt;A proxy is where policy and control live when they don't belong in the cluster or the application. &lt;a href="https://www.conduktor.io/gateway/" rel="noopener noreferrer"&gt;Here's what a Kafka proxy actually does&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. AI on Kafka is an ownership problem, not a streaming problem
&lt;/h2&gt;

&lt;p&gt;The keynotes kept talking about agentic AI as the bottleneck. Kafka developers disagree:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Spinning up infrastructure and writing code is rarely the bottleneck in a full end-to-end business solution." — Staff engineer at a UK retailer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A senior engineer at a major social platform building on-call AI agents put it directly: "Building the AI agent isn't the difficult thing. The data needs to be in the right structure."&lt;/p&gt;

&lt;p&gt;A software engineer at a UK challenger bank has been feeding an AI agent context from ~40 microservices. "You have to give it the entire Java classes of all those microservices. That's usually when it starts hallucinating."&lt;/p&gt;

&lt;p&gt;The hallucination is a legibility problem. The bottleneck is data quality, which sits downstream of governance, which sits downstream of your operating model. Every prerequisite practitioners named like ownership, schema discipline, topic visibility, federated governance, is foundational plumbing, not AI tooling.&lt;/p&gt;

&lt;p&gt;The teams that will run AI on Kafka in 2027 are the teams getting their ownership and governance right in 2026.&lt;/p&gt;




&lt;p&gt;All these discussions are coming from people running Kafka in production at banks, telcos, retailers, and energy companies. Costs are too high and too complex to undersand, self-service needs an operating model, proxies are underused, and AI isn't a shortcut past the governance work you haven't done yet.&lt;/p&gt;

&lt;p&gt;If any of these hit home, the &lt;a href="https://www.conduktor.io/blog/" rel="noopener noreferrer"&gt;Conduktor blog&lt;/a&gt; goes deeper on each one.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>datastreaming</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Most Kafka Cost Calculators Miss This</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Mon, 25 May 2026 15:19:36 +0000</pubDate>
      <link>https://dev.to/conduktor/how-to-analyze-the-cost-of-kafka-2a4b</link>
      <guid>https://dev.to/conduktor/how-to-analyze-the-cost-of-kafka-2a4b</guid>
      <description>&lt;p&gt;Which side are you on: "This is just what Kafka costs at scale" or "We should switch to a cheaper Kafka provider"?&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://conduktor.io" rel="noopener noreferrer"&gt;Conduktor&lt;/a&gt;, our field team works inside Kafka environments that have been running for a long time. We see this: most Kafka teams are overpaying by 25 to 40 percent. Not because anyone did anything wrong, but because of how Kafka got built up over time.&lt;/p&gt;

&lt;p&gt;The cost drivers of Kafka are weirdly context-dependent: the infrastructure and the provider are a tiny part of the full picture. &lt;/p&gt;

&lt;p&gt;The "how" it's being used is the real question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five bad patterns eating budget
&lt;/h2&gt;

&lt;p&gt;Below is what see, the same patterns show up everywhere, and are the first things we work with our customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Partition overprovisioning
&lt;/h3&gt;

&lt;p&gt;"How many partitions?" is the most common question with Kafka. I heard last week someone telling me an org just defaults to "64". I was shocked. Not only providers may price per partitions, but from a Kafka point of view: this takes metadata and open files etc.&lt;/p&gt;

&lt;p&gt;Partitions depend on throughput and concurrency expected (consumer parallelism). If a 64-partitions topic is sitting in a cluster with barely no traffic, you're just losing money on all sides. Multiply by dozens or hundreds of topics at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Retention that makes no sense
&lt;/h3&gt;

&lt;p&gt;Long retention on topics that nobody reads past the last few hours. Do you need replay? Default is 7-day retention, but it's often applied uniformly, when some topics only need a couple of hours and others genuinely need weeks.&lt;/p&gt;

&lt;p&gt;Tips: when using compacted topics and/or Kafka streams (changelog etc.), data is being stored indefinitely, that can cause some security/regulations issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Let's spin up another cluster
&lt;/h3&gt;

&lt;p&gt;One-cluster-per-team was a reasonable isolation strategy a long time ago. We saw this multiple times, more than 500 clusters, with tons of mirroring to share data. Throwing money down the drain.&lt;/p&gt;

&lt;p&gt;You're paying for underutilized clusters instead of consolidating onto fewer well-managed ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Zombie topics
&lt;/h3&gt;

&lt;p&gt;Topics created for experiments, migrations, or one-off tests that were never cleaned up. It's a simple thing but cost so much money as no one is looking. Every one of them is replicated and has retention costs. We've seen enterprises with hundreds of zombie topics, who were so surprised when we showed them.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Runaway egress
&lt;/h3&gt;

&lt;p&gt;We had a customer where egress was running 30x higher than ingress on a single topic because of a misconfigured consumer. Buggy consumers, unnecessary fan-out, and chatty clients create traffic patterns that are invisible without dedicated infra monitoring. Egress is rarely free.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to deal with it
&lt;/h2&gt;

&lt;p&gt;Pick your starting point based on where the waste is concentrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop the bleeding: better defaults
&lt;/h3&gt;

&lt;p&gt;Low-coordination work that pays off over time. It's better to have exceptions rather than wrong defaults you can't rollback.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set sensible low partition defaults (3) and short retention (1 day). Increase if necessary only. &lt;/li&gt;
&lt;li&gt;Enforce client-side compression. (Conduktor Gateway)&lt;/li&gt;
&lt;li&gt;Require ownership metadata at topic creation. (Conduktor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This won't reduce your bill right away, but it will prevent it from getting worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trim the fat: optimize what's running
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tune retention where it's drifted, analyze consumer patterns.&lt;/li&gt;
&lt;li&gt;Retire topics with no active producers or consumers.&lt;/li&gt;
&lt;li&gt;Right-size partition counts (this is the hard one, since it means recreating topics and coordinating with every producer and consumer). - Consolidate Kafka clusters, introduce multi-tenancy (Conduktor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This work easily moves the infrastructure bill, we saw reductions of $500k just doing this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now, keep it clean, be disciplined
&lt;/h2&gt;

&lt;p&gt;After a cleanup, the same "drift" will start operating again.&lt;/p&gt;

&lt;p&gt;To help you keeping the direction, have absolute visibility into what you Kafka ecosystems contains and what it costs (&lt;a href="https://conduktor.io/blog/chargeback-attribute-map-kafka-costs-to-your-business" rel="noopener noreferrer"&gt;chargeback&lt;/a&gt; is powerful for this), clear ownership so every topic and cluster has a team accountable for it, and a regular review cadence to catch drift before it becomes permanent. Not heavyweight governance. Just enough discipline that the cleanup doesn't have to be repeated every year.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;The diagnostic question is simple: which of these patterns are present in your environment, and what are they costing you?&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://conduktor.io/blog/a-better-conversation-about-kafka-costs" rel="noopener noreferrer"&gt;original deep-dive&lt;/a&gt; goes further into the four layers of Kafka cost (infrastructure, ecosystem tooling, vendor/licensing, and operational) and includes a framework for sequencing the work.&lt;/p&gt;

&lt;p&gt;If you want to look at your own estate, Conduktor's field team does a &lt;a href="https://conduktor.io/contact/demo" rel="noopener noreferrer"&gt;free cost analysis&lt;/a&gt; where they walk through your environment with you and give you concrete numbers.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>datastreaming</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Rebuilt GPT-Researcher in Claude Code. Here's What I Learned.</title>
      <dc:creator>Stéphane Derosiaux</dc:creator>
      <pubDate>Wed, 31 Dec 2025 13:25:39 +0000</pubDate>
      <link>https://dev.to/sderosiaux/1500-lines-of-markdown-vs-15000-lines-of-python-5bac</link>
      <guid>https://dev.to/sderosiaux/1500-lines-of-markdown-vs-15000-lines-of-python-5bac</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The best orchestrator might be the one you don't have to build. Claude Code is already an orchestrator. Stop building infrastructure around it.&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Fw9zuwaf1mbyfhu1hiy2t.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.amazonaws.com%2Fuploads%2Farticles%2Fw9zuwaf1mbyfhu1hiy2t.png" alt="orchestration overhead" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent a weekend studying &lt;a href="https://github.com/assafelovic/gpt-researcher" rel="noopener noreferrer"&gt;GPT-Researcher&lt;/a&gt;, an open-source project with 24,000+ GitHub stars. It builds an autonomous research agent that generates comprehensive reports with citations. The architecture is elegant: multiple specialized agents coordinate through LangGraph, parallel execution speeds up research, and quality gates ensure reliable output.&lt;/p&gt;

&lt;p&gt;It uses LLM calls to decide which agent to run. It uses LLM calls to generate sub-queries. It uses LLM calls to select tools. It uses LLM calls to coordinate parallel work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're wrapping LLMs in infrastructure to teach them orchestration... when they can already orchestrate.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestration overhead
&lt;/h2&gt;

&lt;p&gt;Consider a typical agent workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM API call to analyze the task&lt;/li&gt;
&lt;li&gt;LLM API call to plan the approach&lt;/li&gt;
&lt;li&gt;LLM API call to select tools&lt;/li&gt;
&lt;li&gt;LLM API call to execute (finally, the actual work)&lt;/li&gt;
&lt;li&gt;LLM API call to verify results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four out of five calls are orchestration overhead. The LLM produces structured outputs (JSON) that code interprets to decide... what to ask the LLM next and passing it the right context.&lt;/p&gt;

&lt;p&gt;This pattern is everywhere. LangChain and LangGraph provide graph-based workflows. AutoGen from Microsoft enables multi-agent conversations. CrewAI offers role-based agent coordination, used by Oracle, PwC, and NVIDIA. Each framework solves real problems: managing state, coordinating agents, handling failures.&lt;/p&gt;

&lt;p&gt;But they all share the same assumption: the LLM needs infrastructure to orchestrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GPT-Researcher does well
&lt;/h2&gt;

&lt;p&gt;Credit where it's due: GPT-Researcher is excellent. According to its maintainers, it outperforms Perplexity, OpenAI's research tools, and other systems in benchmarks on citation quality, report quality, and information coverage.&lt;/p&gt;

&lt;p&gt;The architecture is sophisticated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent roles&lt;/strong&gt;: Chief Editor orchestrates the process. Researchers investigate subtopics. Editors plan structure. Reviewers validate quality. Revisers incorporate feedback. Writers compile reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel execution&lt;/strong&gt;: Research happens concurrently across subtopics. Multiple retrievers (Tavily, Google, Bing) run in parallel. Web scraping is asynchronous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality gates&lt;/strong&gt;: Review cycles catch errors. Revision loops improve output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The architecture tax
&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.amazonaws.com%2Fuploads%2Farticles%2F1cjj81ph62n01di3jllw.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.amazonaws.com%2Fuploads%2Farticles%2F1cjj81ph62n01di3jllw.png" alt="all the steps from query to report" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what the orchestration layer requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# agent_creator.py - LLM decides which agent to use
response = await llm.call(
    "Analyze this query and return JSON with agent type..."
)
agent_type = parse_json(response)  # error handling, retries

# query_processing.py - LLM generates sub-queries
response = await llm.call(
    "Generate search queries for this task..."
)
queries = parse_list(response)  # more parsing, more error handling

# tool_selector.py - LLM selects MCP tools
response = await llm.call(
    "Select relevant tools from this list..."
)
tools = parse_tool_selection(response)  # yet more parsing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step requires prompt engineering, output parsing, error handling, and retry logic. The orchestration layer is substantial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code's native capabilities
&lt;/h2&gt;

&lt;p&gt;Here's what Claude Code provides out of the box:&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.amazonaws.com%2Fuploads%2Farticles%2F0w6lqo727qrqtmpclc6m.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.amazonaws.com%2Fuploads%2Farticles%2F0w6lqo727qrqtmpclc6m.png" alt="claude code capabilities" width="649" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code doesn't need an LLM call to decide what tools to use. It IS the LLM. It reasons about the task and uses tools directly, in the same context, without round-trips.&lt;/p&gt;

&lt;p&gt;When you ask Claude Code to research a topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It analyzes the query (no separate LLM call)&lt;/li&gt;
&lt;li&gt;It generates sub-queries (no separate LLM call)&lt;/li&gt;
&lt;li&gt;It executes parallel searches (native Task agents)&lt;/li&gt;
&lt;li&gt;It synthesizes results (no separate LLM call)&lt;/li&gt;
&lt;li&gt;It writes the report (native Write tool)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What required infrastructure now requires prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rewrite
&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.amazonaws.com%2Fuploads%2Farticles%2Fxt21recd4tgksz1xae2r.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.amazonaws.com%2Fuploads%2Farticles%2Fxt21recd4tgksz1xae2r.png" alt="how orchestrate with Claude Code" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created Claude Researcher to test this. It's not a Python package. It's four commands and one skill file.&lt;/p&gt;

&lt;p&gt;Commands:&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.amazonaws.com%2Fuploads%2Farticles%2Fd9ghovcytzh34g4ejgun.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.amazonaws.com%2Fuploads%2Farticles%2Fd9ghovcytzh34g4ejgun.png" alt="the 4 claude code commands" width="400" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/research-team&lt;/code&gt; command implements the full multi-agent pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code (Chief Editor)
      │
      ├── [PARALLEL] Research Agent 1 → findings
      ├── [PARALLEL] Research Agent 2 → findings
      └── [PARALLEL] Research Agent 3 → findings
              ↓
         Draft Report
              ↓
         Reviewer Agent → feedback
              ↓
         Reviser Agent → improved draft
              ↓ (repeat until quality gate passes)
         Final Report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quality gates are defined in the command file. Review cycles repeat until scores meet thresholds. The multi-agent patterns from GPT-Researcher, expressed as instructions rather than code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The difference in sub-query generation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-Researcher:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt = f"""Write {max_iterations} google search queries...
You must respond with a list of strings in the following format: [{example}].
The response should contain ONLY the list."""

response = await llm.call(prompt)
queries = json.loads(response)  # parsing, error handling, retries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Claude Researcher:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate 5 search queries to research: "{query}"
- Each query should explore a different angle
- Include queries for recent information when relevant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code executes the queries directly. No parsing layer. No error handling for malformed output. The LLM produces the queries and uses them in the same context.&lt;/p&gt;

&lt;h2&gt;
  
  
  When orchestration frameworks make sense
&lt;/h2&gt;

&lt;p&gt;This isn't a claim that frameworks are useless. They solve real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production systems with strict SLAs. When you need guaranteed response formats, retry logic, circuit breakers, and observability, frameworks provide battle-tested infrastructure. Claude Code is conversational, not transactional.&lt;/li&gt;
&lt;li&gt;Non-Claude environments. If you're building on GPT-4, Gemini, or open-source models, Claude Code isn't available. Frameworks provide the coordination layer those environments lack.&lt;/li&gt;
&lt;li&gt;Complex state machines. Research is relatively linear: gather, synthesize, write. Workflows with branching logic, human-in-the-loop steps, or long-running state benefit from explicit orchestration.&lt;/li&gt;
&lt;li&gt;Team standardization. Frameworks enforce patterns. When multiple developers build agents, shared infrastructure ensures consistency. Markdown commands are flexible but less structured.&lt;/li&gt;
&lt;li&gt;Audit requirements. Enterprise deployments often need detailed logs of every decision. Frameworks with explicit orchestration make this easier than conversational interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question isn't "frameworks vs no frameworks". It's "do you need the framework for THIS task?"&lt;/p&gt;

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

&lt;p&gt;Clone the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/sderosiaux/claude-researcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy to your Claude Code config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cp commands/_*.md ~/.claude/commands/
cp skills/researcher.md ~/.claude/skills/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a research task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/research "Impact of AI agents on software development" --depth=deep

/research-team "Comparison of vector databases" --quality=high

/lookup "What is Claude Opus 4.5 context window?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No pip install. No API keys beyond what Claude Code already uses. No configuration files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code IS the Orchestrator, a really good one
&lt;/h2&gt;

&lt;p&gt;There's a pattern in software engineering: we build abstractions to solve problems, then build abstractions to manage our abstractions. Each layer adds capability but also complexity, configuration, and cognitive load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the base layer already does what I need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is an LLM with native tool access, parallel execution, and context management. GPT-Researcher is infrastructure that makes LLMs do those things. For research tasks, the native capabilities are sufficient.&lt;/p&gt;

&lt;p&gt;The best orchestrator might be the one you don't have to build.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
