<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>What happens when companies become too AI-pilled?</title>
      <dc:creator>Judy</dc:creator>
      <pubDate>Sat, 27 Jun 2026 01:00:26 +0000</pubDate>
      <link>https://dev.to/judy_miranttie/what-happens-when-companies-become-too-ai-pilled-27fo</link>
      <guid>https://dev.to/judy_miranttie/what-happens-when-companies-become-too-ai-pilled-27fo</guid>
      <description>&lt;p&gt;&lt;em&gt;This article is a deep-dive from JudyAI Lab — an AI engineering playbook series with 100+ published guides, 5,000+ weekly readers across 60+ countries, focused on the practical side of running AI agents, trading systems, and content pipelines in production.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📰 TL;DR
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Box founder Aaron Levie recently called out what he calls "AI psychosis" — the phenomenon where executives who greenlight "AI can replace this job" are often the ones who know the least about what that job actually entails. He's warning that this decision-making blind spot is spreading across tech — decision-makers, overconfident in AI's potential, are rushing to implement mass layoffs without truly understanding workflows, role nuances, or the human judgment required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a concrete case, collaboration platform ClickUp just announced cutting 22% of its workforce, explicitly stating it will use AI Agents to take over those functions. This wave of layoffs has pushed 2026's total tech layoffs to nearly match all of 2025 — before even reaching the mid-year point — showing AI-driven workforce reduction is accelerating.&lt;/p&gt;

&lt;p&gt;Levie's core argument isn't against AI adoption, but rather a warning: companies rushing to replace human labor with AI lack deep understanding of the work itself. When decision-makers aren't familiar with the actual complexity of the roles being replaced, they tend to overestimate AI's real coverage capability, ultimately hurting organizational efficiency. This "over-AI'd" thinking is becoming a new management risk in Silicon Valley. Full interview available at the source link.&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 JudyAI Lab Take
&lt;/h2&gt;

&lt;p&gt;The "AI psychosis" Levie pointed out is a red flag worth every AI implementer paying attention to: the execs loudest about "AI can replace this job" are often the ones most unfamiliar with that role — that's the real decision blind spot.&lt;/p&gt;

&lt;p&gt;ClickUp's 22% headcount reduction replacing roles with AI Agents has already pushed 2026 tech layoffs near matching all of 2025 — before midyear. There's a wake-up call for the AI builder community here: the环节 where automation design fails most isn't tech selection, but insufficient understanding of the "work being automated." When designing Agent flows or workflows, skipping deep interviews with actual executors easily leads to overestimating AI's coverage — automating visible steps while missing a lot of implicit human judgment and exception handling. Levie's critique is essentially a requirements analysis problem: not understanding the true complexity of a job means more AI investment can lead to bigger organizational efficiency losses.&lt;/p&gt;

&lt;p&gt;Before planning any AI replacement plan, talk to the people actually doing that job first. Ask them "what do you know that nobody else knows?" — that's often exactly where AI falls shortest.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://techcrunch.com/video/what-happens-when-companies-become-too-ai-pilled/" rel="noopener noreferrer"&gt;What happens when companies become too AI-pilled? | TechCrunch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://finance.yahoo.com/sectors/technology/articles/happens-companies-become-too-ai-175705787.html" rel="noopener noreferrer"&gt;What happens when companies become too AI-pilled?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.onlycfo.io/p/is-my-team-ai-pilled" rel="noopener noreferrer"&gt;Is My Company "AI Pilled"?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://judyailab.com/en/posts/ai-news-20260530-what-happens-when-companies-become-too-ai-pilled/" rel="noopener noreferrer"&gt;Judy AI Lab&lt;/a&gt;. Visit for more articles on AI engineering and development.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>media</category>
    </item>
    <item>
      <title>Why Enterprise AI Needs Structured Dissent, Not Just More Agents</title>
      <dc:creator>Amit Kumar Singh</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:53:39 +0000</pubDate>
      <link>https://dev.to/amising6/why-enterprise-ai-needs-structured-dissent-not-just-more-agents-5cn</link>
      <guid>https://dev.to/amising6/why-enterprise-ai-needs-structured-dissent-not-just-more-agents-5cn</guid>
      <description>&lt;p&gt;Many AI projects today are presented as multi-agent systems.&lt;/p&gt;

&lt;p&gt;One agent investigates. Another agent analyzes risk. A third agent checks compliance. A fourth agent gives a recommendation.&lt;/p&gt;

&lt;p&gt;It sounds advanced.&lt;/p&gt;

&lt;p&gt;But in a bank, adding more agents does not automatically make a workflow safe.&lt;/p&gt;

&lt;p&gt;A bank cannot freeze a customer account, block a payment, file a regulatory report, or label a transaction as fraud simply because an AI system produced a confident answer.&lt;/p&gt;

&lt;p&gt;The real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many AI agents are involved?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the system show evidence, challenge its own conclusion, apply deterministic rules, and stop for human approval when the decision is high impact?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the difference between an interesting multi-agent demo and an enterprise-ready AI workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A banking example: suspicious wire transfer
&lt;/h2&gt;

&lt;p&gt;Imagine a bank detects a wire transfer for $250,000.&lt;/p&gt;

&lt;p&gt;The payment is unusual because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The customer has never sent a transfer of this size.&lt;/li&gt;
&lt;li&gt;The destination account is in a new country.&lt;/li&gt;
&lt;li&gt;The transaction happens outside the customer’s normal business hours.&lt;/li&gt;
&lt;li&gt;The beneficiary was added only a few minutes before the transfer.&lt;/li&gt;
&lt;li&gt;The customer recently changed their phone number and email address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple AI chatbot might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This transaction looks suspicious. Consider blocking it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;A bank needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which transaction patterns triggered the concern?&lt;/li&gt;
&lt;li&gt;Is the customer actually violating a known risk threshold?&lt;/li&gt;
&lt;li&gt;Is there a sanctions or AML issue?&lt;/li&gt;
&lt;li&gt;Could this be a legitimate business payment?&lt;/li&gt;
&lt;li&gt;What policy applies?&lt;/li&gt;
&lt;li&gt;Should the payment be blocked, held, or released?&lt;/li&gt;
&lt;li&gt;Who is allowed to make that decision?&lt;/li&gt;
&lt;li&gt;Can the bank explain the decision later to auditors, compliance teams, and the customer?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where structured multi-agent design matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better design: a banking fraud decision room
&lt;/h2&gt;

&lt;p&gt;Instead of letting one model make a decision, the bank can create a controlled workflow with specialized agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction Alert
      ↓
Fraud Detection Agent
      ↓
Customer Behavior Agent
      ↓
AML / Sanctions Agent
      ↓
Policy and Risk Agent
      ↓
Decision Reviewer
      ↓
Human Compliance Officer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent has a limited responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Fraud Detection Agent
&lt;/h3&gt;

&lt;p&gt;This agent analyzes transaction behavior.&lt;/p&gt;

&lt;p&gt;It may identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unusual payment amount&lt;/li&gt;
&lt;li&gt;New beneficiary&lt;/li&gt;
&lt;li&gt;New country&lt;/li&gt;
&lt;li&gt;Unusual transaction time&lt;/li&gt;
&lt;li&gt;Sudden profile changes&lt;/li&gt;
&lt;li&gt;Prior fraud indicators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its job is not to freeze the transaction.&lt;/p&gt;

&lt;p&gt;Its job is to create a structured fraud signal.&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;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FRAUD_SIGNAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TXN-784921"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUST-10048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk_indicators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"new_beneficiary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"amount_12x_customer_average"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"unusual_country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"recent_contact_change"&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;"risk_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.88&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;This gives the next stage a reviewable artifact instead of a paragraph generated by an LLM.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Customer Behavior Agent
&lt;/h3&gt;

&lt;p&gt;A transaction may look suspicious but still be legitimate.&lt;/p&gt;

&lt;p&gt;For example, a corporate customer may be making a valid acquisition payment or paying a new overseas vendor.&lt;/p&gt;

&lt;p&gt;The Customer Behavior Agent looks at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical payment behavior&lt;/li&gt;
&lt;li&gt;Customer segment&lt;/li&gt;
&lt;li&gt;Typical payment ranges&lt;/li&gt;
&lt;li&gt;Known business relationships&lt;/li&gt;
&lt;li&gt;Recent support interactions&lt;/li&gt;
&lt;li&gt;Whether the customer informed the bank about a major payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This agent can produce a counterpoint:&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;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUSTOMER_CONTEXT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TXN-784921"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"historical_pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Outside normal range"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"known_business_event"&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 supporting event found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customer_contacted_bank"&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;"assessment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transaction behavior remains inconsistent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.76&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;This is important because the system should not treat every unusual payment as fraud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured dissent is necessary
&lt;/h2&gt;

&lt;p&gt;Now imagine the fraud agent recommends blocking the payment.&lt;/p&gt;

&lt;p&gt;A good enterprise workflow should not simply accept that recommendation.&lt;/p&gt;

&lt;p&gt;It should require another role to challenge it.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Fraud Agent says: “High fraud risk.”&lt;/li&gt;
&lt;li&gt;The Customer Context Agent says: “No evidence of a legitimate business event.”&lt;/li&gt;
&lt;li&gt;The AML Agent says: “Beneficiary has elevated geographic risk.”&lt;/li&gt;
&lt;li&gt;The Policy Agent says: “The bank’s hold threshold is met.”&lt;/li&gt;
&lt;li&gt;The Decision Reviewer says: “Human approval required before blocking.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is structured dissent.&lt;/p&gt;

&lt;p&gt;It is not about making agents argue for entertainment.&lt;/p&gt;

&lt;p&gt;It is about making assumptions visible before the bank takes action.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In high-stakes workflows, disagreement is not a weakness. Hidden disagreement is the real risk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The LLM should not make the final decision alone
&lt;/h2&gt;

&lt;p&gt;LLMs are useful for many parts of the workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Summarizing transaction history&lt;/li&gt;
&lt;li&gt;Explaining why a transaction appears unusual&lt;/li&gt;
&lt;li&gt;Reading customer notes&lt;/li&gt;
&lt;li&gt;Interpreting investigation findings&lt;/li&gt;
&lt;li&gt;Drafting a case narrative&lt;/li&gt;
&lt;li&gt;Generating a compliance-review summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But an LLM should not control deterministic rules.&lt;/p&gt;

&lt;p&gt;For example, these should come from governed systems and rules engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily transaction thresholds&lt;/li&gt;
&lt;li&gt;Sanctions screening results&lt;/li&gt;
&lt;li&gt;AML policy conditions&lt;/li&gt;
&lt;li&gt;Regulatory filing timelines&lt;/li&gt;
&lt;li&gt;Customer account restrictions&lt;/li&gt;
&lt;li&gt;Approval authority limits&lt;/li&gt;
&lt;li&gt;Payment-hold policies&lt;/li&gt;
&lt;li&gt;Risk score calculations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A safe architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Layer
- Investigates
- Summarizes
- Explains
- Recommends

Rules Layer
- Calculates thresholds
- Applies risk policies
- Checks sanctions lists
- Enforces approval limits
- Determines required escalation

Human Layer
- Approves
- Rejects
- Overrides
- Requests further investigation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;The AI can explain why a payment looks suspicious.&lt;/p&gt;

&lt;p&gt;The rules engine can determine whether the bank’s fraud-hold threshold has been crossed.&lt;/p&gt;

&lt;p&gt;The compliance officer can decide whether the payment should actually be blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  An evidence panel is more important than a chatbot answer
&lt;/h2&gt;

&lt;p&gt;The final decision should not be a black-box score.&lt;/p&gt;

&lt;p&gt;A compliance officer should see an evidence panel like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction:
TXN-784921

Customer:
Corporate customer — existing account for 4 years

Amount:
$250,000

Risk indicators:
- New beneficiary
- New destination country
- Payment amount is 12x normal average
- Contact information changed within past 24 hours
- No matching historical vendor relationship

Policy checks:
- Enhanced review threshold: Triggered
- Manual compliance approval: Required
- Sanctions screening: Clear
- AML monitoring alert: Triggered

AI assessment:
High-risk transaction requiring manual review

Human decision:
Payment placed on temporary hold

Approved by:
Compliance Officer

Decision timestamp:
2026-06-26 14:22 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what enterprise AI should produce.&lt;/p&gt;

&lt;p&gt;Not just an answer.&lt;/p&gt;

&lt;p&gt;A decision record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human approval is part of the architecture
&lt;/h2&gt;

&lt;p&gt;Human approval should not be added as an afterthought.&lt;/p&gt;

&lt;p&gt;In banking, some actions should be automated.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;AI / system role&lt;/th&gt;
&lt;th&gt;Human role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Summarize alert&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Review if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identify unusual transaction patterns&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Review exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create investigation case&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Place temporary low-risk review hold&lt;/td&gt;
&lt;td&gt;Rule-based&lt;/td&gt;
&lt;td&gt;Review later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freeze account&lt;/td&gt;
&lt;td&gt;Recommend only&lt;/td&gt;
&lt;td&gt;Explicit approval required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File SAR or regulatory report&lt;/td&gt;
&lt;td&gt;Draft supporting evidence&lt;/td&gt;
&lt;td&gt;Compliance approval required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close customer account&lt;/td&gt;
&lt;td&gt;Never autonomous&lt;/td&gt;
&lt;td&gt;Senior human decision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The system should know when to proceed, when to pause, and when to escalate.&lt;/p&gt;

&lt;p&gt;That is not a limitation.&lt;/p&gt;

&lt;p&gt;That is good enterprise design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for data engineering teams
&lt;/h2&gt;

&lt;p&gt;This same pattern applies directly to data engineering.&lt;/p&gt;

&lt;p&gt;A data-engineering copilot should not only generate SQL or YAML from a source-to-target mapping document.&lt;/p&gt;

&lt;p&gt;It should operate as a governed workflow.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STTM / DDL / Source Metadata
          ↓
Metadata Extraction Agent
          ↓
Mapping Validation Agent
          ↓
Transformation Logic Agent
          ↓
SQL / YAML Generator
          ↓
Reviewer Agent
          ↓
Data Engineer Approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reviewer should validate things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the source column exist?&lt;/li&gt;
&lt;li&gt;Is the target data type compatible?&lt;/li&gt;
&lt;li&gt;Is the join supported by the mapping?&lt;/li&gt;
&lt;li&gt;Is the transformation rule documented?&lt;/li&gt;
&lt;li&gt;Is a sign rule missing?&lt;/li&gt;
&lt;li&gt;Is a derived metric using an unapproved assumption?&lt;/li&gt;
&lt;li&gt;Are there duplicate or unused YAML objects?&lt;/li&gt;
&lt;li&gt;Has an engineer approved the generated output?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then every generated artifact should include traceability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Target Column:
PROFIT_AMT

Source:
sales.PROFIT_AMT

Transformation:
CASE WHEN SALES_TYPE = 'CANCEL'
THEN PROFIT_AMT* -1
ELSE PROFIT_AMT
END

Business Rule:
Cancellation transactions must store Profit as negative.

Source Reference:
STTM row 42

Validation:
- Source column exists
- Transformation approved
- Target data type compatible
- Human review status: Approved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how generated code becomes a governed engineering artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist for enterprise AI
&lt;/h2&gt;

&lt;p&gt;Before calling a multi-agent system enterprise-ready, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does each agent have a clear responsibility?&lt;/li&gt;
&lt;li&gt;Are handoffs structured instead of free-text only?&lt;/li&gt;
&lt;li&gt;Can one agent challenge another agent’s conclusion?&lt;/li&gt;
&lt;li&gt;Are critical calculations and policy checks deterministic?&lt;/li&gt;
&lt;li&gt;Can every recommendation be traced to source evidence?&lt;/li&gt;
&lt;li&gt;Does the system show assumptions and confidence levels?&lt;/li&gt;
&lt;li&gt;Is there a clear escalation path for uncertainty?&lt;/li&gt;
&lt;li&gt;Can a human approve, reject, or override the decision?&lt;/li&gt;
&lt;li&gt;Can the organization reconstruct the full decision later?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer is no, the solution may still be a useful prototype.&lt;/p&gt;

&lt;p&gt;But it is not ready for high-stakes enterprise use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The future of enterprise AI is not one intelligent assistant making every decision.&lt;/p&gt;

&lt;p&gt;It is also not a collection of agents talking continuously.&lt;/p&gt;

&lt;p&gt;The future is a governed decision system where AI helps teams investigate faster, compare perspectives, identify risk, and prepare recommendations.&lt;/p&gt;

&lt;p&gt;But evidence remains visible.&lt;/p&gt;

&lt;p&gt;Rules remain enforceable.&lt;/p&gt;

&lt;p&gt;Disagreement remains allowed.&lt;/p&gt;

&lt;p&gt;And people remain accountable.&lt;/p&gt;

&lt;p&gt;That is how AI becomes useful in banking, finance, data engineering, and other enterprise workflows where trust matters as much as speed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dataengineeringcopilot.com" rel="noopener noreferrer"&gt;https://dataengineeringcopilot.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/amising6/data-engineering-copilot" rel="noopener noreferrer"&gt;https://github.com/amising6/data-engineering-copilot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/amit-singh-57980030" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/amit-singh-57980030&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>I wanted a Go networking engine that gets out of the way, so I built one (Breeze).</title>
      <dc:creator>Farshad Khazaei Fard</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:49:42 +0000</pubDate>
      <link>https://dev.to/nelthaarion/i-wanted-a-go-networking-engine-that-gets-out-of-the-way-so-i-built-one-breeze-3jc</link>
      <guid>https://dev.to/nelthaarion/i-wanted-a-go-networking-engine-that-gets-out-of-the-way-so-i-built-one-breeze-3jc</guid>
      <description>&lt;p&gt;Over the past few months, I've been working on Breeze, a networking engine built on top of gnet.&lt;/p&gt;

&lt;p&gt;The goal wasn't to create "another web framework."&lt;/p&gt;

&lt;p&gt;The goal was to explore how far an event-loop architecture can go for modern Go services.&lt;/p&gt;

&lt;p&gt;Some design decisions I made:&lt;/p&gt;

&lt;p&gt;⚡ Event-loop driven architecture&lt;br&gt;
🌐 Native HTTP and WebSocket support&lt;br&gt;
🚀 WebSocket fast-path (avoids the HTTP router after the upgrade)&lt;br&gt;
🧵 Worker pool to keep the event loop responsive&lt;br&gt;
📦 Low-allocation request handling&lt;br&gt;
📚 Built-in Swagger support&lt;br&gt;
🔌 Built-in WebSocket Hub for real-time applications&lt;/p&gt;

&lt;p&gt;One design choice I'm particularly interested in discussing is that HTTP and WebSocket aren't treated the same.&lt;/p&gt;

&lt;p&gt;Every incoming connection is classified inside the event loop. HTTP requests follow the router, while upgraded WebSocket connections are dispatched directly to the WebSocket engine. It keeps the hot path small and avoids unnecessary work once the protocol is established.&lt;/p&gt;

&lt;p&gt;The project is still evolving, and I'm deliberately questioning every architectural decision before calling it "production ready."&lt;/p&gt;

&lt;p&gt;I'd genuinely appreciate feedback from developers who have experience with:&lt;/p&gt;

&lt;p&gt;High-concurrency Go servers&lt;br&gt;
gnet or event-loop architectures&lt;br&gt;
Large-scale WebSocket systems&lt;br&gt;
Low-latency backend services&lt;/p&gt;

&lt;p&gt;Repository:&lt;br&gt;
&lt;a href="https://github.com/nelthaarion/breeze" rel="noopener noreferrer"&gt;https://github.com/nelthaarion/breeze&lt;/a&gt;&lt;br&gt;
Documentation: &lt;br&gt;
&lt;a href="https://nelthaarion.github.io/breeze" rel="noopener noreferrer"&gt;https://nelthaarion.github.io/breeze&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm especially interested in hearing what you would change. Architecture discussions are often more valuable than benchmark numbers.&lt;/p&gt;

&lt;p&gt;Happy to answer any questions or dive into implementation details. 🚀&lt;/p&gt;

</description>
      <category>go</category>
      <category>networking</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Mastering the "Quantified Self": Building a Blazing-Fast Heart Rate Dashboard with DuckDB and Streamlit</title>
      <dc:creator>Beck_Moulton</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:44:00 +0000</pubDate>
      <link>https://dev.to/beck_moulton/mastering-the-quantified-self-building-a-blazing-fast-heart-rate-dashboard-with-duckdb-and-1eed</link>
      <guid>https://dev.to/beck_moulton/mastering-the-quantified-self-building-a-blazing-fast-heart-rate-dashboard-with-duckdb-and-1eed</guid>
      <description>&lt;p&gt;As programmers, we love data. We track our commits, our uptime, and our deployment frequencies. But what about our most important "server"—our heart? 💓&lt;/p&gt;

&lt;p&gt;The "Quantified Self" movement has led to an explosion of wearable data. However, if you've ever tried to analyze raw heart rate CSVs (often sampled every few seconds), you'll quickly realize that standard relational databases or even pure Pandas can get sluggish once you hit that 100k+ row mark. &lt;/p&gt;

&lt;p&gt;In this tutorial, we are going to build a high-performance &lt;strong&gt;Quantified Self Dashboard&lt;/strong&gt;. We will leverage &lt;strong&gt;DuckDB&lt;/strong&gt;—the "SQLite for Analytics"—to perform vectorized execution on heart rate data, paired with &lt;strong&gt;Streamlit&lt;/strong&gt; and &lt;strong&gt;Plotly&lt;/strong&gt; for a slick, interactive frontend. We’ll focus on &lt;strong&gt;Python data engineering&lt;/strong&gt;, &lt;strong&gt;time-series analysis&lt;/strong&gt;, and &lt;strong&gt;fast SQL processing&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why DuckDB? 🦆
&lt;/h2&gt;

&lt;p&gt;Traditional databases are row-based, which is great for transactions but terrible for analytical queries. DuckDB is a &lt;strong&gt;columnar-vectorized query engine&lt;/strong&gt;. This means it processes data in chunks (vectors) and utilizes modern CPU instructions (SIMD) to crunch numbers at speeds that make standard Python loops look like they're standing still.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;Here is how our data pipeline flows from raw pixels (well, raw CSV rows) to actionable insights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    A[Raw Heart Rate CSVs] --&amp;gt;|Direct Ingestion| B(DuckDB Engine)
    B --&amp;gt;|Vectorized SQL Execution| C{Data Aggregation}
    C --&amp;gt;|Moving Averages/Outliers| D[Streamlit App State]
    D --&amp;gt;|Plotly| E[Interactive Visualization]
    E --&amp;gt;|User Input| D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prerequisites 🛠️
&lt;/h2&gt;

&lt;p&gt;Ensure you have the following stack installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3.9+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt;: For the heavy lifting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamlit&lt;/strong&gt;: For the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plotly&lt;/strong&gt;: For the beautiful charts.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;duckdb streamlit plotly pandas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Ingesting 100,000+ Data Points in Milliseconds
&lt;/h2&gt;

&lt;p&gt;One of the coolest features of DuckDB is its ability to query CSV files directly without a formal "import" step. This is a game-changer for developer productivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="c1"&gt;# Let's assume 'heart_rate.csv' has columns: timestamp, bpm
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# DuckDB can read CSVs directly and infer types!
&lt;/span&gt;    &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# High-performance SQL query to aggregate data into 1-minute buckets
&lt;/span&gt;    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SELECT 
        time_bucket(INTERVAL &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1 minutes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, timestamp) AS time,
        AVG(bpm) AS avg_bpm,
        MAX(bpm) AS max_bpm
    FROM read_csv_auto(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
    GROUP BY 1
    ORDER BY 1
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;df&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Building the Interactive Dashboard
&lt;/h2&gt;

&lt;p&gt;Now, let's wrap this in &lt;strong&gt;Streamlit&lt;/strong&gt;. We want to calculate a &lt;strong&gt;Moving Average&lt;/strong&gt; to smooth out the noise from the sensor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plotly.express&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_page_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Heart Rate Analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🏃‍♂️ Quantified Self: Heart Rate Insights&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing 100k+ data points in real-time using **DuckDB**.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;uploaded_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file_uploader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Upload your heart rate CSV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uploaded_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Save the uploaded file temporarily
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temp_data.csv&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;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getbuffer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Query using DuckDB
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temp_data.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Add a moving average using Pandas (or do it in SQL for more speed!)
&lt;/span&gt;    &lt;span class="n"&gt;window_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Smoothing Window (minutes)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;smoothed_bpm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;avg_bpm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Create the Plotly Chart
&lt;/span&gt;    &lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;smoothed_bpm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Heart Rate Trend (Smoothed)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;smoothed_bpm&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;BPM&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;time&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;Time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_traces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ef4444&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plotly_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_container_width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Key Metrics
&lt;/span&gt;    &lt;span class="n"&gt;col1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Max HR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_bpm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; BPM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Avg HR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;avg_bpm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; BPM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data Points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The "Production" Way: Advanced Patterns 🥑
&lt;/h2&gt;

&lt;p&gt;While this setup is perfect for local analysis, scaling "Quantified Self" apps for production requires more robust data architecture. If you're interested in how to deploy these types of analytical apps at scale or want to see more advanced SQL optimization patterns for time-series data, I highly recommend checking out the &lt;strong&gt;&lt;a href="https://www.wellally.tech/blog" rel="noopener noreferrer"&gt;WellAlly Blog&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;They provide excellent deep dives into production-ready data engineering and have some fantastic resources on building performant monitoring systems that go far beyond basic CSV parsing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Why did we use DuckDB instead of standard Pandas? &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Pandas (Standard)&lt;/th&gt;
&lt;th&gt;DuckDB (Vectorized)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSV Ingestion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;0.15s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Group By Aggregation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;td&gt;0.04s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Footprint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Low (Streaming)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, DuckDB is consistently &lt;strong&gt;5-10x faster&lt;/strong&gt; for these analytical workloads. For a developer dashboard where you want instant feedback when sliding a filter, these milliseconds matter!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Take Back Your Data! 📊
&lt;/h2&gt;

&lt;p&gt;Building your own tools to visualize your health data is incredibly rewarding. By combining &lt;strong&gt;DuckDB's&lt;/strong&gt; speed with &lt;strong&gt;Streamlit's&lt;/strong&gt; ease of use, you've created a tool that can handle massive datasets on your laptop without breaking a sweat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try adding a SQL query to detect "Zone 5" training sessions.&lt;/li&gt;
&lt;li&gt;Use DuckDB's &lt;code&gt;JOIN&lt;/code&gt; capabilities to correlate your heart rate with your GitHub commit frequency!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed this tutorial, drop a comment below or share your own Quantified Self projects! And don't forget to visit &lt;strong&gt;&lt;a href="https://www.wellally.tech/blog" rel="noopener noreferrer"&gt;wellally.tech/blog&lt;/a&gt;&lt;/strong&gt; for more advanced engineering content. Happy coding! 💻🔥&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Why your prototype works for you but not for anyone else</title>
      <dc:creator>Saveyourproject</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:37:43 +0000</pubDate>
      <link>https://dev.to/saveyourproject/why-your-prototype-works-for-you-but-not-for-anyone-else-1fgl</link>
      <guid>https://dev.to/saveyourproject/why-your-prototype-works-for-you-but-not-for-anyone-else-1fgl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A prototype that works for you but breaks for everyone else usually isn't bad luck. It's four repeatable culprits: you designed for one assembly, your fasteners drift, the enclosure ignores real loads, and you never wrote down why it works. Fix those, and "works on my bench" becomes "works, period."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You built the thing, and it works. In your hands, on your bench, every single time.&lt;/p&gt;

&lt;p&gt;Then a friend tries it, or it sits in the garage a week, or the temperature drops one night, and it just stops.&lt;/p&gt;

&lt;p&gt;Frustrating doesn't really cover it.&lt;/p&gt;

&lt;p&gt;Here's the reassuring part: that gap between "works for me" and "works for anyone" is almost always the same small handful of culprits. You're not missing some secret skill. Once you've met them a few times, you start designing around them without even thinking about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. You built it for one. Now build it for two.
&lt;/h2&gt;

&lt;p&gt;That first one fit because &lt;em&gt;you&lt;/em&gt; were there — nudging, sanding, coaxing it together. The trouble is, all of that lived in your hands, not in the model.&lt;/p&gt;

&lt;p&gt;So the second copy fights you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you can't make a second one without the fiddling, it isn't done yet.&lt;/strong&gt; Bake the clearance into the CAD, then print one you promise not to touch up. That's the real test.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your fasteners are quietly betraying you.
&lt;/h2&gt;

&lt;p&gt;Press-fits creep. Hot glue lets go. Jumper wires back out. Double-sided tape taps out the first warm afternoon.&lt;/p&gt;

&lt;p&gt;I know the boring fixes aren't the fun part — a screw boss, a captive nut, a bit of strain relief, a connector that actually clicks home. But boring is exactly what's still holding a year from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The enclosure is a load, not a lid.
&lt;/h2&gt;

&lt;p&gt;It's easy to treat the box as an afterthought. But heat, dust, and vibration are real forces working on your build.&lt;/p&gt;

&lt;p&gt;A board that runs cool in the open can slowly cook once it's sealed up. A connector that's happy on the bench can buzz itself loose in a drawer that gets opened every day.&lt;/p&gt;

&lt;p&gt;Give the heat somewhere to go, mount the board instead of letting it dangle from its wires, and clamp down anything that moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write down &lt;em&gt;why&lt;/em&gt; it works.
&lt;/h2&gt;

&lt;p&gt;Future-you is begging you.&lt;/p&gt;

&lt;p&gt;Six months from now, v2 breaks and you won't remember which dimension was load-bearing, which resistor value you finally settled on, or why that one screw is longer than the rest.&lt;/p&gt;

&lt;p&gt;One page of "why" notes — just the decisions that actually mattered — turns a miserable teardown into a five-minute fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don't need a factory for any of this
&lt;/h2&gt;

&lt;p&gt;It's really just one step past "it works," and it's a step anyone can learn.&lt;/p&gt;

&lt;p&gt;(There's a whole other layer once you want to make &lt;em&gt;ten&lt;/em&gt; of something, where sourcing and repeatability change the math. But that's a problem for after this one.)&lt;/p&gt;

&lt;p&gt;So I'm curious: which of these four bites you the most? For me it's almost always #1 — designing for one, and forgetting the second one has to exist too.&lt;/p&gt;

&lt;p&gt;I originally posted this on my own site, if you'd like it in one place: &lt;a href="https://www.saveyourproject.com/blog/bench-to-real-use-reliability" rel="noopener noreferrer"&gt;https://www.saveyourproject.com/blog/bench-to-real-use-reliability&lt;/a&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>sideprojects</category>
      <category>testing</category>
    </item>
    <item>
      <title>Event-Driven Architecture: sistemi che reagiscono invece di chiedere</title>
      <dc:creator>Dev-Iadicola</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:34:20 +0000</pubDate>
      <link>https://dev.to/dev_iadicola/event-driven-architecture-sistemi-che-reagiscono-invece-di-chiedere-1hi6</link>
      <guid>https://dev.to/dev_iadicola/event-driven-architecture-sistemi-che-reagiscono-invece-di-chiedere-1hi6</guid>
      <description>&lt;h2&gt;
  
  
  Il problema: catene di chiamate rigide
&lt;/h2&gt;

&lt;p&gt;Quando un utente si registra, il sistema deve: inviare l'email di benvenuto, creare il profilo default, notificare il team di vendita, aggiornare le statistiche, attivare il periodo di prova. Il controller chiama cinque servizi in sequenza. Se aggiungi un sesto step (integrazione CRM), devi modificare il controller. Se il servizio email e lento, rallenta tutto. Se fallisce, blocca i passaggi successivi. Le dipendenze crescono linearmente con le funzionalita.&lt;/p&gt;

&lt;p&gt;L'&lt;strong&gt;architettura event-driven&lt;/strong&gt; inverte il flusso: il controller fa una sola cosa — registra l'utente e pubblica un evento &lt;code&gt;UserRegistered&lt;/code&gt;. Ogni componente interessato &lt;strong&gt;reagisce&lt;/strong&gt; all'evento indipendentemente. Il controller non sa quanti listener ci sono, ne cosa fanno. I listener non si conoscono tra loro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concetti fondamentali
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Evento: un fatto accaduto
&lt;/h3&gt;

&lt;p&gt;Un evento e un &lt;strong&gt;fatto immutabile&lt;/strong&gt; che descrive qualcosa che e già successo: &lt;code&gt;OrderPlaced&lt;/code&gt;, &lt;code&gt;PaymentReceived&lt;/code&gt;, &lt;code&gt;ArticlePublished&lt;/code&gt;. Non e una richiesta ("fai qualcosa") ma una notifica ("e successo qualcosa"). Questa distinzione e fondamentale: l'emittente non si aspetta una risposta e non sa chi sta ascoltando.&lt;/p&gt;

&lt;h3&gt;
  
  
  Producer e Consumer
&lt;/h3&gt;

&lt;p&gt;Il &lt;strong&gt;producer&lt;/strong&gt; emette l'evento. Il &lt;strong&gt;consumer&lt;/strong&gt; (listener/subscriber) reagisce. Un evento può avere zero, uno o molti consumer. Aggiungere un consumer non richiede modifiche al producer. Rimuoverne uno non rompe nulla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esempio teorico: ciclo di vita di un ordine
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OrderPlaced&lt;/code&gt; → &lt;code&gt;SendOrderConfirmationListener&lt;/code&gt; invia l'email, &lt;code&gt;ReserveInventoryListener&lt;/code&gt; blocca lo stock, &lt;code&gt;NotifyWarehouseListener&lt;/code&gt; prepara la spedizione, &lt;code&gt;UpdateDashboardListener&lt;/code&gt; aggiorna le metriche&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PaymentReceived&lt;/code&gt; → &lt;code&gt;ConfirmOrderListener&lt;/code&gt; aggiorna lo stato, &lt;code&gt;GenerateInvoiceListener&lt;/code&gt; crea la fattura&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OrderShipped&lt;/code&gt; → &lt;code&gt;SendShippingNotificationListener&lt;/code&gt; notifica il cliente, &lt;code&gt;StartDeliveryTrackingListener&lt;/code&gt; attiva il tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ogni listener e una classe isolata, testabile, che fa una sola cosa. Aggiungere un'integrazione con un nuovo sistema di analytics? Crei un listener, lo registri sull'evento, e tutto funziona senza toccare il codice esistente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sincrono vs Asincrono
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sincrono&lt;/strong&gt;: i listener vengono eseguiti nella stessa request. Semplice, ma se un listener e lento, rallenta la response. Adatto per operazioni veloci e critiche (validazione, aggiornamento stato).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asincrono&lt;/strong&gt;: i listener vengono accodati (Redis, RabbitMQ, database) e eseguiti da worker separati. Non rallenta la response. Adatto per operazioni lente (email, PDF, integrazioni esterne) o non critiche.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In pratica, la maggior parte dei sistemi usa un mix: alcuni listener sincroni per le operazioni critiche, altri asincroni per il resto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven in Soft PHP MVC
&lt;/h2&gt;

&lt;p&gt;Il framework usa eventi tramite l'Observer Pattern nei Model: i lifecycle hooks (&lt;code&gt;beforeSave&lt;/code&gt;, &lt;code&gt;afterSave&lt;/code&gt;, &lt;code&gt;beforeDelete&lt;/code&gt;) sono eventi sincroni che permettono di reagire ai cambiamenti delle entita. Il &lt;code&gt;CacheObserver&lt;/code&gt; invalida la cache quando un articolo viene modificato — senza che il Model sappia che la cache esiste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quando usare Event-Driven Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usa eventi&lt;/strong&gt; quando un'azione ha effetti collaterali che non sono responsabilità del componente che la esegue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usa eventi&lt;/strong&gt; quando vuoi che nuove funzionalita possano "agganciarsi" senza modificare il codice esistente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usa eventi&lt;/strong&gt; quando il disaccoppiamento tra componenti e una priorità&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non usare eventi&lt;/strong&gt; per flussi lineari semplici dove una chiamata diretta e più chiara&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non usare eventi&lt;/strong&gt; se il debugging e già difficile: gli eventi rendono il flusso meno tracciabile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'architettura event-driven non e un'alternativa a MVC o Hexagonal: e un principio di comunicazione che si applica dentro qualsiasi architettura. Quando i componenti reagiscono a fatti invece di essere chiamati direttamente, il sistema diventa più flessibile, più estensibile e più resiliente.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://iadicola.it/articoli/event-driven-architecture-sistemi-che-reagiscono?utm_source=devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;Leggi l'articolo completo su iadicola.it&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>evento</category>
      <category>disaccoppiamento</category>
    </item>
    <item>
      <title>I Tried Connecting an OpenAI-Compatible API to WordPress AI</title>
      <dc:creator>Ailles</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:33:22 +0000</pubDate>
      <link>https://dev.to/ailles/i-tried-connecting-an-openai-compatible-api-to-wordpress-ai-k9m</link>
      <guid>https://dev.to/ailles/i-tried-connecting-an-openai-compatible-api-to-wordpress-ai-k9m</guid>
      <description>&lt;p&gt;I was curious about something while testing WordPress AI features:&lt;/p&gt;

&lt;p&gt;What happens if the AI provider I want to use is not one of the default providers?&lt;/p&gt;

&lt;p&gt;WordPress AI can connect to supported providers directly, but many AI services today expose an &lt;strong&gt;OpenAI-compatible API&lt;/strong&gt; instead of being officially supported one by one.&lt;/p&gt;

&lt;p&gt;That includes things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenRouter&lt;/li&gt;
&lt;li&gt;OpenWebUI&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;LiteLLM&lt;/li&gt;
&lt;li&gt;other custom OpenAI-compatible endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, I assumed that if an API follows the OpenAI format, I could just paste the endpoint somewhere and call it a day.&lt;/p&gt;

&lt;p&gt;Turns out, not always.&lt;/p&gt;

&lt;p&gt;So I tested a bridge approach using a WordPress plugin called &lt;strong&gt;Koneek&lt;/strong&gt; to connect an OpenAI-compatible API provider to WordPress AI.&lt;/p&gt;

&lt;p&gt;Here is what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I wanted to test
&lt;/h2&gt;

&lt;p&gt;The question was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can WordPress AI use an OpenAI-compatible provider even if WordPress does not support that provider directly?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my test, I used &lt;strong&gt;OpenRouter&lt;/strong&gt; as the provider because it exposes an OpenAI-compatible endpoint and also has free models available.&lt;/p&gt;

&lt;p&gt;The goal was not to build a custom integration from scratch.&lt;/p&gt;

&lt;p&gt;I wanted to see whether WordPress AI could use a third-party compatible API through configuration only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenAI-compatible APIs are not always plug-and-play
&lt;/h2&gt;

&lt;p&gt;This part surprised me a little.&lt;/p&gt;

&lt;p&gt;Even when a provider says it is OpenAI-compatible, it does not mean every app can automatically use it.&lt;/p&gt;

&lt;p&gt;Different providers may use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a different base URL&lt;/li&gt;
&lt;li&gt;custom authentication behavior&lt;/li&gt;
&lt;li&gt;extra headers&lt;/li&gt;
&lt;li&gt;different model naming&lt;/li&gt;
&lt;li&gt;slightly different endpoint expectations&lt;/li&gt;
&lt;li&gt;different timeout behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So even if the request and response format is mostly compatible, WordPress still needs a way to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where to send the request&lt;/li&gt;
&lt;li&gt;which model to use&lt;/li&gt;
&lt;li&gt;which API key to send&lt;/li&gt;
&lt;li&gt;how long to wait for a response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the plugin comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I used
&lt;/h2&gt;

&lt;p&gt;For this test, I prepared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress 7.0&lt;/li&gt;
&lt;li&gt;the official WordPress &lt;strong&gt;&lt;a href="https://wordpress.org/plugins/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt;&lt;/strong&gt; plugin&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;&lt;a href="https://wordpress.org/plugins/koneek-multi-provider-ai-gateway/" rel="noopener noreferrer"&gt;Koneek – AI Provider for OpenAI-Compatible&lt;/a&gt;&lt;/strong&gt; plugin&lt;/li&gt;
&lt;li&gt;an OpenAI-compatible API key&lt;/li&gt;
&lt;li&gt;a model name&lt;/li&gt;
&lt;li&gt;the provider base URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For OpenRouter, the base URL I used was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://openrouter.ai/api/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the model, the original test used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cohere/north-mini-code:free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact model can change depending on what is available from your provider, so I would not hardcode that forever. Always check the provider's model list before configuring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup flow I tested
&lt;/h2&gt;

&lt;p&gt;After installing and activating the required plugins, I went into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings &amp;gt; Koneek
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, I added a new provider configuration.&lt;/p&gt;

&lt;p&gt;The fields were pretty straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: Any label you want
Provider: OpenAI-Compatible
API Key: Your provider API key
Model: The model name from your provider
API URL: The OpenAI-compatible base URL
Timeout: Optional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For OpenRouter, the important values looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider: OpenAI-Compatible
Model: cohere/north-mini-code:free
API URL: https://openrouter.ai/api/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I left the timeout empty at first because I wanted to see whether the default behavior was good enough.&lt;/p&gt;

&lt;p&gt;If the provider is slow or the model takes longer to respond, setting a custom timeout may be worth testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing before saving was the useful part
&lt;/h2&gt;

&lt;p&gt;One thing I liked about this workflow is that I did not have to blindly save the config and hope it worked.&lt;/p&gt;

&lt;p&gt;Before saving, I tested the connection.&lt;/p&gt;

&lt;p&gt;This is important because there are several small things that can break the setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid API key&lt;/li&gt;
&lt;li&gt;wrong base URL&lt;/li&gt;
&lt;li&gt;typo in the model name&lt;/li&gt;
&lt;li&gt;provider-side rate limit&lt;/li&gt;
&lt;li&gt;free model no longer available&lt;/li&gt;
&lt;li&gt;timeout too low&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, once the connection test returned successfully, I saved the configuration.&lt;/p&gt;

&lt;p&gt;That small test step saves a lot of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling WordPress AI features
&lt;/h2&gt;

&lt;p&gt;After the provider was connected through Koneek, the next step was enabling the AI features from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings &amp;gt; AI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part depends on the WordPress AI plugin configuration.&lt;/p&gt;

&lt;p&gt;Once enabled, WordPress AI could use the configured provider for features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rewriting text&lt;/li&gt;
&lt;li&gt;rephrasing content&lt;/li&gt;
&lt;li&gt;regenerating titles&lt;/li&gt;
&lt;li&gt;other editor AI actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the moment where the setup actually felt useful.&lt;/p&gt;

&lt;p&gt;The provider was not just saved somewhere in settings. It became usable inside WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked well
&lt;/h2&gt;

&lt;p&gt;The biggest win is flexibility.&lt;/p&gt;

&lt;p&gt;Instead of waiting for WordPress AI to support every provider directly, this setup makes it possible to connect providers that expose an OpenAI-compatible API.&lt;/p&gt;

&lt;p&gt;That opens the door to testing different backends, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hosted AI providers&lt;/li&gt;
&lt;li&gt;local AI gateways&lt;/li&gt;
&lt;li&gt;self-hosted OpenWebUI&lt;/li&gt;
&lt;li&gt;LiteLLM routers&lt;/li&gt;
&lt;li&gt;OpenRouter model routing&lt;/li&gt;
&lt;li&gt;internal proxy APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For developers, that is useful because AI providers change fast.&lt;/p&gt;

&lt;p&gt;Today you may want OpenRouter. Tomorrow you may want a local Ollama setup behind an OpenAI-compatible proxy.&lt;/p&gt;

&lt;p&gt;The WordPress side does not need to care too much as long as the bridge configuration works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs I noticed
&lt;/h2&gt;

&lt;p&gt;This setup is convenient, but I would not treat it as magic.&lt;/p&gt;

&lt;p&gt;There are still a few things to watch:&lt;/p&gt;

&lt;h3&gt;
  
  
  Model names matter
&lt;/h3&gt;

&lt;p&gt;OpenAI-compatible providers often use their own model naming format.&lt;/p&gt;

&lt;p&gt;For example, OpenRouter model names usually look different from OpenAI model names.&lt;/p&gt;

&lt;p&gt;If the model name is wrong, the connection may fail even if the API key and base URL are correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free models may change
&lt;/h3&gt;

&lt;p&gt;If you use a free model, do not assume it will stay available forever.&lt;/p&gt;

&lt;p&gt;This is fine for testing, but for production workflows, I would use a stable paid model or at least have a fallback plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout may need tuning
&lt;/h3&gt;

&lt;p&gt;Some models respond slower than others.&lt;/p&gt;

&lt;p&gt;If WordPress keeps failing while the provider works elsewhere, timeout settings are one of the first things I would check.&lt;/p&gt;

&lt;h3&gt;
  
  
  API keys still need to be protected
&lt;/h3&gt;

&lt;p&gt;According to the Koneek plugin information, API keys are stored using AES-256-CBC encryption with the encryption key derived from WordPress secret salts in &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is better than storing plain text secrets, but I would still treat API keys seriously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;do not reuse important keys everywhere&lt;/li&gt;
&lt;li&gt;rotate keys if you suspect exposure&lt;/li&gt;
&lt;li&gt;avoid giving unnecessary permissions&lt;/li&gt;
&lt;li&gt;keep &lt;code&gt;wp-config.php&lt;/code&gt; secure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When I would use this approach
&lt;/h2&gt;

&lt;p&gt;I would use this setup when I want WordPress AI to work with a provider that is not directly supported yet.&lt;/p&gt;

&lt;p&gt;Good use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;testing multiple AI providers&lt;/li&gt;
&lt;li&gt;using OpenRouter for model routing&lt;/li&gt;
&lt;li&gt;connecting an internal OpenAI-compatible gateway&lt;/li&gt;
&lt;li&gt;experimenting with local or self-hosted AI&lt;/li&gt;
&lt;li&gt;avoiding vendor lock-in&lt;/li&gt;
&lt;li&gt;giving WordPress AI access to cheaper or specialized models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I probably would not use a random free provider for serious production content automation unless I had already tested its reliability, latency, and privacy policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main thing I learned
&lt;/h2&gt;

&lt;p&gt;The interesting part is that "OpenAI-compatible" is more of a compatibility layer than a universal guarantee.&lt;/p&gt;

&lt;p&gt;It gives you a common API shape, but the app still needs to know the provider details.&lt;/p&gt;

&lt;p&gt;Once those details are configurable, WordPress AI becomes much more flexible.&lt;/p&gt;

&lt;p&gt;The setup that worked for me was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress AI
    ↓
Koneek provider configuration
    ↓
OpenAI-compatible API endpoint
    ↓
AI provider/model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a pretty clean flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I went into this assuming the hardest part would be the API itself.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;The real trick was making WordPress AI aware of the provider's base URL, model name, and authentication details.&lt;/p&gt;

&lt;p&gt;Once that bridge existed, the rest felt surprisingly simple.&lt;/p&gt;

&lt;p&gt;While researching this setup, I also came across an Indonesian article that walks through a similar configuration: &lt;a href="https://wpgan.com/wordpress/intermediate/cara-menghubungkan-openai-compatible-api-ke-wordpress-ai/" rel="noopener noreferrer"&gt;Cara Menghubungkan OpenAI-Compatible API ke WordPress AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am curious how other WordPress developers are approaching this.&lt;/p&gt;

&lt;p&gt;Are you using official AI providers only, or are you routing everything through OpenAI-compatible gateways like OpenRouter, LiteLLM, Ollama, or OpenWebUI?&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Redis Isn't PostgreSQL: Building a Hybrid Change Data Capture Runtime in Ruby</title>
      <dc:creator>Ken C. Demanawa</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:26:45 +0000</pubDate>
      <link>https://dev.to/kenneth_demanawa_fcc6581e/change-data-capture-redis-pro-5hca</link>
      <guid>https://dev.to/kenneth_demanawa_fcc6581e/change-data-capture-redis-pro-5hca</guid>
      <description>&lt;h2&gt;
  
  
  I Built Commercial Redis CDC Source Drivers for Ruby — Here's What I Learned
&lt;/h2&gt;

&lt;p&gt;For the past couple of years I've been building a Change Data Capture (CDC) ecosystem for Ruby.&lt;/p&gt;

&lt;p&gt;Like many CDC projects, it started with PostgreSQL. PostgreSQL's Write-Ahead Log (WAL) is an excellent source of truth: durable, ordered, replayable, and well understood. It provides exactly the properties you want when you're building reliable event pipelines.&lt;/p&gt;

&lt;p&gt;But the deeper I went into distributed systems, the more I realized something important.&lt;/p&gt;

&lt;p&gt;Many systems don't observe change from PostgreSQL first.&lt;/p&gt;

&lt;p&gt;They observe it from Redis.&lt;/p&gt;

&lt;p&gt;Redis often sits at the front of modern architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis Streams carry application events.&lt;/li&gt;
&lt;li&gt;Pub/Sub distributes transient state changes.&lt;/li&gt;
&lt;li&gt;Keyspace notifications react to cache invalidation and key expiry.&lt;/li&gt;
&lt;li&gt;Redis Cluster routes events across multiple primaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many systems, Redis sees a change before PostgreSQL ever commits it.&lt;/p&gt;

&lt;p&gt;That raised an interesting question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can Redis become a first-class Change Data Capture source?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The obvious answer is "yes."&lt;/p&gt;

&lt;p&gt;The interesting answer is "yes—but not in the same way PostgreSQL does."&lt;/p&gt;

&lt;p&gt;That distinction eventually became &lt;strong&gt;cdc-redis-pro&lt;/strong&gt;, a commercial Redis source driver for the Ruby CDC ecosystem.&lt;/p&gt;

&lt;p&gt;This article isn't a product announcement.&lt;/p&gt;

&lt;p&gt;It's an engineering write-up about the architectural decisions behind the project, the tradeoffs Redis forces you to make, and the execution model that ultimately emerged.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Doesn't Have One CDC Interface
&lt;/h2&gt;

&lt;p&gt;One misconception I frequently encounter is the assumption that Redis has an equivalent of PostgreSQL's WAL.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;Instead, Redis exposes several completely different mechanisms for observing change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Replay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharded Pub/Sub&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyspace Notifications&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At first glance they all look like "events."&lt;/p&gt;

&lt;p&gt;Operationally they're completely different systems.&lt;/p&gt;

&lt;p&gt;Streams are durable.&lt;/p&gt;

&lt;p&gt;Pub/Sub isn't.&lt;/p&gt;

&lt;p&gt;Keyspace notifications exist primarily as operational signals.&lt;/p&gt;

&lt;p&gt;Sharded Pub/Sub introduces routing constraints that don't exist elsewhere.&lt;/p&gt;

&lt;p&gt;Treating them all as the same abstraction inevitably hides important guarantees—and hidden guarantees eventually become production incidents.&lt;/p&gt;

&lt;p&gt;Instead of pretending every Redis source behaves identically, I wanted the API to expose those differences explicitly.&lt;/p&gt;

&lt;p&gt;If a source cannot replay missed messages, the API should say so.&lt;/p&gt;

&lt;p&gt;If a reconnect creates a loss window, operators should know exactly when it happened.&lt;/p&gt;

&lt;p&gt;Infrastructure software shouldn't hide reality.&lt;/p&gt;

&lt;p&gt;It should make reality easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis and PostgreSQL Solve Different Problems
&lt;/h2&gt;

&lt;p&gt;A common question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If Redis can generate change events, why not replace PostgreSQL CDC entirely?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because they solve different problems.&lt;/p&gt;

&lt;p&gt;PostgreSQL's WAL is the durable history of your system.&lt;/p&gt;

&lt;p&gt;Redis is often the earliest signal that something is happening.&lt;/p&gt;

&lt;p&gt;One tells you &lt;strong&gt;what committed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The other tells you &lt;strong&gt;what is happening right now&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They're complementary.&lt;/p&gt;

&lt;p&gt;Not competing.&lt;/p&gt;

&lt;p&gt;Conceptually, I think about them like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    PostgreSQL WAL
                          │
                          ▼
                 Durable Record of Truth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Streams / PubSub / Keyspace
              │
              ▼
        Fast Operational Signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal isn't choosing one over the other.&lt;/p&gt;

&lt;p&gt;The goal is allowing both to participate in the same downstream processing pipeline.&lt;/p&gt;

&lt;p&gt;That required another architectural boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Common Language for Change Events
&lt;/h2&gt;

&lt;p&gt;One of the design goals of the broader CDC ecosystem is that downstream processors shouldn't care where an event originated.&lt;/p&gt;

&lt;p&gt;Whether a change comes from PostgreSQL logical replication or Redis Streams, the downstream processing model should remain identical.&lt;/p&gt;

&lt;p&gt;That boundary is &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of exposing PostgreSQL-specific or Redis-specific payloads to processors, each source is normalized into a common event model.&lt;/p&gt;

&lt;p&gt;Conceptually the pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                PostgreSQL WAL
                     │                     
                pgoutput-client
                     │
                     ▼
                 ChangeEvent
                       ▲
                       │
                 cdc-redis-pro
                       │
        Streams / PubSub / Keyspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything downstream consumes the same normalized event.&lt;/p&gt;

&lt;p&gt;A webhook processor doesn't need to know whether the event came from WAL or Redis.&lt;/p&gt;

&lt;p&gt;A search indexing pipeline doesn't care.&lt;/p&gt;

&lt;p&gt;An audit sink doesn't care.&lt;/p&gt;

&lt;p&gt;Even the execution runtime doesn't care.&lt;/p&gt;

&lt;p&gt;That separation between &lt;strong&gt;source acquisition&lt;/strong&gt; and &lt;strong&gt;event processing&lt;/strong&gt; became one of the defining architectural decisions of the ecosystem.&lt;/p&gt;

&lt;p&gt;As the project grew, it became clear that acquiring events efficiently and processing them efficiently are two different problems—and they scale independently.&lt;/p&gt;

&lt;p&gt;That realization eventually led to a separate execution engine: &lt;strong&gt;cdc-orchestrator-pro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We'll come back to that shortly.&lt;/p&gt;

&lt;p&gt;First, let's look at what makes each Redis source fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Isn't One Event System. It's Four.
&lt;/h2&gt;

&lt;p&gt;The first surprise when building a Redis CDC source is that there isn't a single Redis change stream.&lt;/p&gt;

&lt;p&gt;There are four.&lt;/p&gt;

&lt;p&gt;Each has different delivery guarantees.&lt;/p&gt;

&lt;p&gt;Each behaves differently during failures.&lt;/p&gt;

&lt;p&gt;Each recovers differently after reconnects.&lt;/p&gt;

&lt;p&gt;And each answers a different operational question.&lt;/p&gt;

&lt;p&gt;Treating them as interchangeable would have made the implementation simpler—but it also would have hidden the exact information operators need during production incidents.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;cdc-redis-pro&lt;/code&gt; embraces those differences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Streams: The Durable Path
&lt;/h2&gt;

&lt;p&gt;Redis Streams is the closest thing Redis has to a traditional CDC source.&lt;/p&gt;

&lt;p&gt;Messages are persisted.&lt;/p&gt;

&lt;p&gt;Consumers maintain checkpoints.&lt;/p&gt;

&lt;p&gt;Consumer groups coordinate work.&lt;/p&gt;

&lt;p&gt;Failed consumers leave pending entries behind for recovery.&lt;/p&gt;

&lt;p&gt;In many ways, Streams feels familiar to anyone coming from Kafka or PostgreSQL logical replication.&lt;/p&gt;

&lt;p&gt;That made it the natural foundation for the recoverable side of the driver.&lt;/p&gt;

&lt;p&gt;The Streams implementation supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XREAD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XREADGROUP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Consumer Groups&lt;/li&gt;
&lt;li&gt;Pending-entry inspection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XAUTOCLAIM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Duplicate suppression&lt;/li&gt;
&lt;li&gt;Optional dead-letter streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Operationally, Streams is the only Redis source that provides genuine replay.&lt;/p&gt;

&lt;p&gt;If a downstream worker crashes halfway through a batch, processing resumes from the last committed checkpoint rather than silently dropping work.&lt;/p&gt;

&lt;p&gt;Conceptually, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
             Producer
                │
                ▼
          Redis Stream
                │
          Consumer Group
                │
                ▼
          cdc-redis-pro
                │
           ChangeEvent
                │
                ▼
         Downstream Runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the strongest consistency story Redis offers.&lt;/p&gt;

&lt;p&gt;It isn't PostgreSQL's WAL—but it isn't trying to be.&lt;/p&gt;

&lt;p&gt;It's a durable event log designed for application-level workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pub/Sub: Fast, But Ephemeral
&lt;/h2&gt;

&lt;p&gt;Pub/Sub solves a completely different problem.&lt;/p&gt;

&lt;p&gt;Messages exist only while subscribers are connected.&lt;/p&gt;

&lt;p&gt;Disconnect for five seconds.&lt;/p&gt;

&lt;p&gt;Those five seconds are gone forever.&lt;/p&gt;

&lt;p&gt;That isn't a bug.&lt;/p&gt;

&lt;p&gt;It's the contract.&lt;/p&gt;

&lt;p&gt;Many libraries attempt to hide this by automatically reconnecting.&lt;/p&gt;

&lt;p&gt;The problem is that reconnecting doesn't recover missed messages.&lt;/p&gt;

&lt;p&gt;It only resumes receiving future ones.&lt;/p&gt;

&lt;p&gt;Pretending otherwise creates false confidence.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;cdc-redis-pro&lt;/code&gt; treats Pub/Sub as an explicitly &lt;strong&gt;at-most-once&lt;/strong&gt; source.&lt;/p&gt;

&lt;p&gt;Reconnects are measured.&lt;/p&gt;

&lt;p&gt;Loss windows are reported.&lt;/p&gt;

&lt;p&gt;Operators can immediately see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the disconnect occurred,&lt;/li&gt;
&lt;li&gt;how long the subscriber was offline,&lt;/li&gt;
&lt;li&gt;and exactly where message loss became possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Infrastructure software shouldn't promise guarantees the underlying system doesn't provide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sharded Pub/Sub Changes the Topology
&lt;/h2&gt;

&lt;p&gt;Redis Cluster introduces another variation.&lt;/p&gt;

&lt;p&gt;Sharded Pub/Sub distributes channels across multiple primaries.&lt;/p&gt;

&lt;p&gt;That improves scalability, but it also means subscriptions become topology-aware.&lt;/p&gt;

&lt;p&gt;A reconnect isn't always reconnecting to the same node.&lt;/p&gt;

&lt;p&gt;During resharding, ownership of a channel may move entirely.&lt;/p&gt;

&lt;p&gt;Handling that correctly requires continuously tracking cluster topology rather than assuming a fixed server layout.&lt;/p&gt;

&lt;p&gt;The driver automatically discovers topology through &lt;code&gt;CLUSTER SHARDS&lt;/code&gt; and transparently rebinds subscriptions as ownership changes.&lt;/p&gt;

&lt;p&gt;To downstream processors, events continue arriving normally.&lt;/p&gt;

&lt;p&gt;To operators, topology changes remain observable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keyspace Notifications Aren't Really CDC
&lt;/h2&gt;

&lt;p&gt;Keyspace notifications are probably the easiest Redis feature to misunderstand.&lt;/p&gt;

&lt;p&gt;They're incredibly useful.&lt;/p&gt;

&lt;p&gt;They're also incredibly easy to misuse.&lt;/p&gt;

&lt;p&gt;Keyspace notifications exist to announce that Redis itself performed an operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a key expired,&lt;/li&gt;
&lt;li&gt;a value changed,&lt;/li&gt;
&lt;li&gt;a key was deleted,&lt;/li&gt;
&lt;li&gt;a hash was updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're operational signals.&lt;/p&gt;

&lt;p&gt;They're not durable history.&lt;/p&gt;

&lt;p&gt;They're not replayable.&lt;/p&gt;

&lt;p&gt;And by the time you receive an expiration notification, the value may already be gone.&lt;/p&gt;

&lt;p&gt;That's simply how Redis works.&lt;/p&gt;

&lt;p&gt;Rather than pretending every notification contains complete information, the driver offers optional best-effort value enrichment whenever the value still exists.&lt;/p&gt;

&lt;p&gt;If it doesn't, the event still proceeds.&lt;/p&gt;

&lt;p&gt;The guarantee remains explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delivery Guarantees Should Stay Visible
&lt;/h2&gt;

&lt;p&gt;One design principle shaped almost every API in the project.&lt;/p&gt;

&lt;p&gt;I didn't want to normalize away delivery semantics.&lt;/p&gt;

&lt;p&gt;Instead, I wanted them to remain visible all the way to the operator.&lt;/p&gt;

&lt;p&gt;Think of it like a database transaction.&lt;/p&gt;

&lt;p&gt;You wouldn't want a library to silently convert an eventually-consistent operation into something that merely &lt;em&gt;looks&lt;/em&gt; transactional.&lt;/p&gt;

&lt;p&gt;The same idea applies here.&lt;/p&gt;

&lt;p&gt;Different Redis sources have different operational characteristics.&lt;/p&gt;

&lt;p&gt;The API should preserve them.&lt;/p&gt;

&lt;p&gt;That philosophy can be summarized like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Replay&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Typical Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Durable workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Live events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharded Pub/Sub&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Cluster-scale broadcasts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyspace Notifications&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Operational signals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are "better."&lt;/p&gt;

&lt;p&gt;They're simply optimized for different workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Topology Matters More Than Features
&lt;/h2&gt;

&lt;p&gt;Supporting Redis isn't just about supporting commands.&lt;/p&gt;

&lt;p&gt;It's about supporting deployments.&lt;/p&gt;

&lt;p&gt;A surprising amount of complexity came not from Streams or Pub/Sub themselves, but from the environments they run in.&lt;/p&gt;

&lt;p&gt;The driver currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standalone Redis&lt;/li&gt;
&lt;li&gt;Redis Sentinel&lt;/li&gt;
&lt;li&gt;Redis Cluster&lt;/li&gt;
&lt;li&gt;TLS&lt;/li&gt;
&lt;li&gt;ACL authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cluster support turned out to be particularly interesting.&lt;/p&gt;

&lt;p&gt;Streams must remain within a single hash slot.&lt;/p&gt;

&lt;p&gt;Cross-slot reads fail.&lt;/p&gt;

&lt;p&gt;Pub/Sub subscriptions migrate during resharding.&lt;/p&gt;

&lt;p&gt;Connections disappear during primary failover.&lt;/p&gt;

&lt;p&gt;Those aren't edge cases.&lt;/p&gt;

&lt;p&gt;They're normal operating conditions in production.&lt;/p&gt;

&lt;p&gt;Every supported topology is continuously exercised using Docker-based integration tests covering failover, node restarts, resharding, authentication, and TLS.&lt;/p&gt;

&lt;p&gt;I wanted the implementation to reflect how Redis is actually deployed—not just how it behaves on a laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Acquiring Events Is Only Half the Problem
&lt;/h2&gt;

&lt;p&gt;By this point, the source layer was capable of reliably acquiring events from every major Redis deployment model.&lt;/p&gt;

&lt;p&gt;The next question became much harder.&lt;/p&gt;

&lt;p&gt;How do you process them efficiently?&lt;/p&gt;

&lt;p&gt;One worker?&lt;/p&gt;

&lt;p&gt;Ten workers?&lt;/p&gt;

&lt;p&gt;Hundreds?&lt;/p&gt;

&lt;p&gt;How do you preserve ordering where it's required while still exploiting modern Ruby's parallelism?&lt;/p&gt;

&lt;p&gt;It turned out that reading events from Redis wasn't the difficult part.&lt;/p&gt;

&lt;p&gt;Scheduling what happened &lt;em&gt;after&lt;/em&gt; they were read became the real engineering challenge.&lt;/p&gt;

&lt;p&gt;That challenge eventually became &lt;strong&gt;HybridRuntime&lt;/strong&gt;, the execution engine inside &lt;strong&gt;cdc-orchestrator-pro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And surprisingly, the solution wasn't built around threads.&lt;/p&gt;

&lt;p&gt;It was built around ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture I'm Most Proud Of
&lt;/h2&gt;

&lt;p&gt;Surprisingly, reading events from Redis wasn't the hardest part of the project.&lt;/p&gt;

&lt;p&gt;Scheduling what happened &lt;em&gt;after&lt;/em&gt; those events arrived was.&lt;/p&gt;

&lt;p&gt;Modern Ruby gives us two powerful concurrency primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ractors&lt;/strong&gt; for parallel CPU execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fibers&lt;/strong&gt; for concurrent I/O&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most systems choose one.&lt;/p&gt;

&lt;p&gt;I wanted both.&lt;/p&gt;

&lt;p&gt;That eventually became &lt;strong&gt;HybridRuntime&lt;/strong&gt;, the execution engine inside &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its job isn't tied to Redis.&lt;/p&gt;

&lt;p&gt;Redis simply happened to be the workload that exposed the problem first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event Acquisition and Event Processing Are Different Problems
&lt;/h2&gt;

&lt;p&gt;One architectural realization changed the direction of the project.&lt;/p&gt;

&lt;p&gt;Reading events from a source and processing those events are two completely different concerns.&lt;/p&gt;

&lt;p&gt;They're limited by different bottlenecks.&lt;/p&gt;

&lt;p&gt;They scale independently.&lt;/p&gt;

&lt;p&gt;A PostgreSQL logical replication connection is fundamentally serial.&lt;/p&gt;

&lt;p&gt;A Redis Stream consumer is similarly constrained.&lt;/p&gt;

&lt;p&gt;But once an event has been acquired and normalized into a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;, downstream processing becomes embarrassingly parallel.&lt;/p&gt;

&lt;p&gt;That naturally separates the pipeline into two halves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Source Layer
                         │
         PostgreSQL WAL / Redis Streams
                         │
                         ▼
                CDC::Core::ChangeEvent
                         │
                         ▼
                  Execution Layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once an event reaches the execution layer, its origin no longer matters.&lt;/p&gt;

&lt;p&gt;Redis.&lt;/p&gt;

&lt;p&gt;PostgreSQL.&lt;/p&gt;

&lt;p&gt;A future Kafka adapter.&lt;/p&gt;

&lt;p&gt;A future S3 replay.&lt;/p&gt;

&lt;p&gt;The runtime simply processes &lt;code&gt;ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That separation turned out to be one of the most valuable architectural decisions in the ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  HybridRuntime
&lt;/h2&gt;

&lt;p&gt;HybridRuntime combines two existing execution engines from the CDC ecosystem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cdc-parallel&lt;/strong&gt; provides pools of prewarmed Ractors for true CPU parallelism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cdc-concurrent&lt;/strong&gt; provides asynchronous Fiber pools for overlapping I/O within each Ractor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they form a nested execution model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 HybridRuntime
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
  Ractor Pool                    Ractor Pool
        │                               │
        ▼                               ▼
   Fiber Pool                     Fiber Pool
        │                               │
        ▼                               ▼
Redis Connections              Redis Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting observation is that parallelism and concurrency solve different problems.&lt;/p&gt;

&lt;p&gt;Ractors increase throughput by executing work simultaneously.&lt;/p&gt;

&lt;p&gt;Fibers increase throughput by avoiding idle time while waiting for I/O.&lt;/p&gt;

&lt;p&gt;The runtime deliberately uses both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Inception Pool
&lt;/h2&gt;

&lt;p&gt;As the architecture evolved, I noticed something amusing.&lt;/p&gt;

&lt;p&gt;Every layer owned another pool.&lt;/p&gt;

&lt;p&gt;The runtime owns a pool of Ractors.&lt;/p&gt;

&lt;p&gt;Each Ractor owns a LocalPool.&lt;/p&gt;

&lt;p&gt;Each LocalPool owns a pool of Fibers.&lt;/p&gt;

&lt;p&gt;Each Fiber owns a live Redis connection.&lt;/p&gt;

&lt;p&gt;It looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HybridRuntime
     │
     ▼
Prewarmed Ractor Pool
     │
     ▼
LocalPool
     │
     ▼
Fiber Pool
     │
     ▼
Redis Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally I started calling it the &lt;strong&gt;Inception Pool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A pool containing pools containing pools.&lt;/p&gt;

&lt;p&gt;The name stuck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ownership Instead of Synchronization
&lt;/h2&gt;

&lt;p&gt;Most concurrent systems solve shared state by protecting it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Threads
  │
  ▼
Mutex
  │
  ▼
Shared Connection Pool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more workers you add, the more frequently they compete for the same resources.&lt;/p&gt;

&lt;p&gt;Locks become unavoidable.&lt;/p&gt;

&lt;p&gt;HybridRuntime takes a different approach.&lt;/p&gt;

&lt;p&gt;Instead of synchronizing ownership...&lt;/p&gt;

&lt;p&gt;...it avoids sharing ownership entirely.&lt;/p&gt;

&lt;p&gt;Every Redis client is created inside the Ractor that will use it.&lt;/p&gt;

&lt;p&gt;It never leaves that Ractor.&lt;/p&gt;

&lt;p&gt;Nothing is borrowed.&lt;/p&gt;

&lt;p&gt;Nothing is shared.&lt;/p&gt;

&lt;p&gt;Nothing requires a mutex.&lt;/p&gt;

&lt;p&gt;Conceptually it looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ractor 1
   │
   ├── Redis Connection A
   ├── Redis Connection B
   └── Fiber Scheduler

Ractor 2
   │
   ├── Redis Connection A
   ├── Redis Connection B
   └── Fiber Scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing that crosses a Ractor boundary is an immutable &lt;code&gt;ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything else remains local.&lt;/p&gt;

&lt;p&gt;This aligns naturally with Ruby's ownership model.&lt;/p&gt;

&lt;p&gt;Mutable state belongs somewhere.&lt;/p&gt;

&lt;p&gt;Rather than fighting that constraint, the runtime embraces it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why LocalPool Exists
&lt;/h2&gt;

&lt;p&gt;That ownership model eventually led to another component: &lt;code&gt;Ratomic::LocalPool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unlike traditional connection pools, a LocalPool isn't shared across Ractors.&lt;/p&gt;

&lt;p&gt;The facade itself is shareable.&lt;/p&gt;

&lt;p&gt;The resources are not.&lt;/p&gt;

&lt;p&gt;Each Ractor lazily constructs its own pool the first time it needs one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shareable LocalPool
          │
          ├────────────┐
          ▼            ▼
     Ractor A     Ractor B
          │            │
     Local Pool   Local Pool
          │            │
     Redis Conn   Redis Conn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns out to be a remarkably natural fit for long-lived resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis clients&lt;/li&gt;
&lt;li&gt;PostgreSQL connections&lt;/li&gt;
&lt;li&gt;HTTP clients&lt;/li&gt;
&lt;li&gt;Elasticsearch clients&lt;/li&gt;
&lt;li&gt;Kafka producers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resource stays exactly where it was created.&lt;/p&gt;

&lt;p&gt;The work moves.&lt;/p&gt;

&lt;p&gt;Not the connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Independent Scaling Axes
&lt;/h2&gt;

&lt;p&gt;Another consequence of this architecture is that acquisition and processing no longer have to scale together.&lt;/p&gt;

&lt;p&gt;Suppose a Redis deployment only needs three acquisition workers.&lt;/p&gt;

&lt;p&gt;That says nothing about how many processing workers you need.&lt;/p&gt;

&lt;p&gt;You might run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquisition

3 Ractors
5 Fibers each

↓

Processing

7 Ractors
20 Fibers each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each side can be tuned independently.&lt;/p&gt;

&lt;p&gt;Adding more downstream workers doesn't require opening additional Redis Streams.&lt;/p&gt;

&lt;p&gt;Adding more source readers doesn't require changing the execution topology.&lt;/p&gt;

&lt;p&gt;The two halves of the pipeline evolve independently.&lt;/p&gt;

&lt;p&gt;That separation proved invaluable during benchmarking because it exposed where the real bottlenecks actually lived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond Redis
&lt;/h2&gt;

&lt;p&gt;One realization surprised me.&lt;/p&gt;

&lt;p&gt;HybridRuntime wasn't solving a Redis problem.&lt;/p&gt;

&lt;p&gt;It was solving an event-processing problem.&lt;/p&gt;

&lt;p&gt;Redis happened to be the first source.&lt;/p&gt;

&lt;p&gt;The same execution model works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL logical replication&lt;/li&gt;
&lt;li&gt;Redis Streams&lt;/li&gt;
&lt;li&gt;Webhook delivery&lt;/li&gt;
&lt;li&gt;Search indexing&lt;/li&gt;
&lt;li&gt;Object storage sinks&lt;/li&gt;
&lt;li&gt;Future Kafka adapters&lt;/li&gt;
&lt;li&gt;Future message brokers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything capable of producing a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt; automatically inherits the same execution engine.&lt;/p&gt;

&lt;p&gt;That ultimately justified extracting the runtime into its own commercial component: &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Originally it lived inside another project.&lt;/p&gt;

&lt;p&gt;Eventually it became obvious that it wasn't a Redis runtime.&lt;/p&gt;

&lt;p&gt;It wasn't a Sidekiq runtime.&lt;/p&gt;

&lt;p&gt;It wasn't even a PostgreSQL runtime.&lt;/p&gt;

&lt;p&gt;It was an execution fabric for normalized change events.&lt;/p&gt;

&lt;p&gt;Redis simply happened to be the benchmark that inspired it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallelism Isn't Free
&lt;/h2&gt;

&lt;p&gt;One thing the benchmarks made very clear is that parallelism isn't magic.&lt;/p&gt;

&lt;p&gt;Adding more Ractors doesn't produce linear speedups.&lt;/p&gt;

&lt;p&gt;It introduces coordination costs.&lt;/p&gt;

&lt;p&gt;Partition routing.&lt;/p&gt;

&lt;p&gt;Mailbox communication.&lt;/p&gt;

&lt;p&gt;Ordering constraints.&lt;/p&gt;

&lt;p&gt;Preserving correctness means accepting those costs.&lt;/p&gt;

&lt;p&gt;Understanding where those tradeoffs appear became just as interesting as the throughput numbers themselves.&lt;/p&gt;

&lt;p&gt;Let's look at what those benchmarks actually measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Actually Fits
&lt;/h2&gt;

&lt;p&gt;After spending so much time discussing architecture, it's worth asking a simple question.&lt;/p&gt;

&lt;p&gt;Who actually needs this?&lt;/p&gt;

&lt;p&gt;The honest answer is:&lt;/p&gt;

&lt;p&gt;Not every Rails application.&lt;/p&gt;

&lt;p&gt;If Redis is simply a cache sitting beside your database, this project is probably unnecessary.&lt;/p&gt;

&lt;p&gt;Likewise, if every important state transition already commits to PostgreSQL before anything else happens, PostgreSQL logical replication alone may be all the CDC infrastructure you need.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdc-redis-pro&lt;/code&gt; exists for a much narrower class of systems.&lt;/p&gt;

&lt;p&gt;Systems where Redis is part of the application's event architecture rather than merely its cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Streams as an Event Bus
&lt;/h2&gt;

&lt;p&gt;This is probably the most natural fit.&lt;/p&gt;

&lt;p&gt;Many distributed systems already use Redis Streams as their internal event bus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
    │
    ▼
Redis Stream
    │
    ▼
Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Redis becomes the place where work is coordinated, durability suddenly matters.&lt;/p&gt;

&lt;p&gt;Consumers crash.&lt;/p&gt;

&lt;p&gt;Deployments restart.&lt;/p&gt;

&lt;p&gt;Networks partition.&lt;/p&gt;

&lt;p&gt;A consumer needs to know where to resume.&lt;/p&gt;

&lt;p&gt;Redis Streams already provides those building blocks.&lt;/p&gt;

&lt;p&gt;Consumer Groups.&lt;/p&gt;

&lt;p&gt;Pending Entries.&lt;/p&gt;

&lt;p&gt;Checkpoint IDs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;XAUTOCLAIM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The job of &lt;code&gt;cdc-redis-pro&lt;/code&gt; isn't replacing those mechanisms.&lt;/p&gt;

&lt;p&gt;It's integrating them into a larger event-processing pipeline while preserving their semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fast Signals Before Durable State
&lt;/h2&gt;

&lt;p&gt;Many systems generate transient events before anything reaches PostgreSQL.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventory availability&lt;/li&gt;
&lt;li&gt;market data&lt;/li&gt;
&lt;li&gt;IoT telemetry&lt;/li&gt;
&lt;li&gt;collaborative editing&lt;/li&gt;
&lt;li&gt;multiplayer game state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events often exist for milliseconds.&lt;/p&gt;

&lt;p&gt;Some are never intended to become permanent records.&lt;/p&gt;

&lt;p&gt;Waiting for a database commit before reacting introduces unnecessary latency.&lt;/p&gt;

&lt;p&gt;Redis already has the signal.&lt;/p&gt;

&lt;p&gt;The application simply needs a reliable way to observe it.&lt;/p&gt;

&lt;p&gt;That's exactly where Redis becomes a valuable CDC source.&lt;/p&gt;

&lt;p&gt;Not because it replaces the database.&lt;/p&gt;

&lt;p&gt;Because it observes change sooner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis and PostgreSQL Together
&lt;/h2&gt;

&lt;p&gt;The architecture becomes much more interesting when both sources exist simultaneously.&lt;/p&gt;

&lt;p&gt;Imagine an order-processing pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer clicks Buy
       │
       ▼
Redis Stream
       │
 Immediate downstream processing
        │
 PostgreSQL Transaction
        │
        ▼
 Logical Replication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis carries the operational signal.&lt;/p&gt;

&lt;p&gt;PostgreSQL records the durable history.&lt;/p&gt;

&lt;p&gt;Eventually both become the same normalized object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Streams
        │
        ▼
   ChangeEvent
        ▲
        │
PostgreSQL WAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once normalized, downstream processing becomes identical.&lt;/p&gt;

&lt;p&gt;That separation allows each technology to do what it does best.&lt;/p&gt;

&lt;p&gt;Redis optimizes for responsiveness.&lt;/p&gt;

&lt;p&gt;PostgreSQL optimizes for durability.&lt;/p&gt;

&lt;p&gt;Neither replaces the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event Processing Shouldn't Care About the Source
&lt;/h2&gt;

&lt;p&gt;One of the design goals of the CDC ecosystem is that processors shouldn't know—or care—where an event originated.&lt;/p&gt;

&lt;p&gt;A webhook dispatcher shouldn't behave differently because the event came from Redis instead of PostgreSQL.&lt;/p&gt;

&lt;p&gt;Neither should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search indexing&lt;/li&gt;
&lt;li&gt;audit sinks&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;cache invalidation&lt;/li&gt;
&lt;li&gt;AI pipelines&lt;/li&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;future message brokers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every processor consumes exactly the same event model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis
   │
   ▼
 ChangeEvent
       ▲
       │
 PostgreSQL
      │
      ▼
 Processor
        │
 ┌──────┼────────┬────────┬────────┐
 ▼      ▼        ▼        ▼        ▼

Webhook Search  Audit   Redis   Future...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is what allows the runtime to remain completely source-agnostic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ordered Workloads
&lt;/h2&gt;

&lt;p&gt;Not every workload benefits equally from parallelism.&lt;/p&gt;

&lt;p&gt;Suppose an application updates customer balances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+100
-20
+15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Processing those out of order would produce incorrect state.&lt;/p&gt;

&lt;p&gt;Ordering matters.&lt;/p&gt;

&lt;p&gt;Other workloads don't have that constraint.&lt;/p&gt;

&lt;p&gt;Search indexing.&lt;/p&gt;

&lt;p&gt;Webhook fan-out.&lt;/p&gt;

&lt;p&gt;Telemetry aggregation.&lt;/p&gt;

&lt;p&gt;Independent cache updates.&lt;/p&gt;

&lt;p&gt;Those can often execute concurrently.&lt;/p&gt;

&lt;p&gt;One of the runtime's responsibilities is recognizing that not every processor requires the same ordering guarantees.&lt;/p&gt;

&lt;p&gt;Correctness always comes first.&lt;/p&gt;

&lt;p&gt;Throughput comes second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Sidekiq?
&lt;/h2&gt;

&lt;p&gt;This is probably the question Ruby developers ask most often.&lt;/p&gt;

&lt;p&gt;After all, Sidekiq already provides a robust distributed job system.&lt;/p&gt;

&lt;p&gt;The answer is that jobs and change streams solve different scheduling problems.&lt;/p&gt;

&lt;p&gt;A job queue answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What work should execute next?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A CDC runtime answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How should related events flow through the system while preserving their correctness?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are similar questions.&lt;/p&gt;

&lt;p&gt;They're not the same question.&lt;/p&gt;

&lt;p&gt;Jobs are independent.&lt;/p&gt;

&lt;p&gt;Change events frequently aren't.&lt;/p&gt;

&lt;p&gt;Ordering.&lt;/p&gt;

&lt;p&gt;Checkpoints.&lt;/p&gt;

&lt;p&gt;Replay.&lt;/p&gt;

&lt;p&gt;Transaction boundaries.&lt;/p&gt;

&lt;p&gt;Partition routing.&lt;/p&gt;

&lt;p&gt;Those become first-class concerns in CDC systems.&lt;/p&gt;

&lt;p&gt;Rather than replacing Sidekiq, the runtime sits at a different layer.&lt;/p&gt;

&lt;p&gt;Sidekiq remains an excellent execution engine for background jobs.&lt;/p&gt;

&lt;p&gt;HybridRuntime focuses on ordered event pipelines.&lt;/p&gt;

&lt;p&gt;The two complement one another rather than compete.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building &lt;code&gt;cdc-redis-pro&lt;/code&gt; changed how I think about event-driven systems.&lt;/p&gt;

&lt;p&gt;A few observations kept appearing throughout development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis isn't PostgreSQL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trying to force Redis into a WAL-shaped abstraction usually hides important operational behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery guarantees matter more than APIs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two systems exposing similar methods may have completely different recovery characteristics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership scales better than synchronization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping mutable resources inside a single Ractor proved simpler than sharing them across many workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acquisition and processing are independent problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bottleneck for reading events is rarely the same bottleneck for processing them.&lt;/p&gt;

&lt;p&gt;Treating those concerns separately made both architectures significantly cleaner.&lt;/p&gt;

&lt;p&gt;Most importantly...&lt;/p&gt;

&lt;p&gt;Infrastructure shouldn't hide tradeoffs.&lt;/p&gt;

&lt;p&gt;It should make them explicit.&lt;/p&gt;

&lt;p&gt;That's the philosophy behind the entire project.&lt;/p&gt;

&lt;p&gt;The benchmark results ended up reflecting exactly those design decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Benchmarks Actually Mean
&lt;/h2&gt;

&lt;p&gt;Benchmark numbers are easy to misunderstand.&lt;/p&gt;

&lt;p&gt;They're also surprisingly easy to exaggerate.&lt;/p&gt;

&lt;p&gt;I wanted to avoid both.&lt;/p&gt;

&lt;p&gt;Rather than publishing a single headline number, I built a benchmark matrix that explored how the runtime behaves under different execution strategies.&lt;/p&gt;

&lt;p&gt;The goal wasn't to find the biggest number.&lt;/p&gt;

&lt;p&gt;The goal was to understand where the architecture stops scaling—and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring Different Parts of the Pipeline
&lt;/h2&gt;

&lt;p&gt;Not every benchmark measures the same thing.&lt;/p&gt;

&lt;p&gt;Some benchmarks measure source acquisition.&lt;/p&gt;

&lt;p&gt;Others measure downstream execution.&lt;/p&gt;

&lt;p&gt;Others measure the orchestration layer itself.&lt;/p&gt;

&lt;p&gt;Treating those numbers as interchangeable would be misleading.&lt;/p&gt;

&lt;p&gt;I ended up thinking about the benchmarks as three different phases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Source
      │
      ▼
ChangeEvent Acquisition
      │
      ▼
HybridRuntime
      │
      ▼
Downstream Sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each phase has different bottlenecks.&lt;/p&gt;

&lt;p&gt;Acquisition is constrained by Redis.&lt;/p&gt;

&lt;p&gt;Processing is constrained by CPU, I/O latency, ordering requirements, and scheduling overhead.&lt;/p&gt;

&lt;p&gt;Understanding which phase you're measuring is more important than the final throughput number.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Synthetic Benchmark
&lt;/h2&gt;

&lt;p&gt;The largest number observed was approximately &lt;strong&gt;54,500 events per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's intentionally &lt;strong&gt;not&lt;/strong&gt; presented as an end-to-end Redis benchmark.&lt;/p&gt;

&lt;p&gt;It measures the execution capacity of the orchestration layer after events have already been acquired.&lt;/p&gt;

&lt;p&gt;In other words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ChangeEvent
      │
      ▼
HybridRuntime
      │
      ▼
Processor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This benchmark answers a very specific question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How quickly can the runtime schedule and execute already-available work?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's useful.&lt;/p&gt;

&lt;p&gt;It just isn't the same as measuring an entire Redis pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Pipelines
&lt;/h2&gt;

&lt;p&gt;Real systems spend time doing real work.&lt;/p&gt;

&lt;p&gt;Reading from Redis.&lt;/p&gt;

&lt;p&gt;Writing to PostgreSQL.&lt;/p&gt;

&lt;p&gt;Calling HTTP services.&lt;/p&gt;

&lt;p&gt;Updating search indexes.&lt;/p&gt;

&lt;p&gt;Those operations introduce latency that no scheduler can eliminate.&lt;/p&gt;

&lt;p&gt;When measured end-to-end, the results naturally become lower.&lt;/p&gt;

&lt;p&gt;Current peak observations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis Streams → Runtime: approximately &lt;strong&gt;17,600 events/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL WAL → Redis: approximately &lt;strong&gt;20,000 events/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those numbers include actual I/O rather than isolated scheduling.&lt;/p&gt;

&lt;p&gt;Personally, I find them more interesting than the synthetic benchmark because they reflect complete pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling Isn't Linear
&lt;/h2&gt;

&lt;p&gt;One result immediately stood out.&lt;/p&gt;

&lt;p&gt;Adding more Ractors did &lt;strong&gt;not&lt;/strong&gt; produce proportional speedups.&lt;/p&gt;

&lt;p&gt;That's exactly what I expected.&lt;/p&gt;

&lt;p&gt;Parallelism always introduces coordination costs.&lt;/p&gt;

&lt;p&gt;Events must be routed.&lt;/p&gt;

&lt;p&gt;Partitions must remain consistent.&lt;/p&gt;

&lt;p&gt;Workers communicate through Ractor mailboxes.&lt;/p&gt;

&lt;p&gt;Ordering constraints occasionally delay otherwise-complete work.&lt;/p&gt;

&lt;p&gt;The runtime spends part of its time doing useful work...&lt;/p&gt;

&lt;p&gt;...and part of its time coordinating that work.&lt;/p&gt;

&lt;p&gt;That coordination isn't overhead to eliminate.&lt;/p&gt;

&lt;p&gt;It's the cost of preserving correctness.&lt;/p&gt;

&lt;p&gt;The benchmark matrix made those tradeoffs visible.&lt;/p&gt;

&lt;p&gt;Rather than chasing perfect scaling, the goal became identifying the point where additional parallelism stopped producing meaningful throughput gains.&lt;/p&gt;

&lt;p&gt;For the current implementation, that sweet spot consistently appeared around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3 prewarmed Ractors&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 Redis connections per Ractor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50 Fibers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That balance delivered high throughput without introducing excessive scheduling overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ordering Has a Cost
&lt;/h2&gt;

&lt;p&gt;One benchmark compared ordered and unordered execution.&lt;/p&gt;

&lt;p&gt;The difference wasn't dramatic.&lt;/p&gt;

&lt;p&gt;Ordered execution consistently performed slightly slower.&lt;/p&gt;

&lt;p&gt;That's expected.&lt;/p&gt;

&lt;p&gt;Maintaining ordering means the runtime occasionally waits for earlier work to complete before later work can safely continue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 1
Event 2
Event 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cannot become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 2
Event 3
Event 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;simply because Event 2 happened to finish first.&lt;/p&gt;

&lt;p&gt;Preserving correctness sometimes requires sacrificing a little throughput.&lt;/p&gt;

&lt;p&gt;That's a tradeoff I consider worthwhile.&lt;/p&gt;

&lt;p&gt;Correctness scales better than debugging race conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Interesting Bottleneck
&lt;/h2&gt;

&lt;p&gt;The benchmark wasn't really about Redis.&lt;/p&gt;

&lt;p&gt;It was about coordination.&lt;/p&gt;

&lt;p&gt;At low parallelism, workers spend most of their time processing events.&lt;/p&gt;

&lt;p&gt;At high parallelism, workers spend increasingly more time coordinating with one another.&lt;/p&gt;

&lt;p&gt;Eventually another Ractor contributes more scheduling overhead than useful work.&lt;/p&gt;

&lt;p&gt;Finding that point was considerably more valuable than finding the largest throughput number.&lt;/p&gt;

&lt;p&gt;It answered a much more practical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How should I actually configure this in production?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Chaos Matters More Than Throughput
&lt;/h2&gt;

&lt;p&gt;Raw throughput is only one characteristic of an event pipeline.&lt;/p&gt;

&lt;p&gt;Recovery behavior is arguably more important.&lt;/p&gt;

&lt;p&gt;The benchmark suite includes failure scenarios covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis restarts&lt;/li&gt;
&lt;li&gt;PostgreSQL restarts&lt;/li&gt;
&lt;li&gt;connection interruption&lt;/li&gt;
&lt;li&gt;checkpoint recovery&lt;/li&gt;
&lt;li&gt;consumer recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streams resumed processing from checkpoints.&lt;/p&gt;

&lt;p&gt;Pub/Sub sources reported explicit loss windows.&lt;/p&gt;

&lt;p&gt;Recovery behavior remained consistent with each source's documented guarantees.&lt;/p&gt;

&lt;p&gt;That consistency mattered more to me than achieving another few thousand events per second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Long-Running Stability
&lt;/h2&gt;

&lt;p&gt;Short benchmarks rarely expose operational problems.&lt;/p&gt;

&lt;p&gt;Memory leaks.&lt;/p&gt;

&lt;p&gt;Connection exhaustion.&lt;/p&gt;

&lt;p&gt;Scheduler starvation.&lt;/p&gt;

&lt;p&gt;Queue growth.&lt;/p&gt;

&lt;p&gt;Those usually appear over time.&lt;/p&gt;

&lt;p&gt;The runtime was therefore exercised continuously using soak tests.&lt;/p&gt;

&lt;p&gt;One representative run processed approximately &lt;strong&gt;1.34 million events&lt;/strong&gt; over five minutes.&lt;/p&gt;

&lt;p&gt;No processing failures were observed.&lt;/p&gt;

&lt;p&gt;Median throughput degraded by roughly &lt;strong&gt;2%&lt;/strong&gt; over the duration of the run.&lt;/p&gt;

&lt;p&gt;That's encouraging, although much longer overnight and multi-day soak tests remain on my roadmap.&lt;/p&gt;

&lt;p&gt;Operational confidence comes from sustained behavior—not just impressive graphs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Perhaps the most surprising outcome of the benchmarking work was this:&lt;/p&gt;

&lt;p&gt;The execution runtime wasn't the limiting factor.&lt;/p&gt;

&lt;p&gt;The limiting factor was almost always the surrounding system.&lt;/p&gt;

&lt;p&gt;Network latency.&lt;/p&gt;

&lt;p&gt;Redis.&lt;/p&gt;

&lt;p&gt;HTTP endpoints.&lt;/p&gt;

&lt;p&gt;Disk.&lt;/p&gt;

&lt;p&gt;Database writes.&lt;/p&gt;

&lt;p&gt;The scheduler spent most of its time waiting for external systems.&lt;/p&gt;

&lt;p&gt;That reinforced one of the central architectural decisions behind HybridRuntime.&lt;/p&gt;

&lt;p&gt;Fibers overlap waiting.&lt;/p&gt;

&lt;p&gt;Ractors overlap computation.&lt;/p&gt;

&lt;p&gt;Neither attempts to eliminate latency.&lt;/p&gt;

&lt;p&gt;They simply ensure latency in one part of the system doesn't unnecessarily stall everything else.&lt;/p&gt;

&lt;p&gt;The result isn't infinite scalability.&lt;/p&gt;

&lt;p&gt;It's predictable scalability.&lt;/p&gt;

&lt;p&gt;And for infrastructure software, predictability is usually the more valuable property.&lt;/p&gt;




&lt;p&gt;The complete benchmark reports—including raw CSV data, SVG charts, chaos-recovery artifacts, and soak-test results—are published alongside the documentation.&lt;/p&gt;

&lt;p&gt;I'd much rather readers inspect the raw data than rely on a single headline number.&lt;/p&gt;

&lt;p&gt;Benchmarks are most useful when they're reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cdc-redis-pro&lt;/code&gt; is only one piece of a much larger ecosystem.&lt;/p&gt;

&lt;p&gt;The long-term goal was never to build "yet another Redis client."&lt;/p&gt;

&lt;p&gt;The goal was to build a source-agnostic Change Data Capture platform for Ruby.&lt;/p&gt;

&lt;p&gt;Today, PostgreSQL logical replication and Redis happen to be the two primary sources.&lt;/p&gt;

&lt;p&gt;Tomorrow, that could just as easily include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;NATS&lt;/li&gt;
&lt;li&gt;Amazon SQS&lt;/li&gt;
&lt;li&gt;Webhooks&lt;/li&gt;
&lt;li&gt;Object storage&lt;/li&gt;
&lt;li&gt;Search indexes&lt;/li&gt;
&lt;li&gt;Other databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important observation is that the runtime doesn't need to change.&lt;/p&gt;

&lt;p&gt;As long as a source can be normalized into a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;, everything downstream already knows how to process it.&lt;/p&gt;

&lt;p&gt;That was the motivation behind separating source acquisition from execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Source
           │
           ▼
   CDC::Core::ChangeEvent
           │
           ▼
    cdc-orchestrator-pro
           │
           ▼
        Processors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new source becomes an adapter.&lt;/p&gt;

&lt;p&gt;Not a new runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Split the Runtime?
&lt;/h2&gt;

&lt;p&gt;One architectural decision deserves a brief explanation.&lt;/p&gt;

&lt;p&gt;Originally the execution engine lived inside another project.&lt;/p&gt;

&lt;p&gt;As the ecosystem evolved, I realized something important.&lt;/p&gt;

&lt;p&gt;The runtime wasn't solving a Redis problem.&lt;/p&gt;

&lt;p&gt;It wasn't solving a PostgreSQL problem.&lt;/p&gt;

&lt;p&gt;It wasn't even solving a Sidekiq problem.&lt;/p&gt;

&lt;p&gt;It was solving an event-processing problem.&lt;/p&gt;

&lt;p&gt;That realization led to extracting the execution engine into its own commercial component:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Today it powers Redis CDC.&lt;/p&gt;

&lt;p&gt;Tomorrow it can power any source capable of producing normalized change events.&lt;/p&gt;

&lt;p&gt;Separating those concerns keeps both halves of the system simpler.&lt;/p&gt;

&lt;p&gt;Source adapters acquire events.&lt;/p&gt;

&lt;p&gt;HybridRuntime processes them.&lt;/p&gt;

&lt;p&gt;Each evolves independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Source First
&lt;/h2&gt;

&lt;p&gt;Although &lt;code&gt;cdc-redis-pro&lt;/code&gt; and &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt; are commercial products, the ecosystem they're built upon remains open source.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cdc-core&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdc-parallel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdc-concurrent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-client&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-parser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-decoder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mammoth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those projects define the common event model, execution primitives, and PostgreSQL integration that everything else builds upon.&lt;/p&gt;

&lt;p&gt;The commercial components focus on operational capabilities rather than replacing the open-source foundation.&lt;/p&gt;

&lt;p&gt;That separation is intentional.&lt;/p&gt;

&lt;p&gt;I believe infrastructure ecosystems become valuable through adoption and trust—not artificial feature restrictions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;Redis replication remains one of the larger pieces still on the roadmap.&lt;/p&gt;

&lt;p&gt;Today, &lt;code&gt;cdc-redis-pro&lt;/code&gt; consumes Redis event sources such as Streams, Pub/Sub, and Keyspace Notifications.&lt;/p&gt;

&lt;p&gt;A future version will move further upstream by treating Redis itself as a replication source.&lt;/p&gt;

&lt;p&gt;That's a significantly more ambitious problem.&lt;/p&gt;

&lt;p&gt;I'd rather stabilize the current architecture before expanding its scope.&lt;/p&gt;

&lt;p&gt;There are also areas where I think the execution engine itself can continue to improve.&lt;/p&gt;

&lt;p&gt;Adaptive scheduling.&lt;/p&gt;

&lt;p&gt;Smarter partition routing.&lt;/p&gt;

&lt;p&gt;Better observability.&lt;/p&gt;

&lt;p&gt;Long-running soak tests.&lt;/p&gt;

&lt;p&gt;More topology-aware execution.&lt;/p&gt;

&lt;p&gt;Those improvements belong to the runtime rather than any particular source adapter—which is exactly why separating acquisition from execution turned out to be such a useful architectural boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I started this project thinking I was building Redis CDC.&lt;/p&gt;

&lt;p&gt;Somewhere along the way I realized I was really building an execution model.&lt;/p&gt;

&lt;p&gt;Redis happened to expose the problem first.&lt;/p&gt;

&lt;p&gt;PostgreSQL reinforced it.&lt;/p&gt;

&lt;p&gt;Future source adapters will probably validate it again.&lt;/p&gt;

&lt;p&gt;The most interesting lesson wasn't about Redis at all.&lt;/p&gt;

&lt;p&gt;It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acquiring events and processing events are different problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They have different bottlenecks.&lt;/p&gt;

&lt;p&gt;They scale differently.&lt;/p&gt;

&lt;p&gt;They deserve different architectures.&lt;/p&gt;

&lt;p&gt;Once those responsibilities are separated, the rest of the system becomes remarkably composable.&lt;/p&gt;

&lt;p&gt;Redis becomes another source.&lt;/p&gt;

&lt;p&gt;PostgreSQL becomes another source.&lt;/p&gt;

&lt;p&gt;Tomorrow's adapters become just that—adapters.&lt;/p&gt;

&lt;p&gt;The runtime stays the same.&lt;/p&gt;

&lt;p&gt;For me, that's the most exciting part of the entire project.&lt;/p&gt;

&lt;p&gt;Not because it produced the largest benchmark numbers.&lt;/p&gt;

&lt;p&gt;Not because it uses Ractors or Fibers.&lt;/p&gt;

&lt;p&gt;But because it led to an architecture that's easier to reason about, easier to extend, and honest about the tradeoffs of the systems it builds upon.&lt;/p&gt;

&lt;p&gt;The benchmark reports are public.&lt;/p&gt;

&lt;p&gt;The documentation is public.&lt;/p&gt;

&lt;p&gt;The implementation is commercial.&lt;/p&gt;

&lt;p&gt;If you're building event-driven systems in Ruby—or you're wrestling with Redis and PostgreSQL in the same architecture—I'd genuinely love to hear how you're approaching those problems.&lt;/p&gt;

&lt;p&gt;I'm convinced there's still a lot left to explore.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>redis</category>
      <category>postgres</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Security Profiles Operator hits v1 with stable APIs and a hardening pass</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:24:52 +0000</pubDate>
      <link>https://dev.to/leobaniak/security-profiles-operator-hits-v1-with-stable-apis-and-a-hardening-pass-4hfk</link>
      <guid>https://dev.to/leobaniak/security-profiles-operator-hits-v1-with-stable-apis-and-a-hardening-pass-4hfk</guid>
      <description>&lt;p&gt;After several years carrying a beta tag, the Kubernetes Security Profiles Operator went 1.0.0 on June 26, freezing eight CRD APIs and clearing a third-party security audit with no criticals. For cluster admins, the practical effect is small but consequential: the syscall and LSM profile a workload runs under is now declared on APIs that will not move under your feet.&lt;/p&gt;

&lt;p&gt;The release was announced by Sascha Grunert of Red Hat on the CNCF blog. SPO is the Kubernetes operator that manages seccomp, SELinux and AppArmor profiles as cluster-scoped objects, then attaches them to pods. Until now the value proposition was good and the API was provisional. v1.0.0 nails the second half down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually stable
&lt;/h2&gt;

&lt;p&gt;All eight CRDs graduated to v1, including &lt;code&gt;SeccompProfile&lt;/code&gt;, &lt;code&gt;ProfileRecording&lt;/code&gt;, &lt;code&gt;SelinuxProfile&lt;/code&gt;, &lt;code&gt;RawSelinuxProfile&lt;/code&gt;, and the AppArmor profile type. Conversion webhooks ship with the release, so a cluster running earlier API versions can roll forward without scheduling downtime. The older versions remain available and are slated for removal in a future release. The migration is on the clock, not on fire.&lt;/p&gt;

&lt;p&gt;The audit pass came with some shape changes that are worth reading before you upgrade. &lt;code&gt;SelinuxProfile&lt;/code&gt; swapped its boolean &lt;code&gt;permissive&lt;/code&gt; field for a &lt;code&gt;mode&lt;/code&gt; enum with &lt;code&gt;Enforcing&lt;/code&gt; and &lt;code&gt;Permissive&lt;/code&gt; values, which means any GitOps templates that hard-coded &lt;code&gt;permissive: true&lt;/code&gt; need a rewrite. &lt;code&gt;RawSelinuxProfile&lt;/code&gt; is now gated by an &lt;code&gt;enableRawSelinuxProfiles&lt;/code&gt; configuration flag and a validating admission webhook, so the most privileged path through the operator is off by default. AppArmor inputs run through strict regex validation, raw policy payloads are capped at 500 KB, and the eBPF profile recorder picked up explicit resource limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a cluster team should care
&lt;/h2&gt;

&lt;p&gt;The point of an operator like this is to take the profile out of the host's filesystem and into the API. That changes the blast radius of "we shipped a container with no profile at all." With SPO and a workload-attached profile, the runtime gets the rules from the cluster, the cluster gets the rules from Git, and a rollback is a kubectl apply away. Without it, a profile change usually means a node image bake, which is slower and harder to undo at 3am.&lt;/p&gt;

&lt;p&gt;The v1.0.0 line means platform teams can adopt SPO without writing "subject to API churn" in the runbook. In most platform-engineering shops that was the gating concern, not the technology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet still on the table
&lt;/h2&gt;

&lt;p&gt;KEP 6061, OCI Artifact-Based Security Profile Distribution, is proposed for an upcoming Kubernetes release as alpha. The idea is that kubelet itself learns to fetch profiles from an OCI registry, the same way it already pulls images. If that lands, the cluster no longer needs SPO to deliver the profile to the node; the operator stays useful for authoring and lifecycle, but the distribution path moves into core.&lt;/p&gt;

&lt;p&gt;This is the right direction for reliability. It collapses two control planes into one. It also means SPO's own value proposition will sit closer to "compiler and CI for profiles" than "runtime mover of files." Worth tracking before you commit to a long-term operator deployment plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch list for the upgrade
&lt;/h2&gt;

&lt;p&gt;A few rough edges. The &lt;code&gt;permissive&lt;/code&gt; to &lt;code&gt;mode&lt;/code&gt; rename is a silent break for any template that does not run through the conversion webhook before write. The default-off posture on &lt;code&gt;RawSelinuxProfile&lt;/code&gt; is correct, but teams that relied on it must explicitly opt back in. The upstream KEP is alpha, not GA, so SPO's role in production stays where it is for at least two Kubernetes release cycles.&lt;/p&gt;

&lt;p&gt;How other projects approach the same surface is fragmented. Falco watches behavior at runtime and alerts. SPO writes the rule that would block the behavior in the first place. Kyverno and Gatekeeper can enforce that a pod has a profile, but they neither author nor distribute one. The combination most platform teams end up running is SPO for the rules, an admission policy engine for the "must have a profile" guard, and a runtime detector for what slips through.&lt;/p&gt;

&lt;p&gt;v1.0.0 makes the SPO half of that stack stable enough to depend on. The KEP decides whether its role grows or shrinks from there.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>securityprofilesoperator</category>
      <category>seccomp</category>
      <category>selinux</category>
    </item>
    <item>
      <title>Why I Built a Tiny Repeated-Game Poker Analysis Tool</title>
      <dc:creator>ty215</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:23:00 +0000</pubDate>
      <link>https://dev.to/ty215/why-i-built-a-tiny-repeated-game-poker-analysis-tool-3joa</link>
      <guid>https://dev.to/ty215/why-i-built-a-tiny-repeated-game-poker-analysis-tool-3joa</guid>
      <description>&lt;p&gt;Most poker solvers answer one question very well: given a single hand and a single decision tree, what is the equilibrium strategy? (Yes, there is subgame solving, node locking, and plenty more — but the default frame is still one hand, one equilibrium.)&lt;/p&gt;

&lt;p&gt;I kept getting stuck on a different one. What if the &lt;em&gt;same kind&lt;/em&gt; of spot shows up over and over, and a player can commit to a fixed strategy across those repetitions? In a few toy games I had a hunch, worked out by hand, that committing to a fixed strategy could change its value relative to the one-shot picture. I wanted a tool that could make that commitment value precise — to actually &lt;em&gt;analyze&lt;/em&gt; it rather than just believe it. (Whether any of this rises to a repeated-game equilibrium is a much stronger claim, and one I am deliberately not making here.)&lt;/p&gt;

&lt;p&gt;I'm still learning software engineering, so until recently I couldn't implement this — I was stuck reasoning about toy games on paper. AI tooling made the analysis feasible, so I finally started building it: &lt;code&gt;repeated-poker-analysis&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's a small research project: write one narrow model down, run small examples, and record what the model does and doesn't justify.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;repeated-poker-analysis&lt;/code&gt; is
&lt;/h2&gt;

&lt;p&gt;It is an experimental Python toolkit for small abstract poker games. The current MVP covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed Hero commitment candidates,&lt;/li&gt;
&lt;li&gt;exact Villain best-response diagnostics in small finite trees,&lt;/li&gt;
&lt;li&gt;candidate generation and filtering,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;T_deadline&lt;/code&gt;, an economic adaptation deadline,&lt;/li&gt;
&lt;li&gt;local &lt;code&gt;T_detect&lt;/code&gt;, an observable-distribution sensitivity estimate,&lt;/li&gt;
&lt;li&gt;analysis reports and Markdown summaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is small on purpose. It is not a full solver and it is not wired to real solver ranges. It starts from one toy game — a river spot — that is tiny enough to inspect and test by hand.&lt;/p&gt;

&lt;p&gt;That toy spot is one where showdown always chops but rake still bites. In a single-hand view, putting more money into a raked pot can be locally unattractive. Across repeated occurrences the same spot raises a commitment question: if one player refuses to fold in a fixed pattern, how does the other respond, and how fast would that response have to come for the commitment to stop being worth it?&lt;/p&gt;

&lt;p&gt;This is the question I wanted a tool to make precise — not a claim that any new equilibrium exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why repeated poker is tempting — and where the trap is
&lt;/h2&gt;

&lt;p&gt;Repeated games sound like a natural home for reputation, punishment, and adaptation, and poker has obvious repeated structure: similar river spots, similar blind-vs-blind situations, similar sizings, similar pools.&lt;/p&gt;

&lt;p&gt;Here is the trap I had to respect. If the number of repetitions is known, the game is fully observed, each spot is independent, and both players are perfectly rational, then a finite repeated game often collapses back toward the one-shot equilibrium by backward induction. "This spot happens five times" is &lt;em&gt;not&lt;/em&gt; by itself enough to claim a reputation equilibrium. That is the standard game-theory result, and it is the reason the project keeps the layers below separate.&lt;/p&gt;

&lt;p&gt;So the project keeps several ideas apart that are easy to blur:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a one-hand baseline strategy,&lt;/li&gt;
&lt;li&gt;a fixed Hero commitment candidate,&lt;/li&gt;
&lt;li&gt;Villain's exact best response to that fixed Hero strategy,&lt;/li&gt;
&lt;li&gt;an economic deadline for adaptation,&lt;/li&gt;
&lt;li&gt;a local estimate of how visible the change is,&lt;/li&gt;
&lt;li&gt;and the much stronger claim of a repeated-game equilibrium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MVP mostly lives in the commitment-analysis layer: if Hero is fixed to a candidate strategy in the supplied tree, what are Villain's exact best responses, and what happens to Hero EV under conservative tie handling?&lt;/p&gt;

&lt;h2&gt;
  
  
  What the MVP can do
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(This describes the MVP on &lt;code&gt;main&lt;/code&gt; at the time of writing. I'm still changing it, so details may move.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It runs an end-to-end candidate-analysis pipeline on a small abstract game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build tiny finite two-player game trees with rake,&lt;/li&gt;
&lt;li&gt;evaluate fixed Hero and Villain mixed strategies,&lt;/li&gt;
&lt;li&gt;enumerate exact Villain best responses for small trees,&lt;/li&gt;
&lt;li&gt;report Hero EV under worst- and best-case Villain best-response tie rules,&lt;/li&gt;
&lt;li&gt;generate simple Hero candidates from a baseline,&lt;/li&gt;
&lt;li&gt;filter candidates before comparison,&lt;/li&gt;
&lt;li&gt;compare candidate values against a baseline profile,&lt;/li&gt;
&lt;li&gt;compute &lt;code&gt;T_deadline&lt;/code&gt; and local &lt;code&gt;T_detect&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;render a Markdown summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain terms, the analysis loop is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start from a baseline profile: a fixed Hero strategy and a fixed Villain strategy on the supplied tree. These are action probabilities at each information set — &lt;strong&gt;not hand ranges&lt;/strong&gt;; the tool does not model or import real solver ranges.&lt;/li&gt;
&lt;li&gt;Generate Hero candidates by shifting probability between two actions at a single Hero information set (a blind, systematic enumeration of small shifts — not a search aimed at hurting Villain).&lt;/li&gt;
&lt;li&gt;For each candidate, lock Hero to it and compute Villain's &lt;em&gt;exact&lt;/em&gt; best response. That yields Hero's worst-case EV after Villain adapts.&lt;/li&gt;
&lt;li&gt;Flag a candidate &lt;code&gt;robustly_profitable&lt;/code&gt; only when that post-response worst-case Hero EV is strictly higher than Hero's EV in the baseline profile. The point is not "positive EV" — it is "still better than the one-shot baseline even after the opponent best-responds."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;T_deadline&lt;/code&gt; / &lt;code&gt;T_detect&lt;/code&gt; then add repeated-game timing on top of the candidates that survive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The main entry point is &lt;code&gt;run_candidate_analysis_pipeline&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;scripts/check_mvp.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified workflow:&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;nuts_chop_river&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build_nuts_chop_river&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_hero_strategy&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;candidate_library&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;baseline_villain_strategy&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;repeated_poker&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CandidateFilterConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CandidateGenerationConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_candidate_analysis_pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_nuts_chop_river&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;baseline_hero&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;default_hero_strategy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;baseline_villain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;baseline_villain_strategy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_candidate_analysis_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;baseline_hero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;baseline_villain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;generation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;CandidateGenerationConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shift_amounts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="n"&gt;horizon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;profit_tolerance&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_selection_l1_distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;detection_log_likelihood_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;detection_occurrence_probability_per_opportunity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;filtering&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;CandidateFilterConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;max_l1_distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;min_required_observations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdown_summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a diagnostic report for the model you supplied, not a poker recommendation. Here is an excerpt from the actual output of &lt;code&gt;examples/analysis_pipeline.py&lt;/code&gt; on the nuts-chop river toy game (I've trimmed the Configurations block and some columns; 8 candidates generated, 6 dropped by the filter, 2 compared):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;generated=8 kept=2 excluded=6
compared=2

&lt;span class="gu"&gt;## Candidate Analysis Summary&lt;/span&gt;

&lt;span class="gu"&gt;### Summary Counts&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; total: 2
&lt;span class="p"&gt;-&lt;/span&gt; eligible: 2
&lt;span class="p"&gt;-&lt;/span&gt; excluded: 0
&lt;span class="p"&gt;-&lt;/span&gt; minimum_villain_ev: 1
&lt;span class="p"&gt;-&lt;/span&gt; pareto_frontier: 2

&lt;span class="gu"&gt;### Candidate Rows&lt;/span&gt;

| candidate_id            | fixed_hero_ev | post_response_hero_ev_worst | robustly_profitable | t_detect | exclusion_reasons |
| ----------------------- | ------------- | --------------------------- | ------------------- | -------- | ----------------- |
| H1&lt;span class="se"&gt;\|&lt;/span&gt;check-&amp;gt;bet&lt;span class="se"&gt;\|&lt;/span&gt;shift=0.1 | 0.625         | -0.850                      | no                  | 278      | -                 |
| H1&lt;span class="se"&gt;\|&lt;/span&gt;bet-&amp;gt;check&lt;span class="se"&gt;\|&lt;/span&gt;shift=0.1 | 0.275         | -0.750                      | no                  | 294      | -                 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The baseline Hero EV in this run is +0.45. The column that matters is &lt;code&gt;robustly_profitable&lt;/code&gt;: it is &lt;code&gt;yes&lt;/code&gt; only when &lt;code&gt;post_response_hero_ev_worst&lt;/code&gt; exceeds that baseline. Here both candidates are &lt;code&gt;no&lt;/code&gt; (-0.85 and -0.75 are below +0.45). A candidate that clears the baseline is rare and can exist in constructed cases — the tool's job is to search the candidates and find it when it does. The next section is a hand-built spot where one does.&lt;/p&gt;

&lt;h2&gt;
  
  
  A toy game where the commitment beats the baseline
&lt;/h2&gt;

&lt;p&gt;I needed at least one example where the machinery clearly does what it is meant to: a known spot where committing to a fixed strategy leaves Hero better off than the one-shot baseline, &lt;em&gt;even after&lt;/em&gt; the opponent best-responds. This nuts-chop steal is that example, and I wrote a dedicated test for it (&lt;a href="https://github.com/guriguri215-lang/repeated-poker-analysis/blob/main/tests/test_nuts_chop_steal_commitment.py" rel="noopener noreferrer"&gt;&lt;code&gt;tests/test_nuts_chop_steal_commitment.py&lt;/code&gt;&lt;/a&gt;). Treat it as a check that the tool can detect the effect at all — not as the end goal, and not as a claim about real games. Outside this constructed spot I do not know which situations, if any, are profitable to commit in.&lt;/p&gt;

&lt;p&gt;The spot: a river where the board is already the nuts, so every showdown chops. There is no value betting — the only reason to bet (shove) is fold equity. Rake is below its cap, so a &lt;em&gt;called&lt;/em&gt; pot just bleeds chips to the house. With a small starting pot and a big shove, a single hand looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initial commitment = 1, initial pot = 2, bet = 98, rake = 5%, cap = 4

| Line         | Hero/IP EV | Villain/OOP EV |
|--------------|-----------:|---------------:|
| check-check  |      -0.05 |          -0.05 |
| bet-fold     |      -1.00 |          +1.00 |
| bet-call     |      -2.00 |          -2.00 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In one hand the caller folds: -1.00 (fold) beats -2.00 (call). So the one-shot subgame answer is &lt;strong&gt;OOP bets / IP folds&lt;/strong&gt; — a pure steal, since the board is a chop and there is no value in betting.&lt;/p&gt;

&lt;p&gt;Now lock IP to &lt;em&gt;always call&lt;/em&gt; and ask the tool for OOP's exact best response. The steal's only profit source (fold equity) is gone, a called pot is -2.00 for OOP, so OOP's exact best response flips to &lt;strong&gt;check&lt;/strong&gt; — and check-check is -0.05 for both. The test asserts exactly this: &lt;code&gt;solve_exact_response&lt;/code&gt; returns &lt;code&gt;{"OOP_river": "check"}&lt;/code&gt; once Hero is locked to call.&lt;/p&gt;

&lt;p&gt;And crucially, this clears the baseline: Hero's EV goes from -1.00 (the one-shot steal baseline) to -0.05 after OOP adapts — still negative, but strictly better than the baseline, which is exactly the &lt;code&gt;robustly_profitable&lt;/code&gt; condition. That is the whole point of the project stated in one example: &lt;strong&gt;the one-shot subgame answer (bet/fold) is not the answer under the fixed commitment I wanted to test (check/check).&lt;/strong&gt; The commitment to call removes the opponent's only incentive to bet. (Whether this constitutes a repeated-game &lt;em&gt;equilibrium&lt;/em&gt; is the stronger claim I am deliberately not making — this is a commitment-analysis result, not an equilibrium proof.)&lt;/p&gt;

&lt;p&gt;The tool also puts a number on &lt;em&gt;how long&lt;/em&gt; that commitment stays worth it. With baseline Hero EV = -1.00 (steal), pre-adaptation = -2.00 (locked call while OOP still bets), post-adaptation = -0.05 (OOP has switched to check), &lt;code&gt;T_deadline&lt;/code&gt; comes out as &lt;code&gt;floor(1 + 19N/39)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| N (horizon) | T_deadline |
|------------:|-----------:|
|          10 |          5 |
|          20 |         10 |
|          50 |         25 |
|         100 |         49 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest caveat: this is a tiny, hand-built tree, and the EVs are ones I can check by hand — that is exactly why I trust &lt;em&gt;this&lt;/em&gt; result more than anything else in the repo. It is not evidence about real games; it is evidence that the model and the code agree on one constructed example built to validate the effect.&lt;/p&gt;

&lt;p&gt;Verification on my machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;python -m pytest tests/test_nuts_chop_steal_commitment.py -v&lt;/code&gt; → 15 passed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python -m pytest -q&lt;/code&gt; → 500 passed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python scripts/check_mvp.py&lt;/code&gt; → passes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git diff --check&lt;/code&gt; → clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I worked with AI
&lt;/h2&gt;

&lt;p&gt;I supplied the algorithm and the poker model. Codex wrote the implementation instructions and reviewed the results; Claude Code wrote the code. I checked Codex's prompts and corrected wrong premises, but I did not review the code line by line — I relied on the Codex/Claude review loop and the test suite (currently 500 passing tests).&lt;/p&gt;

&lt;p&gt;Two things from that process are worth recording:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The assistant kept drifting toward the general case. For the commitment analysis I wanted Hero fully fixed and only Villain's exact best response computed, but it repeatedly tried to set up CFR — wasted machinery when Hero is fixed. Stopping it led to a side question I hadn't considered: CFR with one side frozen looks like a fixed-environment learning problem. I'm noting that as a question, not a result.&lt;/li&gt;
&lt;li&gt;Explaining the toy game to the model was harder than explaining it to a person — it over-generalized and assumed things that don't apply (e.g. Villain value-bets on a board that is already the nuts). I ended up brushing each spec up in a chat first, then handing the cleaned version to the coding agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on terms the code keeps separate: &lt;code&gt;T_deadline&lt;/code&gt; is economic (how late Villain can adapt while the locked policy still beats the baseline); &lt;code&gt;T_detect&lt;/code&gt; is visibility (how many local observations before the candidate's action distribution looks distinguishable from baseline). They are different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best-response ties matter.&lt;/strong&gt; If Villain has several best responses with identical Villain EV, Hero's EV can still differ across them. Returning one arbitrary response would hide that risk, so the MVP reports both &lt;code&gt;ev_h_worst&lt;/code&gt; and &lt;code&gt;ev_h_best&lt;/code&gt; across the tie set. (Verified: &lt;code&gt;BestResponseResult&lt;/code&gt; exposes both and the action variation across optimal pure strategies.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small examples are not a weakness.&lt;/strong&gt; The nuts-chop river benchmark is tiny on purpose: easier to hand-check, harder to mistake for a real-money recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;The main one: &lt;strong&gt;the code has not had an independent human code review.&lt;/strong&gt; Tests pass, but I haven't read the implementation line by line and nobody else has either. Rather than rely on reading the code, I plan to validate it from the outside — design the verification to be as exhaustive as I can make it, run simulations across many configurations, and check that the results hold up. Whether static or property-based checking can give that coverage is something I'm still working out.&lt;/p&gt;

&lt;p&gt;The narrower limits: it is not a full solver, does not import real solver ranges yet, does not solve large no-limit games, and does not do STT / ICM / preflop push-fold yet. The exact response engine enumerates Villain pure strategies, so it is meant for small abstract trees only — there is an explicit &lt;code&gt;max_pure_strategies&lt;/code&gt; ceiling, default 100,000. Candidate generation is simple: finite shifts from a baseline, not a continuous strategy space.&lt;/p&gt;

&lt;p&gt;Most importantly: positive EV &lt;em&gt;inside this model&lt;/em&gt; does not guarantee profitable play. The model can be wrong if the abstraction, action tree, rake rule, ranges, or adaptation assumptions are wrong.&lt;/p&gt;

&lt;p&gt;This is not gambling, bankroll, financial, or legal advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The toy game confirmed the effect, so next I want to extend the tool: analyze with hand ranges rather than abstract action probabilities, and model the opponent adapting gradually (e.g. a Bayesian update of their response over repetitions) instead of switching to an exact best response in one step. Alongside that, I want to firm up the outside-in verification described above before trusting results on new spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/guriguri215-lang/repeated-poker-analysis" rel="noopener noreferrer"&gt;guriguri215-lang/repeated-poker-analysis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MVP walkthrough: &lt;a href="https://github.com/guriguri215-lang/repeated-poker-analysis/blob/main/docs/mvp_walkthrough.md" rel="noopener noreferrer"&gt;docs/mvp_walkthrough.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Assumptions and limitations: &lt;a href="https://github.com/guriguri215-lang/repeated-poker-analysis/blob/main/docs/assumptions_and_limitations.md" rel="noopener noreferrer"&gt;docs/assumptions_and_limitations.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Publication policy: &lt;a href="https://github.com/guriguri215-lang/repeated-poker-analysis/blob/main/docs/publication_policy.md" rel="noopener noreferrer"&gt;docs/publication_policy.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I used AI assistance throughout this project and to draft this article. The division of labor was deliberate: I supplied the algorithm and the poker model, Codex handled instructions and review, and Claude Code wrote the code; I checked the prompts and relied on automated review and tests for the implementation. This article was also drafted with AI help and then rewritten to reflect my own decisions, mistakes, and open questions. Technical claims are marked where I have verified them against the code myself; where I say something is provisional or unreviewed, that is literally true.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Tests Pass, Design Breaks: Why TDD Can't Hold the Line on Design Intent</title>
      <dc:creator>Sho Naka</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:21:34 +0000</pubDate>
      <link>https://dev.to/nomurasan/tests-pass-design-breaks-why-tdd-cant-hold-the-line-on-design-intent-42ml</link>
      <guid>https://dev.to/nomurasan/tests-pass-design-breaks-why-tdd-cant-hold-the-line-on-design-intent-42ml</guid>
      <description>&lt;p&gt;There is a popular misconception that if you do TDD, your design also stays correct. That if the tests pass, quality is guaranteed. In AI-assisted development, this misconception is the kind that quietly accumulates — the more tests you have, the more invisible damage builds up underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  All tests passed. The design was still broken.
&lt;/h2&gt;

&lt;p&gt;Here is what happened today.&lt;/p&gt;

&lt;p&gt;A function called &lt;code&gt;safe_post.py&lt;/code&gt; had its signature changed. Two arguments — &lt;code&gt;notify_sh&lt;/code&gt; and &lt;code&gt;doctor_sh&lt;/code&gt; — were removed. The test suite passed in full.&lt;/p&gt;

&lt;p&gt;But the callers were still using the old signature. They were silently broken.&lt;/p&gt;

&lt;p&gt;Why did the tests pass? Because the test code itself was using the old signature. The tests had been written (by AI) at a time when the design intent was already misunderstood. The misunderstanding was baked into the tests from the start.&lt;/p&gt;

&lt;p&gt;Tests passing and the design being correct are two different things.&lt;/p&gt;

&lt;p&gt;"All tests pass" tells you only one thing: the implementation matches what the tests expect. Whether the tests express the right design intent is a separate question.&lt;/p&gt;

&lt;h2&gt;
  
  
  TDD verifies "implementation against tests" — nothing more
&lt;/h2&gt;

&lt;p&gt;Let me restate the TDD definition.&lt;/p&gt;

&lt;p&gt;Red → Green → Refactor. Write a test. Write the implementation that passes the test. Refactor.&lt;/p&gt;

&lt;p&gt;In this loop, what the test verifies is whether the implementation meets the test's expectation. That is one verification — and only one.&lt;/p&gt;

&lt;p&gt;What TDD does not verify is whether the test itself correctly expresses the design intent.&lt;/p&gt;

&lt;p&gt;The structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design intent  →  Tests  (← this link is not verified)
                    ↓
                  Implementation  (← this link is verified by tests)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the person writing the tests misunderstands the design intent, the tests will pass and the design will still be wrong. Machine learning engineer Hamel Husain calls this the "Gulf of Specification" — the gap between what you intended to measure and what your metric actually measures. Optimize hard against a flawed metric and you optimize hard in the wrong direction. The same dynamic plays out in TDD.&lt;/p&gt;

&lt;p&gt;This is not a critique of TDD. It is a statement that TDD, by its structure, cannot solve this particular problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't escape the snowball
&lt;/h2&gt;

&lt;p&gt;"Then review the tests," is the natural counter. Yes — but how do you review the review?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design intent  →  Tests  →  Implementation
                    ↑
               Human reviews (does it express intent?)
                    ↑
               Who reviews the reviewer?
                    ↑
               ... (infinite regress)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only way out of this snowball is to design a terminator for the review chain. And the terminator must, eventually, be a human.&lt;/p&gt;

&lt;p&gt;The problem is that AI accelerates this loop. AI writes the implementation quickly, writes the tests quickly, makes them pass quickly. The faster the AI side moves, the more "is this test expressing intent?" work piles up on the human side. The paradox is sharp: the more you automate, the more confirmation work humans inherit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed and quality, paradoxically
&lt;/h3&gt;

&lt;p&gt;As a Forward Deployed Engineer working on AI adoption in the field, I run into this paradox often. The pattern goes: "AI made our development faster" — then a few weeks later — "but the design is getting more tangled."&lt;/p&gt;

&lt;p&gt;When speed goes up, the share of time allocated to design review goes down in relative terms. When the number of tests goes up, the cost of asking "is this test correct?" goes up with it. Use AI without being aware of this, and the speed benefit converts itself into a quality cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you combine AI and TDD
&lt;/h2&gt;

&lt;p&gt;AI is good at writing tests. "Write tests for this code" — a few seconds, and you have a plausible test file.&lt;/p&gt;

&lt;p&gt;That is exactly where the problem is.&lt;/p&gt;

&lt;p&gt;The tests AI writes tend to be "tests reverse-engineered from the implementation." They describe what the code currently does. This is excellent for verifying "implementation against tests." It is nearly useless for verifying "tests against design intent."&lt;/p&gt;

&lt;p&gt;The reason is simple: AI does not know the design intent. Unless it is in the context, AI reads the implementation, observes the behavior, and turns that behavior into tests. It converts "this is how it currently behaves" into a test, not "this is how it was supposed to behave."&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;safe_post.py&lt;/code&gt; story is exactly this. The tests had been written against the old signature. Nobody noticed. The tests faithfully verified that the implementation matched a now-outdated assumption. After the signature changed, the tests stayed where they were.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "tests pass = OK" trap
&lt;/h3&gt;

&lt;p&gt;What makes this nasty is that the discovery is delayed.&lt;/p&gt;

&lt;p&gt;Normal bugs are caught the moment the implementation fails the test. But "tests don't express the design intent" bugs only surface when the actual runtime behavior diverges from what was intended. From the test output, everything looks fine.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;safe_post.py&lt;/code&gt; case, the fact that callers were using the old signature didn't surface until the code path actually ran. From the test suite alone, the answer was "all green."&lt;/p&gt;

&lt;h2&gt;
  
  
  The one way out
&lt;/h2&gt;

&lt;p&gt;The only way to stop the snowball is to separate what can be machine-verified from what cannot.&lt;/p&gt;

&lt;p&gt;Machine-verifiable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the implementation passes the tests (TDD's job)&lt;/li&gt;
&lt;li&gt;Whether signatures and types are consistent (the type checker's job)&lt;/li&gt;
&lt;li&gt;Whether boundary conditions hold (automated tests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not machine-verifiable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the tests correctly express the design intent&lt;/li&gt;
&lt;li&gt;Whether the implementation's "why" matches the design intent's "why"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Humans only confirm the second category. Everything in the first goes to machines.&lt;/p&gt;

&lt;p&gt;If you skip this split and march forward under the belief that "more tests = more safety," every new test adds another item to the "do I trust this test?" pile. Confirmation cost grows linearly with test count.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a type checker really buys you
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;safe_post.py&lt;/code&gt; case, the signature change was something a type checker could have caught. With Python type annotations, &lt;code&gt;mypy&lt;/code&gt; could have pointed straight at the caller using the old signature.&lt;/p&gt;

&lt;p&gt;A different layer from TDD. A different mechanism. Widening the machine-verifiable surface is the realistic way to keep design integrity intact. Be explicit about which range tests own, which range the type checker owns, and which range humans own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimizing the human surface
&lt;/h3&gt;

&lt;p&gt;To shrink the human surface, externalize design intent as context.&lt;/p&gt;

&lt;p&gt;When asking AI to write tests, lead with the intent. Not "write tests for this function" but "this function's responsibility is X and Y; it does not handle Z; please write tests that verify those two." When you change a signature, write: "this function's responsibility now excludes the notification side; tests should reflect that exclusion."&lt;/p&gt;

&lt;p&gt;Even then, misunderstandings happen. But the divergence between intent and generated test is smaller than when you hand AI nothing but implementation code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not a criticism of TDD
&lt;/h2&gt;

&lt;p&gt;To be clear: I am not against TDD.&lt;/p&gt;

&lt;p&gt;Tests are necessary. Automated tests are the only practical way to verify boundary conditions. They are the only mechanism that can flag "did this signature change break the callers?" — provided the prerequisite holds, that the tests themselves correctly express the design intent.&lt;/p&gt;

&lt;p&gt;The problem is the belief that "if you do TDD, your design is also safe."&lt;/p&gt;

&lt;p&gt;TDD is a tool that raises implementation quality. It is not a tool that verifies design intent. Use it with that distinction in mind, and TDD becomes a powerful weapon. Confuse the two and you get a state where "confidence rises but the actual coverage of quality assurance shrinks."&lt;/p&gt;

&lt;p&gt;In AI-assisted development this distinction matters more, not less. The faster AI can generate tests, the more the gap between "tests written" and "intent verified" widens — unless you deliberately design the mechanism that closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A three-layer model for test design
&lt;/h2&gt;

&lt;p&gt;A practical organizing frame:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: implementation correctness (TDD)&lt;/strong&gt;&lt;br&gt;
Tests carry expectations; the implementation must satisfy them. Red/Green/Refactor. The layer AI is best at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: design integrity (types / static analysis)&lt;/strong&gt;&lt;br&gt;
Signature consistency, type matching, contracts with callers. Type checkers and linters do this. Machine-owned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: alignment with design intent (humans)&lt;/strong&gt;&lt;br&gt;
Whether the test truly expresses "why this should behave this way." Whether the implementation's "why" matches the design intent. Humans only.&lt;/p&gt;

&lt;p&gt;When AI accelerates test generation, Layers 1 and 2 stay machine-owned. Build the discipline of confirming only Layer 3 by human. That is the realistic design for keeping speed and quality together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "verbalizing design intent" is the core skill of the AI era
&lt;/h2&gt;

&lt;p&gt;The conversation broadens slightly from here.&lt;/p&gt;

&lt;p&gt;As AI-assisted development accelerates, the value of being able to articulate design intent rises.&lt;/p&gt;

&lt;p&gt;The cost of writing code has dropped. The cost of writing tests has dropped. Both can be generated in seconds. But "what should we build?" and "why does this design have to look like this?" — these AI does not figure out for you. More precisely: unless you put the intent into the context, AI defaults to "the design inferred from the current implementation."&lt;/p&gt;

&lt;p&gt;A person who can verbalize design intent gives AI more concrete instructions. "This function's responsibility is X and Y. Z is out of scope. Tests should verify these two." Hand AI that, and the gap between intent and generated tests shrinks.&lt;/p&gt;

&lt;p&gt;A person whose design intent lives only in their own head, hands AI nothing concrete. Every confirmation step boomerangs back to the human. When the design intent is not verbalized, the faster AI goes, the more confirmation cost the human inherits.&lt;/p&gt;

&lt;p&gt;I see this pattern more often in the field now: "we introduced AI, development sped up, but quality confirmation has become exhausting." The "exhausting" part is mostly the design-intent verbalization gap. Speed exposes what was tacit.&lt;/p&gt;

&lt;p&gt;TDD does not guarantee design intent for the same reason AI does not guarantee design intent. Both are tools that process what is written. Design intent, unless humans put it into writing, lives nowhere a machine can read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to write the design intent
&lt;/h2&gt;

&lt;p&gt;A concrete question: where should you put it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In code, via test names.&lt;/strong&gt; Not in comments, in the test name itself. The test name is the place to say "what should this implementation be doing, and why." &lt;code&gt;test_safe_post_handles_missing_file&lt;/code&gt; says less than &lt;code&gt;test_safe_post_completes_without_notify_when_notify_sh_is_absent&lt;/code&gt;. The longer name carries the intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In documents, via ADRs (Architecture Decision Records).&lt;/strong&gt; Why you chose this design, what alternatives existed, the assumptions behind the choice. You do not need perfection. A single paragraph — "the current signature is X and Y for these two reasons" — drastically lowers the cost of judging a future signature change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In conversation, via PR comments and issue threads.&lt;/strong&gt; A code review comment that carries design intent becomes a future tracer for "why is it like this?"&lt;/p&gt;

&lt;p&gt;The common move across all three: externalize design intent. Do not keep it in your head. Put it where a machine can reference it.&lt;/p&gt;

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

&lt;p&gt;There is no shortcut to verifying design intent. The region machines cannot handle stays with humans.&lt;/p&gt;

&lt;p&gt;What you can do is shrink the human region. Automate the machine-verifiable side aggressively. Confirm only what is left.&lt;/p&gt;

&lt;p&gt;Not "tests pass, so we are correct." But "did I confirm that the tests express the design intent correctly?"&lt;/p&gt;

&lt;p&gt;TDD is a powerful tool. Use it with a clear sense of what it covers and what it does not. Without that distinction, the faster AI development gets, the more quietly things break underneath.&lt;/p&gt;

&lt;p&gt;That is the lesson from today.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hamel Husain, "Your AI Product Needs Evals" (2024) — origin of the "Gulf of Specification" concept&lt;/li&gt;
&lt;li&gt;Jeannette Wing, "Computational Thinking" (2006, Communications of the ACM)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post was adapted (not literally translated) from a Japanese original at &lt;a href="https://nomuraya-hub.pages.dev/" rel="noopener noreferrer"&gt;nomuraya-hub.pages.dev&lt;/a&gt;. I am the same author writing under different pen names — "nomuraya / shimajima / 中翔" — depending on the medium.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tdd</category>
      <category>testing</category>
      <category>softwaredesign</category>
      <category>aidevelopment</category>
    </item>
    <item>
      <title>I Built a Serverless VPN on Lambda MicroVMs — 12 Builds, 5 Dead Ends, 1 Working Architecture</title>
      <dc:creator>Vivek V.</dc:creator>
      <pubDate>Sat, 27 Jun 2026 00:20:56 +0000</pubDate>
      <link>https://dev.to/aws-heroes/i-built-a-serverless-vpn-on-lambda-microvms-12-builds-5-dead-ends-1-working-architecture-5ged</link>
      <guid>https://dev.to/aws-heroes/i-built-a-serverless-vpn-on-lambda-microvms-12-builds-5-dead-ends-1-working-architecture-5ged</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built a personal VPN using AWS Lambda MicroVMs. Your traffic exits from AWS. When you disconnect, the MicroVM terminates — zero cost, nothing running. When you reconnect, a fresh MicroVM launches in about 20 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vpn.sh start   &lt;span class="c"&gt;# All Mac traffic now exits from AWS&lt;/span&gt;
./vpn.sh stop    &lt;span class="c"&gt;# Back to your real IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what I learned across 12 image builds — dead ends, kernel limitations, and what finally worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;Lambda MicroVMs launched in June 2026 (4 days ago). They are Firecracker VMs with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full Linux OS — your own binaries, eBPF, iptables, network namespaces&lt;/li&gt;
&lt;li&gt;Suspend/resume — state preserved on snapshot, resumes in ~1s per GB (or terminate for zero ongoing cost)&lt;/li&gt;
&lt;li&gt;Hardware-level isolation — every session gets its own sandbox&lt;/li&gt;
&lt;li&gt;Per-second billing — ~$0.13/hr for a 2GB ARM64 (Graviton) instance&lt;/li&gt;
&lt;li&gt;8-hour max lifetime (active + suspended combined)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to run a VPN inside one. Connect when I need privacy. Disconnect and pay nothing for compute. Resume instantly when I reconnect.&lt;/p&gt;

&lt;p&gt;Took 12 image builds to get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Tried (and Failed)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Attempt 1: NAT Gateway Replacement (The Original Idea)
&lt;/h3&gt;

&lt;p&gt;This is actually where the project started. I was paying $32/mo for a NAT Gateway and thought: what if a MicroVM running nftables could replace it? Serverless NAT. Pay only when traffic flows.&lt;/p&gt;

&lt;p&gt;Why it failed: Lambda MicroVMs cannot act as VPC route targets. Their networking is ingress-only (HTTPS + JWT). Other VPC resources cannot route &lt;em&gt;through&lt;/em&gt; a MicroVM. The VPC egress connector gives the MicroVM its own internet access. It does not make it a transit device.&lt;/p&gt;

&lt;p&gt;That killed the NAT idea. But it made me think — if I cannot route VPC traffic through it, what about routing &lt;em&gt;my own laptop's&lt;/em&gt; traffic through it? That is how Serverless VPN was born.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 2: VPC Egress Connector
&lt;/h3&gt;

&lt;p&gt;I created a VPC, subnets, security groups, and a network connector. One hour wasted.&lt;/p&gt;

&lt;p&gt;MicroVMs have &lt;code&gt;INTERNET_EGRESS&lt;/code&gt; by default. The connector is only needed for reaching private VPC resources (RDS, internal NLBs). For a VPN that exits to the public internet, default egress works fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 3: Kernel WireGuard
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;add wg0 &lt;span class="nb"&gt;type &lt;/span&gt;wireguard
&lt;span class="go"&gt;Error: Unknown device type.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MicroVM kernel does not have &lt;code&gt;wireguard.ko&lt;/code&gt;. Setting &lt;code&gt;additionalOsCapabilities: ["ALL"]&lt;/code&gt; does not help. "ALL capabilities" means Linux capabilities (CAP_NET_ADMIN, etc.). Not kernel modules. The Firecracker kernel is compiled by AWS. You cannot load modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 4: Boringtun (Userspace WireGuard)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;Failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;initialize&lt;/span&gt; &lt;span class="n"&gt;tunnel&lt;/span&gt;
&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Os&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uncategorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"No such device"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even after &lt;code&gt;mknod /dev/net/tun c 10 200&lt;/code&gt;, the kernel has no TUN driver. &lt;code&gt;CONFIG_TUN&lt;/code&gt; is not compiled in. The device node exists in the filesystem, but nothing in the kernel backs it.&lt;/p&gt;

&lt;p&gt;eBPF works because &lt;code&gt;CONFIG_BPF=y&lt;/code&gt; is in the kernel. TUN is not. This is a kernel config choice, not a permissions issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 5: Boringtun Daemonize
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BoringTun failed to start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boringtun forks by default. The Firecracker environment blocks fork() in daemon mode. Fix: &lt;code&gt;--foreground&lt;/code&gt; flag. But TUN still does not work, so this was moot.&lt;/p&gt;

&lt;p&gt;Always use &lt;code&gt;--foreground&lt;/code&gt; for any daemon process in MicroVMs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  veth + SOCKS5 Proxy
&lt;/h3&gt;

&lt;p&gt;Credit to &lt;a href="https://www.linkedin.com/in/aidansteele/" rel="noopener noreferrer"&gt;Aidan Steele&lt;/a&gt; (AWS Serverless Hero) who pointed me towards veth pairs. He had already built a &lt;a href="https://github.com/aidansteele/microvm-fun" rel="noopener noreferrer"&gt;Kubernetes cluster across MicroVMs&lt;/a&gt; — multiple pods per MicroVM, all using veth + network namespaces. No TUN needed.&lt;/p&gt;

&lt;p&gt;The kernel supports veth pairs, network namespaces, iptables, and IP forwarding. Just not TUN.&lt;/p&gt;

&lt;p&gt;Final architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mac → wstunnel (WSS) → MicroVM:8080 → microsocks (SOCKS5) → internet (AWS IP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No TUN. No WireGuard kernel module. No VPC. Just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;wstunnel&lt;/strong&gt; — WebSocket tunnel. Wraps TCP in WSS for MicroVM ingress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;microsocks&lt;/strong&gt; — 20KB SOCKS5 proxy. Routes traffic to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iptables MASQUERADE&lt;/strong&gt; — NATs traffic out the MicroVM's eth0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS networksetup&lt;/strong&gt; — Sets system-wide SOCKS proxy.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The 12-Build Journey
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;What Changed&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;Kernel WireGuard&lt;/td&gt;
&lt;td&gt;❌ Unknown device type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2–v4&lt;/td&gt;
&lt;td&gt;Fixed S3 access, IAM propagation&lt;/td&gt;
&lt;td&gt;❌ Access denied → fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;additionalOsCapabilities: ALL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ ip_forward works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;td&gt;Added boringtun (pre-built binary)&lt;/td&gt;
&lt;td&gt;❌ Binary was HTML 404 page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;td&gt;Multi-stage Docker build&lt;/td&gt;
&lt;td&gt;❌ Multi-stage FROM not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8&lt;/td&gt;
&lt;td&gt;Single-stage Rust compile&lt;/td&gt;
&lt;td&gt;✅ Built, but ENODEV on TUN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v9&lt;/td&gt;
&lt;td&gt;ALL caps + microvmHooks enabled&lt;/td&gt;
&lt;td&gt;✅ /run hook fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v10&lt;/td&gt;
&lt;td&gt;mknod /dev/net/tun + boringtun --foreground&lt;/td&gt;
&lt;td&gt;❌ ENODEV (no CONFIG_TUN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v11&lt;/td&gt;
&lt;td&gt;Better error logging&lt;/td&gt;
&lt;td&gt;Confirmed: no TUN driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v12&lt;/td&gt;
&lt;td&gt;Filed AWS support ticket&lt;/td&gt;
&lt;td&gt;Waiting on CONFIG_TUN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;veth + microsocks + wstunnel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Working&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. update-microvm-image Strips Settings
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;additionalOsCapabilities&lt;/code&gt; and &lt;code&gt;hooks&lt;/code&gt; are lost when you call &lt;code&gt;update-microvm-image&lt;/code&gt;. Always create a fresh image with a new name.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. API Version Prefixes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda MicroVMs: &lt;code&gt;/2025-09-09/&lt;/code&gt; (service: &lt;code&gt;lambda&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Network Connectors: &lt;code&gt;/2026-04-04/&lt;/code&gt; (service: &lt;code&gt;lambda-core&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Both use the same host: &lt;code&gt;lambda.us-east-1.amazonaws.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. boto3 Does Not Have the Service Model
&lt;/h3&gt;

&lt;p&gt;Lambda Python 3.12 runtime ships boto3 that does not know &lt;code&gt;lambda-microvms&lt;/code&gt;. Use SigV4-signed raw HTTP or update the CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Do Not Setup Networking in /ready Hook
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/ready&lt;/code&gt; runs during image build. Networking capabilities may not be fully available. Only do filesystem ops. Real networking goes in &lt;code&gt;/run&lt;/code&gt; hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. ALL Capabilities ≠ Kernel Modules
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;additionalOsCapabilities: ["ALL"]&lt;/code&gt; grants Linux capabilities. It does NOT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load kernel modules&lt;/li&gt;
&lt;li&gt;Enable CONFIG_TUN or CONFIG_WIREGUARD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It DOES enable: sysctl, iptables, ip link (veth/bridge), eBPF, network namespaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Hooks Must Be Set at Image Creation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--hooks&lt;/span&gt; &lt;span class="s1"&gt;'{"microvmHooks":{"run":"ENABLED","runTimeoutInSeconds":60}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you forget this, &lt;code&gt;/run&lt;/code&gt; never fires and your VPN never starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. wstunnel Version Compatibility
&lt;/h3&gt;

&lt;p&gt;Server v10.1.0 and client v10.5.5 may crash. Match versions exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. 8-Hour Max Lifetime
&lt;/h3&gt;

&lt;p&gt;MicroVM terminates after 8 hours (active + suspended combined). Run &lt;code&gt;./vpn.sh start&lt;/code&gt; again when it expires.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Token Expiry (60 min max)
&lt;/h3&gt;

&lt;p&gt;Auth tokens expire. The WebSocket connection persists after initial auth, but reconnection needs a fresh token.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./vpn.sh start
    ↓
aws lambda-microvms run-microvm (image: serverless-vpn-v13)
    ↓
MicroVM launches from snapshot (~20s)
    ↓
/run hook fires:
  - Creates veth pair + network namespace
  - Enables ip_forward
  - iptables MASQUERADE
  - Starts microsocks (SOCKS5 on :1080)
  - Starts wstunnel server (WSS on :8080)
    ↓
wstunnel client on Mac connects via WSS
    ↓
networksetup -setsocksfirewallproxy Wi-Fi 127.0.0.1 1081
    ↓
ALL TCP TRAFFIC → SOCKS5 → WSS → MicroVM → AWS IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connected (browsing)&lt;/td&gt;
&lt;td&gt;~$0.13/hr (2GB ARM64 MicroVM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disconnected (terminated)&lt;/td&gt;
&lt;td&gt;$0 — nothing running, no storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each session start&lt;/td&gt;
&lt;td&gt;~20s cold start from snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data transfer out&lt;/td&gt;
&lt;td&gt;$0.09/GB (standard AWS egress)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical month (2hr/day)&lt;/td&gt;
&lt;td&gt;~$8 AWS costs + $5 software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light usage (1hr/day)&lt;/td&gt;
&lt;td&gt;~$5 AWS costs + $5 software&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MicroVM can burst up to 4x baseline (8GB/4vCPU) during peak usage at peak rates.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Deploy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Subscribe on AWS Marketplace (10 minutes)
&lt;/h3&gt;

&lt;p&gt;The full stack is available on AWS Marketplace with a 7-day free trial. One-click CloudFormation deploy, no servers to manage.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://serverless-vpn.waltsoft.net/" rel="noopener noreferrer"&gt;Serverless VPN on AWS Marketplace&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Build It Yourself (under 1 hour)
&lt;/h3&gt;

&lt;p&gt;This blog documents every gotcha I hit. Point &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; or any AI coding agent at this post plus the &lt;a href="https://github.com/aws/agent-toolkit-for-aws/blob/main/skills/specialized-skills/serverless-skills/aws-lambda-microvms/SKILL.md" rel="noopener noreferrer"&gt;Lambda MicroVM Skill&lt;/a&gt; and it will scaffold the stack. The gotchas above are what take weeks to discover on your own.&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI v2.27+ (&lt;code&gt;brew upgrade awscli&lt;/code&gt; — required for lambda-microvms commands)&lt;/li&gt;
&lt;li&gt;wstunnel (&lt;code&gt;brew install wstunnel&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;AWS account with Lambda MicroVM access (us-east-1, us-east-2, us-west-2, eu-west-1, ap-northeast-1)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Multi-Region
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run setup once per region (builds the MicroVM image there)&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-northeast-1 ./vpn.sh setup  &lt;span class="c"&gt;# One-time per region&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-northeast-1 ./vpn.sh start  &lt;span class="c"&gt;# Tokyo&lt;/span&gt;

&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eu-west-1 ./vpn.sh setup
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eu-west-1 ./vpn.sh start       &lt;span class="c"&gt;# Ireland&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your IP appears from that country. Each region requires a one-time &lt;code&gt;setup&lt;/code&gt; (image build is regional). Available in us-east-1, us-east-2, us-west-2, eu-west-1, and ap-northeast-1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Good to Know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session VPN, not 24/7&lt;/strong&gt; — designed for work sessions, not always-on. Use for privacy during active browsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOCKS5 proxy&lt;/strong&gt; — routes TCP traffic (web, APIs). Does not tunnel UDP or raw IP (gaming, VoIP) like a full WireGuard VPN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs on Graviton (ARM64)&lt;/strong&gt; — Amazon Linux 2023 base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS and Linux&lt;/strong&gt; — Windows supported via WSL2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own instance&lt;/strong&gt; — dedicated MicroVM in your AWS account. No shared servers. No traffic logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail auditable&lt;/strong&gt; — all operations logged in your account.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CONFIG_TUN support&lt;/strong&gt; — I have filed an AWS support ticket requesting TUN/TAP in the Firecracker guest kernel. If AWS adds it, microsocks gets swapped for WireGuard (proper full-tunnel VPN with UDP support).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP tunneling&lt;/strong&gt; — currently SOCKS5 handles TCP only. WireGuard would fix this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-token refresh&lt;/strong&gt; — re-auth before 60-min expiry for long sessions.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms.html" rel="noopener noreferrer"&gt;Lambda MicroVM Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/agent-toolkit-for-aws/blob/main/skills/specialized-skills/serverless-skills/aws-lambda-microvms/SKILL.md" rel="noopener noreferrer"&gt;Lambda MicroVM Skill (agent toolkit)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/erebe/wstunnel" rel="noopener noreferrer"&gt;wstunnel (WebSocket tunneling)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rofl0r/microsocks" rel="noopener noreferrer"&gt;microsocks (lightweight SOCKS5)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>lambda</category>
      <category>microvm</category>
      <category>firecracker</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
