Building a Feature Store With Neo4j and Feast
Build Production-Ready AI Phone Agents: Infrastructure, Models, and Telephony
Why Prompt Caching Matters for LLM Applications? Modern LLM apps often get the same, or almost the same, prompt more than once. This happens with form submissions, common FAQ questions, agent tool calls, or a user asking the same thing in different words. Without caching, each of these repeat requests pays full token cost, full latency, and adds unwanted pressure on your rate limits. There are four caching methods used in production today. This article, Part 1 of a 3-part series, covers the first and most basic one: exact-match caching. Part 2 covers semantic caching, for requests that mean the same thing but are worded differently. Part 3 covers what happens at the model-serving layer itself: self-hosted KV cache reuse and provider prefix caching, plus the production details every team ends up needing: watching your metrics, working out your costs, and a full checklist. Gateway or application exact-match caching. A hash lookup on a byte-identical or normalized request. Zero cost on a hit. Covered here.Semantic caching. Embedding plus a vector similarity search. Zero model cost on a hit for reworded queries. Covered in Part 2.Inference-engine KV cache reuse, for teams running their own models (vLLM, SGLang). Covered in Part 3.Provider prompt or prefix caching (Anthropic, OpenAI, Bedrock). Covered in Part 3. These layers stack. Check exact match first, then check semantic, then let the request reach the model, where either your own inference engine or the API provider reuses whatever part of the prompt stayed the same. The Full Picture Exact match sits right at the front of the stack. It is the cheapest check to run, so it should catch as many requests as it can before anything more costly runs. Layers 1 and 2 sit in front of the model and can skip the model call entirely. Layers 3 and 4, covered in Part 3, sit inside the model call itself and never skip it. They just make it much cheaper when a part of the prompt repeats. 1. Exact Query Caching (Baseline) Treat the prompt, or a cleaned-up version of it, as a key in a fast key-value store: Redis, Memcached, or an in-process LRU cache. Key Design Hash the full context that matters: system prompt, user message(s), tool definitions, and any settings (like temperature) that change the output.Clean up whitespace, and sort tool lists if their order does not change how the model behaves.Store the full response, or structured output, plus details like timestamp, model version, and TTL.For agents, the key needs more than just the user message. Include the full tool schema (name, description, parameters), any tool-call results you fold back into the context if they are always the same, and leave out anything you don't want changing the key, such as a request ID or trace ID that changes every call but doesn't affect the output. Two common ways a cache silently breaks here: tool definitions listed in a different order between calls, and JSON keys written in a different order by different parts of the same codebase. Sort both before hashing. Don't count on the order matching by chance. Where Should the Cache Live? The store you pick changes how it can fail, more than most teams expect at first: In-process LRU (a local dict, or a library like cachetools) is the fastest choice, under a millisecond, with no network call. It only sees traffic hitting that one process, so on a deployment with many instances, each instance builds its own cache on its own, and the hit rate stays low until every instance has warmed up. This is fine for small deployments, or for caching things that are cheap to redo per instance anyway.A shared store (Redis, Memcached) gives every instance the same view, so a hit on instance A also counts as a hit for a request landing on instance B. This is the right default once you go beyond a single instance, at the cost of a network round trip, usually a few milliseconds on the same network.A two-level setup (a small in-process LRU in front of shared Redis) gives you the best of both: the busiest keys never leave the process, and everything else falls through to the shared store. Worth the extra work once a small number of keys account for most of your traffic, which is common on FAQ-style workloads. Handling Stale Data A fixed TTL, for example, 60 seconds, for time-sensitive data such as travel prices or stock levels.LRU eviction once the cache fills up.Clear the cache on purpose when something changes: a price update, an outage, a policy change. Python example (Redis): Python import hashlib import json import redis from typing import Optional r = redis.Redis(host="localhost", port=6379, db=0) def make_key(prompt: str, model: str, temperature: float = 0.0) -> str: payload = json.dumps({"prompt": prompt, "model": model, "temp": temperature}, sort_keys=True) return "llm:exact:" + hashlib.sha256(payload.encode()).hexdigest() def get_exact(prompt: str, model: str, temperature: float = 0.0) -> Optional[str]: key = make_key(prompt, model, temperature) try: return r.get(key) except redis.RedisError as exc: # Cache being down should never break the request. # Log it and fall through to the model. logger.warning("exact cache read failed: %s", exc) return None def set_exact(prompt: str, response: str, model: str, temperature: float = 0.0, ttl: int = 300): key = make_key(prompt, model, temperature) try: r.setex(key, ttl, response) except redis.RedisError as exc: logger.warning("exact cache write failed: %s", exc) Treat the cache as a nice-to-have, not something the app depends on. If the cache goes down, fall back to full model calls; don't fail the request. Should You Cache Errors? Most production systems say no, or only in some cases, and it is easy to skip thinking about this until something goes wrong. A few good defaults: Never cache a 429 (rate-limited) or 5xx error from the model provider as if it were a real answer. Retry with backoff instead of remembering the failure.If your own guardrail or content filter rejects a response, that is a judgment call. Some teams cache the rejection briefly (a few seconds to a minute) just to stop a broken or hostile client from triggering the same expensive rejection path again and again. Others never cache rejections at all, so they don't end up serving a stale "rejected" result after the input has been fixed.If you do cache rejections, use a clearly different TTL and a different key prefix (llm:reject: instead of llm:exact:) so you can clear them, or leave them out of your hit-rate numbers, separately from real answers. Warming the Cache A cache that just went live is empty, and the first few minutes after any deploy that clears the cache see a drop in hit rate. If your logs keep old prompts and responses, a simple warm-up job that loads the most common prompts from the last few days into the new cache, before it takes real traffic, avoids that drop. This matters most for FAQ-style traffic where a small number of questions make up most of the volume. Request Coalescing: Solving the Thundering Herd Exact-match caching has one failure mode that only shows up under real load: if fifty identical requests arrive at almost the same time, before the first one has filled the cache, all fifty can miss, and all fifty can call the model on their own. This is called a cache stampede, and it is not rare at real traffic levels. A viral post, a batch job starting up, or a retry storm from another system can all set it off. How to Fix It Request coalescing, also called single-flight. The first request for a key takes a short lock (or sets an in-flight marker), and the other requests wait on that same result instead of calling the model on their own. Redis's SET NX pattern, or a library like singleflight in Go, handles this cleanly.Cache the "in progress" state too, not just finished responses, so other requests can see "someone is already working on this" instead of racing to do it themselves.Put a fair timeout on the wait, with a fallback to a normal model call if the in-flight request does not finish in time, so one stuck request does not hold up every other request waiting on it. Without this fix, exact-match caching can actually make traffic spikes worse, not better. Every process learns about a busy key at almost the same moment and hits the model all at once, right when the model can least handle it. When It Shines Templated queries, form-driven flows, test suites, batch jobs — anywhere the same string keeps coming back. Hit rates can be very high in narrow domains and close to zero in open, free-form chat, which is exactly the gap semantic caching, covered in Part 2, is built to close. What's Next Exact-match caching only helps when the input matches exactly, which is common for templated and form-driven traffic but rare in open conversation. Part 2 covers semantic caching: matching reworded questions with embeddings, doing it safely with a verification step, and the invalidation, multi-turn, and security issues that come with trusting a similarity score instead of an exact match. Further Reading Redis documentation, SET command options (including NX for locking patterns)Redis documentation, key eviction policies (LRU, LFU, TTL-based)
Software teams are adopting AI agents to write, run, and repair tests faster than ever. The breakthrough is usually not the model alone. ChatGPT and Cursor can share the same underlying intelligence, yet one feels like a copy-and-paste assistant, while the other feels like a teammate that can edit files, run commands, and iterate until the job is done. The difference is the agent harness: the orchestration layer that wraps the model with instructions, tools, context, and guardrails so raw reasoning becomes reliable action. In test automation, that gap matters even more. A failing scenario can produce thousands of lines of Playwright output, span page objects, step definitions, test data, and multiple environments. Dump all of that into a single long chat, and the model slows down, costs rise, and decisions get noisier. A well-designed harness treats the model as a planner, not a dump truck. It indexes the codebase so the agent can ask structural questions instead of reading entire files. It delegates test execution and file generation to smaller, isolated workers that return compact summaries. It validates credentials, environment profiles, and reference fixtures before a run starts, so failures are caught early instead of mislabeled as “broken locators.” And when something does break, it routes the failure through a governed path: diagnose, fix, audit the patch, re-run only what changed, report results in a form humans can act on. This article walks through that architecture in practice: what an agent harness is, how memory and code indexing reduce token waste, why tiered model routing balances cost and quality, and how governance gates keep AI-assisted automation predictable rather than magical. Whether you are building on Cursor, Claude Code, or your own SDK integration, the pattern is the same. The model is the engine. The harness is what makes it drivable at scale. Agent Harness An agent harness is the software infrastructure that wraps around an AI model (like Claude, GPT-4) to turn it into an autonomous working agent. Harness acts more like an operating system, providing the model with memory, tools, execution environments, and guardrails to complete multi-step tasks in the real world What the Harness Does Harness handles three things: Instructions – system prompts, rules (like the CLAUDE.md), skills, and subagent definitions that shape behavior.Tools – file read/write, terminal, search, MCP servers, browser automation, etc.The loop – the model proposes an action → the harness executes it → results go back to the model → repeat until the task is done Beyond that, a good harness also manages: Context – which files, docs, and tool outputs the model sees (and when to trim them)Orchestration – routing tasks to specialized subagents (planner vs. runner vs. reviewer)Safety boundaries – sandboxing, permissions, hooksTermination – knowing when the job is finished vs. stuck What can a typical agent harness look like: Agent harness How Agent Harness Can Be Achieved Agent harness cannot be achieved by a single library — it is a layered orchestration system built from Claude Code config files under .claude/, and CLAUDE.md as the control plane. The codebase needs to add rules for who does what, when to stop, and how information flows back. Control pane – CLAUDE.md + .claude/settings.jsonSkills – Orchestrated WorkflowSubagents – constrained workersInfrastructure – MCP tools + hooks Control Pane Considering we are using Anthropic's ecosystem, the main session is always Opus (planning, judgment, MCP). Heavy but repetitive work goes to Haiku subagents in isolated contexts. CLAUDE.md defines the routing table and hard rules — e.g., never run Playwright directly in Opus; always delegate to test-runner: Markdown ## Tiered model routing (Opus parent + Haiku runner) This project pins the main Claude Code session to **Opus** (`.claude/settings.json` → `model: claude-opus-4-7`) and delegates **test execution only** to a Haiku subagent defined in `.claude/agents/test-runner.md`. ... 1. When the user says "run", "execute", "verify", or "re-run" a scenario id / tag / feature file → invoke the `test-runner` subagent. Do not run `npx playwright test` directly in the main session. ... 4. If `failure_category` is `auth`, `config`, `network`, or `other` → do **not** self-heal. Surface the JSON report and stop. Skills: Orchestrated Workflows Skills are markdown playbooks the Opus session follows. They encode multi-step pipelines with explicit gates. /selfheal — failure → fix → verify: Markdown # Self-Heal Test 0. **Preflight.** Invoke `/preflight` ... 0.5. **Fixture check** ... 1. Run the failing scenario(s) via the `test-runner` subagent. 2. Inspect failures via MCP Playwright browser snapshot ... ... 5. **Invoke the `heal-edit-reviewer` subagent** ... 7. Identify **all affected scenarios** via `get_impact_radius_tool` ... 8. **Re-run in BOTH env1 and env2 — in parallel.** /codegen — CSV → BDD with planner/renderer split: Markdown 0. **Preflight.** ... 2. **Plan reuse via graph.** ... `semantic_search_nodes_tool` ... 6. **Invoke `codegen-writer`** with the BuildSpec. 7. **Audit the `.feature` file** via `feature-reviewer` ... 8. **Audit the emitted `.ts` files** via `heal-edit-reviewer` ... 10. **Smoke-run one scenario.** ... via `test-runner` ... Skills are the harness’s state machine — they define order, stop conditions, and which actor handles each step. Subagents: Constrained Haiku Workers Each subagent in .claude/agents/*.md is a specialized worker with a narrow tool set, fixed output schema, and hard prohibitions. Example — test-runner: YAML --- name: test-runner description: ... Does NOT self-heal, edit code, or modify any files. model: claude-haiku-4-5 tools: Bash, Read, Glob --- It must return strict JSON (~200 bytes) instead of flooding Opus with Playwright stdout: JSON { "status": "pass" | "fail", "exit_code": <number>, ... "failure_category": "locator-timeout" | ... | null, "stdout_tail": "<last ~30 lines of stdout/stderr>" } So the different subagents that the codebase harness are: Subagent harness MCP servers extend what the harness can touch: Playwright browser – live UI inspection for locator discovery and debuggingcode-review-graph – structural + semantic code index (avoid reading full files) Hooks in .claude/settings.json run automatically outside the agent loop: Hooks used in the Claude ecosystem Memory and Indexing: Two Complementary Systems “Memory indexing” here is not one thing. The project uses structural code indexing (knowledge graph) and persistent session memory (conventions/feedback). They solve different problems. Code Knowledge Graph (Structural Indexing) Plugin: code-review-graph v1.8.x Update cadence: Incremental after every Edit/Write via PostToolUse hooks (~2s typical)What gets indexed (Tree-sitter AST parsing): AST parsing output Each node stores: qualified name, file path, line range, parameters, return type, SHA-256 hash (for change detection). Semantic layer: After embed_graph_tool, all ~611 code nodes get 384-dim vectors (all-MiniLM-L6-v2). This powers meaning-based search — e.g., CSV step "Validate that the username field is present" matches Username.verifyUsernameFieldVisible() even when names differ. Primary MCP tools: Graph-first rule (from CLAUDE.md): Query the graph before reading files. Read narrowly with offset/limit only when you need exact insertion points. Session Memory (Governance & Conventions) Location: .claude/memory/ (initialized from .claude/memory-template/) Purpose: Persist decisions and feedback across sessions so the agent doesn’t re-learn conventions every time. Documented memory categories include: Session memory is always loaded alongside CLAUDE.md. It governs how the agent behaves; the knowledge graph governs what exists in the codebase. How Indexing Is Used in Practice During/codegen (CSV → BDD): Opus parses CSV → scenario titles and stepsFor each step: semantic_search_nodes_tool("<step text>") → reuse existing step or mark reuse: nullGraph checks existing page object methods, fixtures, scenarios before emitting anythingHaiku codegen-writer renders files from BuildSpec only — no judgment, no MCPReviewers gate output before lint/smoke run During/selfheal (locator failure): test-runner returns compact JSON (not raw stdout in Opus context)Opus scopes via graph: callers_of(brokenMethod) → read only affected stepsMCP browser diagnoses live UIAfter fix: get_impact_radius_tool → re-run only affected scenariosParallel test-runner on redama + fedramp During reviews: /code-review-graph:review-delta — changes since last commit, target <800 tokens context/code-review-graph:review-pr — full PR with blast-radius and test-coverage gaps Tiered Model Routing (Cost + Quality Governance) The harness explicitly separates judgment from execution: Tiered routing architecture Haiku costs ~1/15th of Opus per token — so even when total system tokens rise slightly, the dollar bill drops. Clean Governance of AI Usage Preflight Gates (Fail Fast, Don't Self-Heal the Unhealable) Governance here means predictable behavior, auditable outputs, and hard stops on bad patterns — not just “use AI less.” Separation of Concerns (No Guessing) If spec is ambiguous → error JSON, not a guess Hygiene Rules Enforced By Reviewers Context Budget Rules (9 Rules in CLAUDE.md) Graph-first queriesNarrow reads (offset/limit)Narrow MCP snapshotsDelegate bulk I/O to subagentsTrim subagent payloads (no stdout on pass)Reuse over rewrite (semantic search before authoring)Impact-radius re-runs (not whole suite)No echo-back of raw artifactsNo cat/head/tail on source Automatic Enforcement via Hooks From .claude/settings.json: Hooks run silently — governance without Opus token spend. Output Contracts (Structured, Not Prose) Every subagent returns strict JSON. The parent renders tables — never pastes raw JSON or full traces to the user. This is both UX governance and context governance. End-to-end: One failure, fully governed Conclusion The value of AI in test automation is not unlocked by choosing a larger model. It is unlocked by building a harness that knows what to delegate, what to validate, and what to keep out of the planner’s context. At a high level, four principles hold across any stack: Separate judgment from execution. One session plans, inspects, and decides. Specialized workers run tests, emit files, and audit changes — each with a narrow mandate and a structured response format. That boundary is what prevents a single conversation from drowning in logs and generated code. Validate before you execute. Credential sessions, environment profiles, and reference fixtures should be checked at the door. Failures that originate in setup or data drift must not enter a self-heal loop designed for code and locator problems. Fail fast, report clearly, and reserve automated repair for repairable categories. Index before you read. A maintained structural and semantic index turns broad codebase questions into small, precise queries. That reduces cost, speeds up triage, and keeps the planner focused on the lines that actually need to change. Govern every exit path. Delegation rules, audit gates, bounded heal attempts, parallel multi-environment re-verification, and human confirmation on data substitution are not bureaucracy — they are the difference between a demo and a production workflow. Taken together, these standards describe an agent harness that teams can adopt incrementally: start with an execution worker and structured JSON results, add preflight gates, introduce a code index, then layer in render and review workers as codegen volume grows. The model remains the reasoning engine throughout. The harness is what makes that reasoning trustworthy enough to run unattended, cheap enough to run often, and clear enough that engineers still know what happened when something goes wrong.
A Claude Code skill is a Markdown document that the assistant loads when a task matches its description. A collection of skills, agents, and hooks can be packaged as a plugin and distributed through a marketplace. Since the summer of 2026, my development environment has relied on such a plugin for Java work, consisting of a set of Spring Boot, Quarkus, Apache Camel, and Jakarta EE agents and skills. This collection started as a small set of artifacts but, little by little, I extended it such that, after several months, it contained 43 files and 17,338 lines. This raised a precise question: were the stack conventions provided by this plugin really contributing anything measurable to the final outcome, or could the model, by its intrinsic general knowledge, infer the same result with much fewer tokens? This article describes how the Anthropic evaluation tool answered the question, and what the measurements revealed about the skills and the evaluation method itself. A Criterion for Useful Skill Content A skill contributes when it changes what the model produces. Four kinds of content satisfy that criterion: Personal or team conventions, which the model has no means to infer.Facts more recent than the model's training data, such as new versions and renamed artifacts.Non-obvious pitfalls and the decisions that avoid them.Corrections to guidance that is wrong elsewhere, including in upstream plugins. Textbook material, such as the definition of an Enterprise Integration Pattern or the meaning of an HTTP status code, belongs to what the model already knows. Trying to provide this same knowledge in skills consumes between 5,000 and 8,000 tokens each time a skill loads. Additionally, the given skill might be outdated. For example, my files still referenced Spring Boot 3.2.1 months after the move to Spring Boot 4. Restructuring Before Measuring The first step was to collect all the loose folders in ~/.claude, defining agents, skills, and hooks, to remove the duplicates and to consolidate them in stand-alone plugins. The result is a marketplace named simplex-software that lists five plugins: PluginActivationspring-boot-templateper projectquarkus-templateper projectjava-extrasuser scope, every sessioncamelper projectjakarta-eeper project The stack plugins take precedence on every conflict. java-extras holds personal rules, cross-stack decisions, operational notes, and a UserPromptSubmit hook that reminds the model, on every prompt, which skills apply to the detected stack. Activation per project keeps stack material out of sessions that don't use it. The Measurement Method Claude Code provides claude plugin eval, an open-source evaluation tool allowing you to test the pertinence of your skills, agents, and plugins. For example, you might want to evaluate the efficiency of a given Claude Code plugin. To do that, you need to define "test cases," execution scenarios that specify an input prompt, the required context or tools, and the logic or criteria for determining success. A case is a directory with a prompt and a set of graders. The harness runs each case with and without the plugin, several times per arm, and reports the score difference. A case whose score difference stays near zero identifies a plugin that contributes little to the content, since the model can produce the same result without it. Such a plugin must be ablated because its skills add only tokens. Instead of creating new files in the project, each case asked Claude to provide the requested code and configuration directly in its answer. Therefore, they don't require a prepared sample project, write permission, etc. The graders examined the answer text. Each case combined three kinds of graders: Regular expressions for the specific facts the skill claims. These cost nothing.A judge model (Claude Haiku by default) with three votes for overall correctness.A tool_used grader that checks whether Claude invoked the skill through the Skill tool. It shows whether the plugin arm actually loaded the skill. It is excluded from the score because the arm without the plugin has no skill to invoke and would fail it in every run, which would inflate the difference. A case for the Camel plugin illustrates the format. The prompt file: Markdown --- name: quarkus-route-test tags: [camel, quarkus] max_turns: 8 allowed_tools: [Skill, Read, Glob, Grep] --- Quarkus 3 project using Camel Quarkus. A RouteBuilder defines route id `process-route` from `direct:process` to `kafka:processed-orders`. Write a test that verifies one message reaches the Kafka endpoint without a real Kafka broker being used for that endpoint. Include the Maven test dependencies. Its regular expression grader, graders/test-artifact.md: Markdown --- type: regex pattern: "camel-quarkus-junit(?!5)" match: contains --- Uses camel-quarkus-junit, the successor of the deprecated camel-quarkus-junit5. Its judge grader, graders/judge.md, whose body is the criterion the judge model applies to the answer: Markdown --- type: llm --- Pass only if ALL hold: - test dependency is org.apache.camel.quarkus:camel-quarkus-junit (not camel-quarkus-junit5) - test extends CamelQuarkusTestSupport, overrides isUseAdviceWith() to return true, advises route "process-route" (mockEndpoints / mockEndpointsAndSkip) and then starts the context - does not use @MockEndpoints (not available in Camel Quarkus) - no Thread.sleep Its skill indicator, graders/skill-fired.md, which passes when Claude loads a skill whose name contains camel: Markdown --- type: tool_used tool: Skill input_match: "camel" --- Indicator only: the plugin skill was loaded. The command runs every case of a plugin, three runs per arm, with a cost ceiling: Shell claude plugin eval . -j 3 --no-publish --max-cost-usd 5 First Results Ten cases covered the three own plugins. The table reports the mean score with the plugin, without it, and the difference. Two scores are corrected for grader defects described in the next section. PluginCaseWithWithoutDifferencecamelCamel Quarkus route test1.000.33+0.67camelSpring Boot route unit test1.000.33+0.67camelSpring Boot Kafka integration test1.000.92+0.08jakarta-eeintegration test on WildFly0.890.33+0.56jakarta-eepom setupequalequal0jakarta-eeresource, service and injectionequalequal0java-extrasCircleCI with Testcontainers1.000.33+0.67java-extrasgraceful shutdown on Kubernetes1.000.50+0.50java-extrasSpring Boot to Quarkus migration1.000.89+0.11java-extrasReactor to Mutiny translation0.560.67about 0 The cases with a large difference show that the plugin had a consistent contribution to the final result. In the case of Camel, for example, without the plugin the model estimated the Maven artifact version as 4.18.0, whereas the skill states 4.23.0, built on Spring Boot 4.1.1. It also selected the test artifacts camel-test-spring-junit6 instead of camel-quarkus-junit with the skill. For CircleCI it chose a plain Docker executor, which offers no Docker daemon to Testcontainers. For graceful shutdown, it omitted the arithmetic that relates the preStop delay, the application shutdown timeout and terminationGracePeriodSeconds. The cases with a difference near zero show the opposite pattern. The Reactor to Mutiny cheat sheet, the Jakarta EE pom and the CDI constructor injection rules describe knowledge the model already applies correctly. Reading the answers confirmed it: both arms produced the same code. Trimming, and the Regression the Evaluation Caught The trimming pass removed content with near-zero differences and reduced the rest to facts, decisions, and pitfalls. The Reactor to Mutiny section disappeared. The Spring Boot to Quarkus table kept only the rows where a mistake is likely; the Jakarta EE setup became a short list of facts and The Camel Kafka test skeleton became one sentence. The second run, after trimming, confirmed that every case with a large difference scored 1.00 with the plugin. Only the migration from Spring Boot to Quarkus case declined from 1.00 to 0.78, but part of the decline came from judge noise. However, a part was genuine: the shortened table row stated that the Quarkus @Transactional annotation has no readOnly attribute, which led the model to mistakenly remove the read-only behavior of the original method. Without the skill, the model preserved it through a read-only Hibernate session. The corrected rule prescribes that the intended behavior should be achieved via a read-only Session with FlushMode.MANUAL, or a org.hibernate.readOnly query hint. A third run of that case, judged by Claude Sonnet with a criterion that checks the read-only behavior, passed six runs out of six. The episode is the strongest argument for the evaluation: a detail that appeared harmless changed the generated code, and only the measurement detected it. Final State and Cost MeasureBeforeAfterAgent and skill files436Lines of agent and skill content17,338638Tokens added to every sessionabout 2,100about 520, plus about 150 for each optional plugin enabled in the projectEvaluation casesnone6, each with a measured difference The complete exercise, including the exploratory run, both full runs and the verification run, cost about 20 US dollars in model usage. A full run of the remaining suite costs about 6 dollars and takes about five minutes with three parallel runs. Lessons The useful content of a skill is narrow: versions, renamed artifacts, corrections and non-obvious conventions. Explanations of well-known frameworks contribute nothing measurable.An evaluation with and without the plugin is inexpensive and decisive. It replaces a debate about relevance with a number per case.Graders require the same scrutiny as the skills, and a small judge model is inconsistent on long answers.A reduction of content is a change like any other and deserves a regression test. The readOnly episode demonstrates it. Limitations Each arm ran three times, which indicates a direction rather than a statistically established effect. The results describe one model at one date. Here, our Camel plugin pinned version 4.23, which a later model may know without assistance. This is precisely why the suite should run again after each model change. The cases reflect one developer's projects, and the personal rules in `house-rules` were deliberately left out of the evaluation, since the model has no means of guessing them.
Every GitOps setup meets the same moment: something is on fire, someone runs kubectl scale on prod, and the fix works. What happens next depends on two settings that rarely get checked before an incident: self-healing on the GitRepo, and pause on the cluster. I ran all three combinations in a lab on Rancher 2.15.1 with Fleet (biased, I work at SUSE). Three EKS clusters: dev and staging on Kubernetes 1.36, prod on 1.35. Cases 1 and 2 used the guestbook app from the public rancher/fleet-examples repo, with the frontend Deployment at 3 replicas. Case 3 used the podinfo Helm chart from my own repo. Every manual change went through the scale button in the Rancher UI, which sends a PATCH to the Deployment's scale subresource, the same request kubectl scale makes. The Two Settings Self-healing is spec.correctDrift.enabled on the GitRepo. It is off by default. In the Rancher UI it sits in step 4 of the GitRepo wizard, Advanced, Resource Handling, "Enable self-healing". To change it on an existing GitRepo: Continuous Delivery, App Bundles, the GitRepo, the three-dot button top right, Edit Config, then Next until step 4. Pause lives on the cluster: Continuous Delivery, Clusters, three-dot menu, Pause. Fleet stops deploying to that cluster until you unpause it. 1. Self-Healing On Turning self-healing on for a GitRepo that already had drift fixed the drift in about 11 seconds. Then I scaled the frontend from 3 to 1 again. Fleet had it back at 3, with 3 pods ready, in under 7 seconds. The GitRepo flickered NotReady for about 3 seconds and went back to Active. Anyone watching the UI would likely never see the drift. That is what you want from GitOps on a normal day. During an incident it works against you: your fix is gone before you can tell whether it helped. 2. Self-Healing On, Cluster Paused I paused the dev cluster and scaled the frontend to 5. It held, 5 of 5 ready, for the twelve minutes the cluster stayed paused. While the cluster was paused, the GitRepo read Ready, 2/2 clusters, with no Modified state anywhere. The app looked healthy in Continuous Delivery while one cluster ran something Git did not say. If you review drift by looking at the GitRepo, a paused cluster drops out of the review. When I unpaused, the frontend went from 5 back to 3 in about 9 seconds. The GitRepo showed Modified for a moment and then Active. The fix lasted exactly as long as the pause. 3. Self-Healing Off With self-healing off, the manual scale stays, and Fleet reports it precisely. I scaled prod from 2 to 4 and the GitRepo went to Modified, 2/3, and stayed there. The Ready condition read: Modified(1) [Cluster fleet-default/c-kqgtk]; deployment.apps shop/shop-podinfo modified {"spec":{"replicas":2} That is the best drift report in the product. Two details to know when you read one: it names the cluster by its ID rather than its display name, and the number it shows is the one Git wants. The value currently on the cluster is missing. The surprise came with the next commit. The repo deploys one app in two lanes: two cluster groups, non-prod and prod, and one fleet.yaml with prod pinned a chart version behind. Promotion is a one-line commit that bumps the prod target. I promoted podinfo 6.14.0 to 6.15.0 while the manual scale was still in place. The GitRepo sat in WaitApplied, 2/3, and prod stayed on the old version with 4 replicas. The Helm release on the prod cluster recorded the reason: Upgrade "shop" failed: conflict occurred while applying object shop/shop-podinfo apps/v1, Kind=Deployment: Apply failed with 1 conflict: conflict with "Mozilla" with subresource "scale" using apps/v1: .spec.replicas This is server-side apply doing its job. The manual scale made the browser's client the field manager of spec.replicas. "Mozilla" is the user agent the browser sent, so managedFields records the client that made the call. The person who clicked appears nowhere in the object. From a terminal the manager would be kubectl. When Fleet's upgrade tried to set spec.replicas back to 2, the API server refused to take the field from another manager without being forced. None of that showed in Continuous Delivery. The GitRepo showed WaitApplied and the drift message. The conflict text existed only in the failed Helm release on the downstream cluster. The fix needed no force. I scaled prod back to 2, the value Git asks for, and the promotion landed about 40 seconds later: new chart version, 2 of 2 ready. The Kubernetes docs explain why that works. When two managers set a field to the same value, they share ownership and there is no conflict. What to Check on Your Own Clusters Read the setting before the incident. Check spec.correctDrift.enabled on each GitRepo that targets prod, so you know which of the three cases you are in.If you pause a cluster, keep a list of what you paused. Continuous Delivery will not show drift on it until you unpause.If a promotion sits in WaitApplied after an incident, look on the downstream cluster. Run helm history on the release and read the description of the failed revision.Inspect ownership with kubectl get deployment <name> -n <namespace> -o yaml --show-managed-fields. A manager like "Mozilla" or kubectl next to a field that Git also sets is a manual change waiting to block a deploy. Limits of This Test One run, on Rancher 2.15.1 with Fleet and EKS 1.35 and 1.36. Every timing is a single measurement, and the GitRepos polled Git every 60 seconds. I scaled from the UI, not from kubectl in a terminal. I did not test correctDrift.force, which the Fleet docs say can recreate resources, or Helm's force option for this case. After the fix, the browser client may still co-own spec.replicas. Per the server-side apply docs, a later commit that changes replicas could conflict again. I did not test that. What I'd Take From It Pause is a working break-glass. Use it, and keep in mind that while a cluster is paused, Continuous Delivery tells you nothing about it.Put the fix in Git before you unpause, or you lose it.With self-healing off, an emergency change that never makes it back to Git blocks your next deploy. If a promotion sits in WaitApplied after an incident, look for a field someone changed by hand. This is a lab finding, not official guidance.
TL; DR: The Product Backlog Refinement Agent and the Risk When a Suggestion Becomes a Decision Preparing a refinement session is tedious, and AI is good at the tedious part: reading every Product Backlog item, every repeated comment column, and the team’s own records on the afternoon before. The risk sits elsewhere. A refinement agent asked to “help with refinement” can take the conversation away from the team without writing a single acceptance criterion: “suggesting” to the Product Owner or Product Manager which items to close, merge, split, or create is enough, and the questions the team was supposed to work out arrive in the brief already answered. The remedy is a boundary decided before the first run: the agent marks evidence and asks questions, the Product Owner checks the brief and sets the agenda, and the team refines. Writing that boundary into an A3 Handoff Canvas, together with what the agent reads and who checks its output, turns a good intention into a rule the agent follows and the Product Owner can verify. Thesis: This article explains how Product Owners and Product Managers can use AI to prepare Product Backlog refinement without pre-empting the team’s conversation: four evidence checks, a four-part test separating suggestions from decisions, written boundaries, and human verification of the agent’s brief. Disclaimer: I read Charniak/McDermott’s book on “Artificial Intelligence” decades ago; of course, I use AI for research, translations, proofreading, challenging story arcs and article structures, creating artifacts such as graphics, and summarization. It is a production tool, not a substitute for thinking. The Danger of a Preparation Turning Into a Decision The familiar worry about AI in Product Backlog refinement is that a model writes the Product Backlog items, the acceptance criteria, and the estimates, and a tired team nods them through on a Tuesday afternoon. The worry is justified; however, it describes the easy case, because a drafted acceptance criterion is visible, and a team would usually notice that someone else did its homework. The more challenging case looks like help. Consider a hypothetical brief by a refinement agent that contains no descriptions, no acceptance criteria, and no new order, yet recommends closing one item as a duplicate, splitting another, creating a third, and calling four items “ready candidates” for the next Sprint. Every line reads as friendly advice to the Product Owner, and every line settles a question before the team has discussed it. A recommendation decides nothing until someone accepts it and acts on it. Once it sits on the agenda, however, the team is likely to discuss the recommendation instead of the question behind it, and that is how preparation quietly turns into a decision. It sneaks in through the back door, feeding on the human desire to avoid unnecessary work when organizational pressure is already high. What Refinement Is For, and What Preparation Should Leave Alone The Scrum Guide describes refinement as “an ongoing activity to add details, such as a description, order, and size,” which captures the mechanics and leaves out the purpose. I see refinement as the final part of product discovery, once hypothesis validation has been established: the team asks whether an item is really the best use of its time or whether something else would create more value for customers. A Product Backlog item is a token for that discussion, an invitation to align, and a good session ends with shared understanding sufficient to make the next investment decision, including the decision not to build something. Many teams hold a regular session for this, and often those sessions turn into a hunt for problems someone could have spotted the day before: an item a stakeholder insists on moving to the top, a backlog item without acceptance criteria that everyone expected someone else to write, an item nobody has touched for 6 months. Preparation exists to spare the team that kind of administrative work, and reading a Jira export and the team’s notes on the afternoon before is exactly the kind of thorough, tedious work an AI agent does well. The question is where preparation ends. In September, I described ten anti-patterns that get worse when teams add AI to a dysfunctional Product Backlog process, “Refinement Without the Team” among them, and an assistant that hands the Product Owner ready-made resolutions is a polite version of that anti-pattern: the team still meets, yet the decisions it was supposed to reach already arrive pre-made. In my view, the most critical consequence of dysfunctional refinement is a team that loses its ability to have productive discourse. Typically, building the wrong thing follows; think of lack of return on investment and opportunity costs. So my rule for such a refinement agent is short: mark, suggest, never solve. The agent marks what the team needs to talk about and may suggest what to discuss, and solving stays the team’s job. Four Things a Product Backlog Refinement Agent Can Check, and What It Cannot Know Of the 27 Product Backlog and refinement anti-patterns I have described, many are behavioral and leave no trace in a Jira export. Four do leave traces, provided the agent reads the team’s own records next to the export: the Product Goal, the product roadmap with its change log, the anti-product backlog, the notes from previous Sprint Reviews, and the actions from the last few Retrospectives. Readiness The agent looks at items early in the current order and reports what the records contain: whether an item states a problem or an intended outcome, whether it lists open questions, and whether a Product Backlog item waits for an open spike for technical clarification. Whether that is enough for a useful conversation remains the Product Owner’s judgment. A missing acceptance criterion at the start of refinement is no defect; it is an agenda question, because the Product Owner will have ideas about how an item should meet the desired outcome, and the team negotiates the criteria together by the end of refinement. Being early in the order does not preselect an item for the next Sprint, either: the order reflects the team’s best current judgment of how to use its time, and in Sprint Planning, the team first agrees on the Sprint Goal, and then the Developers select Product Backlog items they deem necessary to accomplish the goal. Goal Fit and Prioritization by Proxy The agent compares the Product Backlog with the Product Goal, the roadmap, and the anti-product backlog, which lists the ideas and requests the team decided not to build, either not now or not in this product, with the reason for each decision. It marks items that serve no part of the Product Goal and items that revive an idea the team already rejected. It also marks prioritization by proxy, which an export reveals only indirectly, because no outsider has write access to the Product Backlog: a stakeholder can neither create an item nor change its position. What the export does show is a member of the Scrum Team, usually the Product Owner, creating or moving an item on a stakeholder’s behalf: a comment such as “per the sales director’s request,” a label such as “mgmt-request,” a creation date right after an escalation, and no discovery decision and no link to the Product Goal behind the item. An item that skipped discovery because someone with political weight asked for it was never put to the validation test. However, the agent cannot know what the conversation behind the item looked like, or whether a rejected idea deserves a second look; that takes the people involved. Outdated Items Items nobody has touched for months, or that still name a release that shipped long ago, deserve a look, because a Product Backlog that turns into a storage for ideas hides the work the Product Goal needs. Age is a signal for inspection, nothing more: an untouched item can be the very work the Product Goal depends on, and a refinement agent that simply sorts by the “Updated” column will happily declare it dead without inspection. Missing Work The Scrum Guide calls the Product Backlog “the single source of work undertaken by the Scrum Team.” The agent can hold that claim against the team’s records: open items from the Sprint Review, Retrospective actions, and even work done outside the Sprint Backlog. A discrepancy is a finding. Whether it leads to a new item is for the Product Owner and the team to decide, because the work may sit in another team’s Product Backlog, stay outside it on purpose, or already be resolved. This check is also why the agent needs more than the export, since an export cannot show what is missing from it. For any of the four checks, “no supported finding” and “insufficient evidence” are valid results. However, the teams that would gain most from this preparation often keep the fewest records: no anti-product backlog, no product roadmap change log, Sprint Review notes that say little more than “stakeholders loved what we built,” and the reasons behind past decisions stored in the heads of the Product Owner and a few senior engineers, or buried in Slack threads. Such a team will get a brief full of “insufficient evidence.” That brief is still a finding because it shows where the team has no memory of its own decisions. Also, it helps only when people read it as a statement about their records rather than a failed run. Otherwise, the team has added AI on top of a dysfunctional system. A small start is enough to make the checks useful: the Product Goal, the last Sprint Review notes, and the last Retrospective actions all cover readiness and missing work, and a short list of recent “no” decisions, one line of reason each, gives the goal-fit check something to work with. In the agent’s instruction, name only the records the team actually keeps, so the brief does not report the same missing roadmap change log on every run. Where a Suggestion Turns Into a Decision A hypothetical example shows where this red line runs; the item keys, people, and dates are invented. A Jira export taken on Monday shows EXP-187, “Export for enterprise accounts,” created last Tuesday by the Product Owner, in the second position of the order, with the Priority field set to “Highest,” a one-line description, and the Product Owner’s own comment “Per the sales director’s request, moved to the top; a big customer is waiting.” The roadmap’s change log keeps the export feature in “Next,” no discovery decision mentions the request, and EXP-112, a CSV export item from June, may cover part of the same need. A brief that does its job marks this as prioritization by proxy and cites its sources: the comment, the creation date, the position in the order, the roadmap entry, and the missing discovery decision. It adds what it cannot know (which customer, and what problem that customer has), and it may suggest that the Product Owner revisit the request with the sales director before refinement. A brief that crosses the line writes “Close EXP-187 as a duplicate of EXP-112,” or “Move EXP-187 back down the order,” or “Ready candidates for the next Sprint: EXP-187.” Each of these settles a decision that belongs to the Product Owner, however helpful it sounds. What follows is the Product Owner’s work, and most of it happens outside refinement. The sales director’s request is exactly what my discovery article describes as a passive source: “Product requirements from internal stakeholders, which arrive unrequested and usually with political weight attached.” It goes through the funnel I described in AI-Assisted Product Discovery, where only what serves the general product vision and the current product goal passes the neck and earns a place in the work: it may become a hypothesis to validate with an experiment, an entry in the anti-product backlog with its reason, or an item the Product Owner places where it belongs. Until then, EXP-187 has no claim on the second position, until the Product Owner reconsiders the order of the Product Backlog. A sales director who can push an item to the top through the Product Owner points to an impediment for the Product Owner and the Scrum Master. Also, it is not an agenda item for the Developers, as refinement time stays reserved for work that has legitimately entered the Product Backlog. Questions that belong in refinement need the same discipline. Take a story that waits for a spike that is still open: “What do we need to learn from the spike before we can negotiate the story’s acceptance criteria?” leaves the conversation to the team. “Should we split the story by permission level?” ends with a question mark, yet it carries a solution, and it leaves the team to say yes or no to an answer the agent chose. A valid refinement suggestion by an AI agent has to pass four tests: It rests on the supplied records,It makes the uncertainty visible,It leaves materially different answers open, andIt does not assume the disputed resolution. Why be this strict about a polite suggestion? A ready-made answer shapes the judgment of the person who reads it. In two preregistered experiments with 2,582 participants, Sterling Williams-Ceci, Mor Naaman, and colleagues found that people writing about societal issues with a biased AI writing assistant shifted their attitudes toward its position, most of them unaware of the bias and its influence, and warnings before or after the task did not mitigate the shift (Biased AI Writing Assistants Shift Users’ Attitudes on Societal Issues, Science Advances, 2026). The study tested autocomplete suggestions during writing, which influenced people more than similar arguments shown as a static list. An AI-assisted refinement brief is a static list, so the study is a reason for caution and does not prove that a brief anchors a Product Owner. Writing the Line Down Before the First Run Writing the red line down before a refinement agent runs for the first time turns it from an intention into a rule. The A3 Handoff Canvas is the place for it: it records what the agent reads, what its brief looks like, who checks it, what the agent must never do, and the last part goes into the agent’s instruction word for word. For an AI-assisted refinement agent, the stop rules read: Never write acceptance criteria,Never estimate,Never split, reorder, or remove an item, and never move one to the anti-product backlog,Never advise closing, merging, splitting, creating, or editing an item; ask the team a question insteadNever propose what goes into the next Sprint or what the Sprint Goal should beNever invent a source, a reason, or a figure. (If a record needed for a check was not supplied, cannot be located in the supplied material, or cannot be read, state that limitation under “What I could not check.” Do not infer that the record does not exist. Do not treat an experiment, proposal, or unverified note as a completed decision or shipped release; state the exact status recorded in the source.) Three of these rules deserve a word. Acceptance criteria stay out because the team negotiates them by the end of refinement, and an agent’s draft would preempt that negotiation; a different workflow can still help with criteria after the team has talked. Estimates stay out because their value lies in the Developers’ discussion that reveals diverging ideas of the effort before work starts, as I argued in Estimates Are Useful, Just Ditch the Numbers; the number was never the point. And the last rule addresses a quieter error: a model that reads a planned experiment as a commitment, or a hypothesis as a result, turns the team’s open question into a fact nobody decided. In terms of the A3 Framework, this preparation is Assist: “AI expands options. You own the outcome.” The refinement agent works from a read-only export, so it cannot change the Product Backlog even if a rule fails. And if you already run an agent for product discovery, it needs its own Handoff Canvas, because it reads different inputs, delivers different output, and stops at a different line; I consider that AI transparency at the operational level. The Product Owner Routes Every Finding A brief that sounds thorough invites a dangerous shortcut: forwarding it to the team unread. Polished AI artifacts quickly fool people into believing that the AI is doing the real job. So the Product Owner opens the item or record behind every finding that could change the agenda and then looks at a few things the brief did not flag, for example, the items nearest the top that it never mentions, because a brief cannot report its own omissions. Then the Product Owner decides where each finding goes, and only some of them belong in refinement: Settled by the Product Owner: Housekeeping, such as an obvious duplicate created by mistake or a release field nobody updated, and restoring the order of the Product Backlog.Sent into discovery: Requests that skipped the funnel, such as the sales director’s export, and rejected ideas whose revisit condition may have occurred.On the refinement agenda: Questions that need the team’s knowledge, such as what a Product Backlog item needs before the team can negotiate its acceptance criteria.Raised as an impediment: Systemic causes, such as stakeholder pressure that bypasses discovery or records the team does not keep, for the Product Owner and the Scrum Master, or for the next Retrospective. Sharing the agenda with the Developers and the Scrum Master before the session gives them the chance to add the questions the brief missed. If a brief proposes resolutions anyway, correct the instruction and run it again; if it keeps failing, prepare the agenda by hand, the way the Product Owner hopefully did before the agent existed. The remaining failure mode relates to the Product Owner: Treating the brief as sufficient grounds to settle a question that needs the team’s knowledge brings back the very problem the stop rules were meant to prevent, only with a human signature. The brief must not become a substitute for a discussion that has not happened. Conclusion: Try It Before Three Refinements, Then Decide Treat the first runs as a local experiment. Note how long your usual preparation takes, then run the refinement agent on a sanitized export, in an AI account your company has approved, before three or four refinements. Record the total time, including checking the findings that proved useful, the findings the records did not support, and the omissions you noticed, knowing you won’t catch all of them. If checking and correcting the brief keeps taking longer than preparing the agenda by hand, stop, because the agent then costs more than it saves. Otherwise, ask the team whether refinement gained time for the critical discussion, and compare the results with your usual preparation; a few sessions give you the basis to decide whether to continue, adjust, or stop. If you would rather practice first, the AI4Agile Online Course v4, which launches on November 27, 2026, includes an exercise in which you build this refinement preparation agent: you fill its A3 Handoff Canvas, run it on a 40-item Product Backlog export and the team records of MegaBrain.io, the course’s fictional startup, check its brief, and set the Product Owner’s agenda before comparing your decisions with my worked solution. When the next brief arrives on your desk the afternoon before refinement, which of its suggestions already contains an answer your team has not given yet? Key Questions This Article on the Product Backlog Refinement Agent Answers How Can AI Help a Product Owner Prepare Product Backlog Refinement? A refinement agent can read the Product Backlog export and the team’s records on the afternoon before refinement and mark Product Backlog anti-patterns: items not ready for a conversation, items that serve no part of the Product Goal or entered at a stakeholder’s request without a discovery decision, outdated items, and work the Product Backlog does not show. The Product Owner checks the brief and routes each finding; the team refines. What Can an AI Agent Check in a Product Backlog Before Refinement? Four checks leave traces in an export and a team’s records: readiness, goal fit, outdated items, and missing work. Prioritization by proxy shows only indirectly, because no outsider can write to the Product Backlog: an item the Product Owner created or moved up at a stakeholder’s request, visible in comments, labels, and a missing discovery decision. None of the checks can establish what the team understands or still wants; the people involved need to do that. How Do You Tell an Open Question From a Disguised Recommendation? A question passes four tests when it rests on the supplied records, makes uncertainty visible, leaves materially different answers open, and does not assume the disputed resolution. “Should we split the story by permission level?” fails despite its question mark, because it carries a solution. “What do we need to learn from the spike before we can negotiate the acceptance criteria?” leaves the answer to the team. Should a Refinement Agent Write Acceptance Criteria or Estimates Before the Team Refinement? Not in a preparation brief. The Product Owner brings ideas about how an item should meet the desired outcome, and the team negotiates the acceptance criteria together by the end of refinement; a refinement agent’s draft would preempt that negotiation. The value of an estimate lies in the Developers’ discussion of diverging ideas of the effort; the Scrum Guide assigns sizing to “the Developers who will be doing the work.”
Finding an eval() call in a Python repository is easy. Understanding where its argument comes from takes more work. The same problem appears with shell commands. A scanner can point to subprocess.run(), but a reviewer still needs to trace the command through assignments, string formatting, and branches. Then comes the harder question: can someone outside the intended trust boundary control that input? I built rce-path to make that investigation more structured. It scans a local Python checkout, identifies modeled execution sinks, and records the input flow it can establish from the source. Each finding includes locations, ordered evidence, and the assumptions that still need review. The package also supports optional OpenAI and Ollama annotations. Static scanning works independently, so you can inspect the evidence before deciding whether to involve a model. The Problem I Wanted to Address A suspicious function call is a starting point for security review. It leaves several questions unanswered: Where did the argument originate?Which assignments or expressions carried it to the execution call?Does the path depend on a branch or an unknown value?Is the source request data, a function parameter, or local input?Which parts of the flow remain unresolved? These details matter because a function parameter is not necessarily remotely controlled. A request-derived value may pass through authentication or validation outside the scanner's view. A shell invocation using fixed literals needs a different investigation from one assembled from request data. My goal with rce-path is to give the reviewer a traceable path and make the remaining uncertainty visible. The Core Idea The current release, version 0.1.0, uses Python's abstract syntax tree and per-scope control-flow analysis to track modeled inputs toward execution sinks. A source is an input origin the analyzer recognizes. A sink is an operation that can execute Python code or a shell command. Between them, the analyzer follows supported assignments and expressions, including concatenation and f-strings. It also handles branches, early returns, and bounded loop analysis. The report preserves the steps connecting these operations. If a call's return value cannot be resolved, the scanner records that uncertainty rather than filling the gap with an assumed data flow. Targets are read as source data. A normal scan does not import the target package, install its dependencies, invoke its build hooks, or execute its code. What the First Release Covers The initial sink rules cover: eval() and exec(), including supported built-in aliases.os.system() and os.popen().Shell execution through subprocess.run(), call(), check_call(), check_output(), and Popen().subprocess.getoutput() and getstatusoutput(), which imply shell execution. Modeled sources include function parameters and Flask request inputs such as request.args, request.form, request.data, and request.get_data(). Environment variables, command-line input, stdin, and file reads are categorized as local inputs. The subprocess rules account for shell behavior. An unknown shell value introduces an explicit condition. Cases using shell=False have coverage limitations; they should not be interpreted as a general safety guarantee. This is a focused first release. It lets researchers and Python developers investigate supported execution paths without requiring an AI backend. A Small Example Consider this deliberately unsafe example. Save it as demo.py, but do not run it as an application: Python from flask import request import subprocess def lookup(): host = request.args.get("host", "") command = f"nslookup {host}" subprocess.run(command, shell=True) The security question is whether request data reaches a shell command. Here, the analyzer can model the request source, its assignment to host, its inclusion in the f-string, and the assignment to command before the shell invocation. There is no route registration in this snippet. It illustrates a data-flow pattern; it does not establish that the function is deployed or reachable over a network. To try rce-path, use Python 3.11 or newer and install the package from PyPI: PowerShell pip install rce-path Save the example as demo.py, then generate a Markdown report: PowerShell rce-path scan demo.py --format markdown --output report.md For this example, the report identifies the subprocess.shell rule at line 7, with static_path evidence and a network_request source. Its reachability field is network_source_modeled. The evidence follows the request read on line 5, assignment to host, f-string construction on line 6, assignment to command, and the execution sink on line 7. Those fields need careful interpretation. network_source_modeled means the analyzer recognized a network-input pattern. Authentication, deployment exposure, and actual exploit feasibility remain questions for human review. Using the Python API The command is named rce-path; the Python import is rce_path. The same scan is available through the public API: Python from rce_path import ScanConfig, scan if __name__ == "__main__": report = scan( "demo.py", config=ScanConfig(ai_enabled=False), ) print(report.summary) for finding in report.findings: print( finding.rule_id, finding.location.file, finding.location.line, ) for step in finding.steps: print( step.kind, step.location.line, step.description, ) The report and findings are typed Pydantic records. You can export them with report.to_json() or report.to_markdown() and use the structured fields in your own review workflow. Each finding carries a fingerprint, source category, evidence tier, source locations, and review state. Conditions, assumptions, and unresolved calls are available alongside the evidence. The report also records analyzed and skipped files, source content hashes, and coverage diagnostics. Fingerprints exclude the absolute checkout root and AI annotations, which helps compare the same finding across equivalent checkouts. They are location-based, so moving code can change a fingerprint. Where AI Review Fits An LLM can explain a finding, identify follow-up questions, or assess whether the available evidence supports an interpretation. In rce-path, that review is an optional annotation layer. Both provider implementations use a common evidence-and-review contract. Responses are validated, including references to evidence IDs. AI annotations do not rewrite the static finding or change its fingerprint. For a local Ollama review, install the extra: PowerShell pip install "rce-path[ollama]" Start your Ollama server and choose a model you already have installed: PowerShell rce-path scan demo.py --ai ollama --model YOUR_MODEL The default endpoint is http://127.0.0.1:11434. The package does not automatically download a model or fall back to a cloud provider. For OpenAI, install the corresponding extra and set OPENAI_API_KEY in your environment: PowerShell pip install "rce-path[openai]" rce-path scan demo.py --ai openai --model YOUR_MODEL --allow-remote-code YOUR_MODEL is a placeholder for an operator-selected model that supports the provider's required structured output. OpenAI and non-loopback Ollama endpoints require explicit permission to transmit code. The package sends bounded evidence slices and applies best-effort redaction of known secret patterns. Redaction can miss sensitive content, so review what you permit it to transmit. OpenAI requests use store=False, which is not a promise of zero retention. Review failures stay visible. A refused, unavailable, invalid, or budget-exceeded review is reported as unsuccessful rather than being presented as a completed assessment. Running It in a Review Pipeline For machine-readable output, scan a local checkout and save JSON: PowerShell rce-path scan ./checkout --format json --output findings.json You can also choose a findings policy: PowerShell rce-path scan ./checkout --fail-on static-path This fails the selected policy when the report contains a modeled static path. It does not mean the package has confirmed an exploitable vulnerability. The CLI uses three exit codes: the scan completed and the selected policy passed.the findings policy failed.an operational error, incomplete scan, or failed requested AI review occurred. The default policy is --fail-on none. Completion status takes precedence over the findings policy, so an incomplete scan cannot be treated as a successful clean scan. A repository-level rce-path.toml can configure rules, exclusions, and analysis budgets. Repository configuration cannot enable AI or grant permission to transmit code; those decisions require runtime opt-in. Current Limits rce-path uses bounded static analysis. Cross-function and cross-file flow, dynamic dispatch, closures, complex heap mutation, comprehensions, and precise exception handling are outside its current coverage. Branches are joined conservatively. A reported path can include conditions that are not jointly feasible at runtime. An unknown function is not automatically treated as a sanitizer, and a guard does not by itself prove an argument safe. For that reason, static_path means the analyzer established a path within its models. A sink_only finding identifies a sink whose input flow remains unresolved. Neither establishes remote code execution, and the package assigns no automatic CVSS score or exploitability probability. Read the coverage diagnostics alongside the findings. An empty completed report means "No modeled findings," not that the repository has been proved secure. What Comes Next The roadmap includes bounded local call summaries, additional framework and deserialization models, SARIF output, and separate human-approved validation workflows. These capabilities are planned for later phases and are not implemented in the current release. For now, I want rce-path to make a specific part of vulnerability review easier to inspect: how an input reaches an execution operation, what evidence supports that path, and what remains unknown. Start with a small file whose behavior you understand. Read the evidence steps, check the assumptions, and then apply the same workflow to a local repository. That is the most useful way to judge whether the package fits your research or code-review process. Project Links PyPIGitHubUsage and limitations
Every generation of technology arrives with the same expectation: this time, organizations will transform successfully, absorb the new technology, and reap the benefits. And sometimes they do. Yet after decades of technological progress, one pattern remains consistent. Organizations continue to spend valuable resources and dollars on technology initiatives that fail to achieve their intended outcomes. They fail because organizations attempt to automate what they don’t understand. They fail because the strategic purpose is not defined, change-readiness is not assessed, cultural barriers are not explored, and the outcomes expected are not clarified. Projects overrun. Adoption stalls. Strategic priorities drift. Employees become cynical. Competitive advantage slips away. A Pattern That Repeats With Every New Technology The history of business technology is a narrative of change. Mainframes gave way to personal computers. Enterprise applications consolidated isolated systems. The Internet created a global network. Cloud computing redefined infrastructure. Agile promised a better way to build software. Today, artificial intelligence promises another transformation. Technology is tangible. It can be purchased, implemented, configured, and deployed. Organizational clarity cannot. It requires conversation, alignment, judgment, and difficult choices. Those choices are often postponed because technology appears to offer a shortcut. Technology does not resolve organizational ambiguity. It exposes it. What Technology Can’t Decide For You An ERP system cannot decide how purchasing should work. A CRM system cannot define what constitutes a customer. Analytics cannot settle disagreements about which metrics matter. Artificial intelligence cannot compensate for unclear objectives or conflicting priorities. Every technology faithfully reflects the decisions that came before it. When those decisions are thoughtful, technology amplifies them. When they are confused, technology scales the confusion. This lesson has resurfaced in every technological era. The industry has repeatedly searched for technical solutions to organizational problems. We debated Waterfall versus Agile. On-premises versus cloud. Build versus buy. Low-code versus custom development. Today we debate copilots, agents, and autonomous systems. These are important decisions. None of them substitutes for clarity. Methodologies improve execution. Platforms expand capability. Artificial intelligence accelerates delivery. But no methodology, platform, or model can determine why an organization is changing, what success should look like, who owns the decisions, or how people will ultimately work. Clarity Before Technology This is why our engagement philosophy begins with clarity before technology. Clarity comes from ensuring that before the conversation turns to technology, software, infrastructure, or AI agents, leaders should have clear answers in the following dimensions: Purpose: Why are we making this change?Outcomes: What benefits will we gain when this effort succeeds?Ownership: Who has the authority to make decisions?Process: How does the work actually happen now? What will it look like after the initiative succeeds?Priorities: What matters most when tradeoffs arise?Tradeoffs: What are we willing to sacrifice to achieve something valuable?Governance: What constraints, risks, or obligations must shape our decisions?Culture: How much change can our teams absorb? How will we prepare them to succeed in the new environment? The Cost of Skipping This Step The rush to technology doesn’t answer these questions. It amplifies the lack of answers. The consequences of ignoring them extend far beyond an unsuccessful implementation. Organizations lose momentum. Investments fail to produce strategic advantage. Executive attention is consumed by avoidable challenges. Employees stop believing the next initiative will be different. Competitors, often with fewer resources, move ahead because their direction is clearer. The cost of ambiguity isn’t measured by a project plan. We’ve learned the lesson repeatedly that projects can be on time, on budget, and in scope, yet still deliver no business benefit. Lack of clarity in these dimensions is measured in lost opportunity and squandered advantage. Why AI Raises the Stakes And now, artificial intelligence has made the clarity discipline even more important. For the first time, organizations can prototype capabilities in days that once required months of engineering effort. Building has become dramatically easier. Experimentation has become far cheaper. This is an extraordinary opportunity, but it can also pose immense risk. The easier technology becomes to build, the easier it becomes to build the wrong thing. The faster an organization can act, the more certain it must be that it is acting on the right objectives, with the right governance, and in service of the right strategy. How Much Clarity Is Enough? Beginning by establishing clarity before turning to technology manages these potential pitfalls. This doesn’t have to mean that every initiative must begin with lengthy planning exercises. Because in truth, not every initiative requires months of analysis. Some decisions require only a few hours of focused alignment. But others deserve deeper examination. The discipline should always match the complexity and risk of the work. Navor’s engagement philosophy is to help organizations reach the level of understanding necessary to make the right investment decisions, execute them with confidence, and adopt change that lasts. Technology is one of the most powerful forces available to modern organizations. The potential of AI is monumental. The decisions firms make now require clear objectives, clear metrics, clear expectations. Successful transformation begins with clarity.
Have you considered what the defining infrastructure challenges of the next few years will be? If not, my interview with Srinivas Chippagiri may have you thinking more about how AI could change the way you manage infrastructure and what that means for building reliable, secure systems. But before we get into all of that, I always like to start by learning a little more about the person behind the expertise. So, let's kick it off with where Srinivas's journey began! Tell me what initially drew you into the field of technology. My journey began in software engineering, drawn to what I still think is the hardest question in the field: how to build software systems reliably at scale? I didn’t want to just build systems. I wanted to understand and document why they behave the way they do, which is what led me into research. Over the years, I’ve published peer-reviewed papers and book chapters on cloud security, Kubernetes performance, distributed scheduling, and applied AI. For me, engineering and research feed each other. Building at scale gives me the hard problems, and writing about them forces the clarity that makes the next system better. You mentioned that engineering and research feed into each other, which, for some developers, can make it a bit harder to navigate. When you're faced with a difficult technical problem, how do you typically approach finding a solution? I start by quantifying the problem before I touch a design. Who depends on this, what breaks if it fails, and what “good” actually looks like in numbers. Then I design for reuse rather than for the immediate fix. A lot of my research work comes directly out of that discipline. My work on Kubernetes network performance and on distributed task scheduling and load balancing using nature-inspired optimization began as real engineering problems that I then studied rigorously and published. That loop is my process: observe a hard problem in production, model it, measure it, and generalize it into something reusable. I trust measurement over intuition, and I try to leave behind a pattern others can apply, not just a one-off solution. I like that you focus on creating solutions others can reuse rather than just addressing the immediate problem. With that in mind, what's something you wish more people understood about cloud-native development? That “moving to the cloud” and “being cloud-native” are not the same thing. Many teams lift-and-shift a monolith onto managed infrastructure, watch the bill climb, and conclude cloud-native was oversold. The real win isn’t the platform. It’s designing systems as reusable, observable, governed building blocks, with security and compliance treated as engineering problems from day one rather than checklists bolted on after an incident. I’ve argued this in my research too. Several of my papers and book chapters look at cloud security and the organizational side of cloud transformation, because the hardest part of cloud-native isn’t the technology. It’s the discipline and architecture around it. You brought up the importance of building systems with security and compliance in mind from the start. Looking ahead, what emerging technology or trend has your attention right now, and why? Applied AI in infrastructure, meaning using AI not just as the workload but to operate and self-manage the systems underneath it. It sits right at the intersection of my two research threads: distributed systems and applied AI. My published work has gone deep on both sides. In a recent journal article, my co-authors and I designed an IoT-based AI framework for real-time health monitoring that pairs edge and fog processing with federated learning and differential privacy, so intelligence runs close to the data while privacy is preserved. On the systems side, I’ve studied scheduling and optimization. What fascinates me now is the reliability question. As we let intelligent systems make operational decisions, how do we keep them observable, safe, and accountable? That’s the same reliability-first instinct that has driven my entire career, applied to a far more autonomous stack, and I think it’s the defining infrastructure challenge of the next few years. It's interesting that reliability has remained a priority throughout your career, even as the technology evolves. What's one lesson you've learned along the way that's had a lasting impact on how you approach your work? That your highest-impact work is the work other people can build on. That reframed how I work. I now deliberately design and document for reuse, whether that’s a system architecture or a paper. I optimize for the multiplier, not just the deliverable, and the recognition I value most isn’t finishing something. It’s seeing my approach become a pattern others adopt. And now for my final question, which is usually my favorite: When you’re not working or thinking about technology, how do you like to spend your free time? A lot of my off time still points back toward the community. I’m active in the professional cloud computing community, and I mentor younger engineers and researchers, which I genuinely enjoy. Outside of tech, I like unhurried time with family and getting away from screens entirely. Travel and being outdoors help me return to hard problems with a clearer head. I’ve found the best architectural ideas rarely arrive at a keyboard. Check out more of his content here.
Modern data platforms are increasingly responsible for powering critical decisions, applications, analytics systems, and AI workloads. As organizations move more business processes onto data-driven architectures, the reliability of data pipelines becomes as important as the reliability of application services. A pipeline that fails loudly is usually easy to diagnose. A failed job, broken dependency, or unavailable service typically generates an alert and directs engineers toward a specific issue. The more difficult problems are the ones that do not interrupt execution at all. A pipeline can complete successfully while producing incorrect, incomplete, or misleading data. This is where data observability becomes essential. Traditional monitoring focuses primarily on whether systems are running. Data observability focuses on whether the data flowing through those systems remains trustworthy. It examines changes in structure, volume, freshness, distribution, and behavior to identify failures that traditional operational monitoring cannot detect. The Problem With Silent Data Failures Modern data ecosystems are rarely static. Enterprises often operate hundreds or thousands of pipelines connecting transactional systems, applications, event streams, warehouses, and analytical platforms. These pipelines evolve continuously as teams add new features, modify applications, and introduce new data sources. A database team may add a new column to a production table. An application team may change the format of an event payload. A third-party provider may alter the frequency or structure of delivered data. Each change may be intentional and technically valid, but it can create unexpected consequences downstream. Consider a customer analytics pipeline that receives transaction events from multiple systems. The pipeline may continue processing records after an upstream application changes a field format from a numeric value to a string. The ingestion process may not fail. The transformation jobs may still complete. Dashboards may continue refreshing. However, the business logic depending on that field may now produce incorrect results. The challenge is not detecting that a system is unavailable. The challenge is detecting that a system is operating while producing unreliable output. Detecting Schema Drift Before It Creates Downstream Issues Schema drift occurs when the structure of data changes over time. This can include adding or removing fields, changing data types, renaming attributes, or modifying relationships between datasets. Many organizations still rely on static schemas and manual documentation to manage these changes. While these approaches provide value, they often cannot keep pace with highly distributed environments. Data observability introduces automated schema monitoring that continuously compares incoming data against expected patterns. Instead of only checking whether a pipeline completed, observability systems examine whether the data structure remains consistent. For example, an observability layer can identify: A previously required field becoming consistently emptyA new field appearing unexpectedly in event streamsA data type changing from integer to textA significant reduction in the number of columns arriving from a source system The goal is not to prevent every schema change. Modern systems need to evolve. The goal is to identify changes quickly, understand their impact, and determine whether they represent expected evolution or an unintended failure. Understanding Data Drift Through Behavioral Changes Schema validation only examines structure. Many data quality issues happen without any structural change. Data drift occurs when data characteristics change over time. A dataset can maintain the same columns while its underlying patterns shift significantly. Examples include: A sudden increase in transactions from a specific regionA sharp decrease in average order valuesUnexpected changes in customer behavior patternsA growing percentage of missing valuesAbnormal distributions in machine learning features These changes are particularly important for AI and analytics systems. Machine learning models often assume that incoming data resembles the data used during development. When production data changes significantly, model performance can degrade without any visible infrastructure failure. Data observability helps teams establish baselines for normal behavior. Instead of defining every possible failure condition manually, systems can analyze historical patterns and identify statistically unusual changes. Combining Deterministic Rules With Intelligent Detection A common misconception is that data observability requires replacing traditional validation with machine learning. In practice, reliable data platforms need both deterministic checks and intelligent anomaly detection. Deterministic validation remains valuable because some conditions are clear. A customer identifier should not be null. A timestamp should follow an expected format. A transaction amount should not be negative. These rules provide strong guarantees for known requirements. However, not every failure can be captured through predefined rules. Unknown issues require systems that can recognize unusual behavior. Statistical and machine learning approaches can analyze: Record volume changesDistribution shiftsUnusual null patternsProcessing delaysUnexpected relationships between datasetsChanges in key business metrics The combination creates a layered defense. Rules catch known problems. Intelligent detection identifies emerging problems that engineers did not anticipate. Data Lineage: Understanding Where Problems Begin Detecting an anomaly is only the first step. The harder question is determining where the issue originated. Large organizations often have complex dependency chains. A single dashboard may depend on dozens of upstream tables, transformations, and source systems. Without visibility into these relationships, engineers may spend hours investigating symptoms rather than causes. Data lineage provides a map of how information moves through the organization. When an anomaly appears, lineage allows teams to answer important questions: Which pipelines depend on this dataset?Which upstream source introduced the change?Which downstream applications may be affected?When did the behavior begin changing? This turns incident response from manual investigation into a structured troubleshooting process. Building Effective Alerting and Root Cause Analysis Poor alerting can create another operational challenge. If every unusual data point generates an alert, engineering teams quickly experience alert fatigue. If alerts are too limited, important failures remain unnoticed. Effective data observability requires context-aware alerting. A sudden increase in records may not indicate a failure if it aligns with a known business event. A small change in a critical dataset may be more significant than a large change in a less important pipeline. Alerts should incorporate factors such as: Business importance of the affected dataSeverity of the anomalyHistorical behaviorDownstream dependenciesConfidence level of detection The objective is not simply generating more notifications. It helps engineers prioritize issues that require action. Designing Reliable Data Platforms for the Future As organizations adopt real-time analytics, AI applications, and increasingly complex data architectures, data reliability becomes an engineering discipline of its own. Reliable pipelines require more than successful job execution. They require confidence that the information moving through those pipelines remains accurate, complete, and useful. A mature data observability strategy combines multiple capabilities: Automated schema monitoringData quality validationStatistical anomaly detectionFreshness trackingPipeline performance monitoringData lineageIntelligent alertingRoot cause analysis The future of data engineering will not be defined only by how much data organizations can process. It will be defined by how confidently they can trust that data. Data observability provides the foundation for building that trust. By detecting silent failures before they become business problems, engineering teams can create data platforms that are not only scalable but also dependable.
Everyone is rushing to build AI agents right now, but GenAI integration really just puts a stronger spotlight on solid system design and backend reliability. An AI agent relies entirely on the APIs and data pipelines underneath it. If your backend drops events or double-processes payments during a network blip, even the best agent will just execute a broken workflow faster. To build resilient systems, you need to see how they break. Reading Kafka documentation on fault tolerance helps, but intentionally introducing network chaos shows how the underlying mechanics actually behave. In this post, we will set up a local testing environment using Spring Boot, Kafka, and Toxiproxy. We'll configure producer properties for topic-level idempotence and walk through common network simulation pitfalls that lead to duplicate data. 1. Setting Up the Chaos Test Architecture To test network issues accurately, stopping the Kafka broker isn't enough. We need to simulate realistic network conditions like latency spikes, packet drops, and bandwidth throttling. Toxiproxy acts as a proxy between our producer and broker to simulate these conditions. The Network Architecture Creating a Local/Dev Kafka Cluster We start by spinning up the Kafka cluster and Kafka UI using Docker. YAML # docker-compose-kafka.yml version: "3.8" #avoid this services: kafka: image: apache/kafka:latest container_name: kafka ports: - "9092:9092" environment: CLUSTER_ID: "4L62xdwCQKWvZs96A39vGA" KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" KAFKA_INTER_BROKER_LISTENER_NAME: "INTERNAL" # Configure distinct internal/external routing ports KAFKA_LISTENERS: "INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" # We will bookmark this line, dear reader! KAFKA_ADVERTISED_LISTENERS: "INTERNAL://kafka:29092,EXTERNAL://localhost:19092" KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_LOG_DIRS: "/var/lib/kafka/data" volumes: - kafka_data:/var/lib/kafka/data kafka-ui: image: provectuslabs/kafka-ui:latest container_name: kafka-ui ports: - "9080:8080" depends_on: - kafka environment: DYNAMIC_CONFIG_ENABLED: "true" KAFKA_CLUSTERS_0_NAME: "local-cluster" KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: "kafka:29092" volumes: kafka_data: To spin it up, we use Docker Compose: docker compose -f "docker-compose -kafka.yml" up Both your local Kafka cluster and the kafka-ui should spin up. Note that KAFKA_ADVERTISED_LISTENERS includes the external port localhost:19092; we'll see why. A Local Proxy to Sit Between Kafka Broker and Producer We use Docker for this, too! YAML # docker-compose-toxiproxy.yml version: "3.8" # Don’t use this version - it’s deprecated services: toxiproxy: image: shopify/toxiproxy:latest container_name: toxiproxy ports: - "8474:8474" # Toxiproxy Management API - "8000:8000" # Example port exposed for a proxied application service - "19092:19092" networks: - toxiproxy-net toxiproxy-ui: image: buckle/toxiproxy-frontend:latest container_name: toxiproxy-ui ports: - "8080:8080" # We don’t use UI in this article environment: - TOXIPROXY_URL=http://toxiproxy:8474 depends_on: - toxiproxy networks: - toxiproxy-net networks: toxiproxy-net: driver: bridge docker compose -f docker-compose-toxiproxy.yml up Once running, create a proxy tunnel via the Toxiproxy CLI to listen on port 19092 and forward traffic to Kafka on port 9092: ./toxiproxy-cli create -l 0.0.0.0:19092 -u host.docker.internal:9092 kafka-proxy Note: We use the above CLI inside the toxiproxy container. So, you must use docker exec. In some versions, the CLI tool might not be included in the $PATH, and you might need to switch the current directory: Shell /go/bin # ls toxiproxy toxiproxy-cli If you're using this toxiproxy tool for the first time, you can refer to the official Toxiproxy documentation or this starter guide we created. 2. The Kafka Client Producer Code We create a basic application using Spring Boot 4.1 (though we do not have any version restriction until now) and use the following dependency: XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-kafka</artifactId> </dependency> We'll keep the Spring Boot service simple. We define a basic payload configuration and direct messages to topic T3 via KafkaConfig.java. Config: Java // KafkaConfig.java package com.soham.selfteaching.kafka.kafkadeepdive.config; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @Configuration @Data public class KafkaConfig { @Value("${app.topic.name:T3}") private String topic; } Our application.yml is minimal, accepting defaults provided by the Kafka library. Properties files # application.yml server: port: 8082 spring: application: name: kafka-deep-dive kafka: bootstrap-servers: "localhost:19092" producer: acks: "1" # Required to bypass idempotence shield key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer properties: request.timeout.ms: 3000 delivery.timeout.ms: 120000 enable.idempotence: false # We disable it first before we explore producer level idempotence The domain event is defined as a Java record. Java // EventDto.java package com.soham.selfteaching.kafka.kafkadeepdive.dto; public record EventDto(String id,String payload) { } Producer Code The core logic sits in ProducerService.java. We use CompletableFuture to handle delivery timeouts asynchronously without blocking thread execution. If the broker is unreachable initially, max.block.ms handles the immediate block on the main thread while background retries handle transient issues. Java // ProducerService.java package com.soham.selfteaching.kafka.kafkadeepdive.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.support.SendResult; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; @Service @Slf4j @RequiredArgsConstructor public class ProducerService { private final KafkaTemplate<String, Object> kafkaTemplate; public void sendMessage(String topic, String key, Object payload) { log.info("Attempting to send message to topic: {} with key: {}", topic, key); CompletableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, key, payload); future.whenComplete((result, exception) -> { if (exception == null) { log.info("Message successfully delivered. Topic: {}, Partition: {}, Offset: {}", result.getRecordMetadata().topic(), result.getRecordMetadata().partition(), result.getRecordMetadata().offset()); } else { log.error("Fatal delivery failure for key {}. Exhausted delivery.timeout.ms.", key, exception); } }); } } 3. Configuring Topic-Level Idempotence During a network timeout, the producer cannot verify whether the message failed to reach the broker or if the acknowledgment (ACK) was lost on the return path. By default, the producer retries the request, which can produce duplicate records without proper safeguards. Exactly-Once Semantics (EOS) and topic-level idempotence address this by assigning a producer ID (PID) and sequence number to each message batch. If the broker receives a retried batch with a sequence number it has already processed, it ignores the duplicate write. Disabling Idempotence to Force Duplicates To demonstrate retry behavior, we can explicitly disable these safety settings in application.yml. Since Kafka 3.0, enable.idempotence defaults to true. To allow duplicates during testing, set acks: "1" and explicitly set enable.idempotence: false. Setting request.timeout.ms to 3000 (3 seconds) creates a tight window to trigger network retries during latency testing. 4. Running the Kafka Producer and Verify We run the Spring Boot application: Java @SpringBootApplication public class KafkaDeepDiveApplication { public static void main(String[] args) { SpringApplication.run(KafkaDeepDiveApplication.class, args); } } And hit a REST API (Hold on! It's a simple HTTP API which may NOT quite follow REST paradigms). curl -X GET http://localhost:8082/api/send Java @RestController @RequestMapping("/api") @Slf4j @RequiredArgsConstructor public class ProducerController { private final ProducerService service; private final KafkaConfig kafkaConfig; @GetMapping("/send") public String send() { long ts = System.currentTimeMillis(); Object payload = new EventDto("p1.t1." + UUID.randomUUID().toString(), ("Hello message " + i)); service.sendMessage(kafkaConfig.getTopic(), "k1", payload); return "Process took :%s ms".formatted(System.currentTimeMillis() - ts); } } In the case when idempotence is disabled, it's fair to expect that duplicate messages will arrive on a topic if a producer does not receive timely ACK from the broker leaders in alignment with the ack flag. But we do not yet see any duplicate messages at all. This is fairly explainable, since you're all set up locally, and there's usually no lost acknowledgment. Introduce Chaos to the Local Network Add 5000ms latency to the proxy. ./toxiproxy-cli toxic add -n false_positive -t latency -a latency=5000 -d kafka-proxy Send a message to the endpoint. After an API hit, wait for 2–3 seconds and remove the toxic Remove the Toxic While the Retry Is in Flight Instantly run this command to remove the toxic while the retry is in flight: ./toxiproxy-cli toxic remove -n false_positive kafka-proxy While this should have produced duplicates due to retries by the producer following a timed-out broker ACK, we find NO DUPLICATE MESSAGES even after repeating the process above many times. 5. Let Us Debug Our Disconnect Common Pitfalls During Network Injection Simply starting the proxy and adding latency doesn't always trigger duplicates as expected. Below are two common issues related to how Kafka clients handle metadata and connections. Issue 1: Advertised Listeners Bypass If Toxiproxy is running on port 19092 and a 5000ms latency is emulated, messages sent through the proxy might still complete in milliseconds. ./toxiproxy-cli toxic add -n false_positive -t latency -a latency=5000 -d kafka-proxy This occurs because of how Kafka advertises broker addresses during metadata discovery. If you look closely at the internal Kafka NetworkClient logs during startup, you will find out why: Plain Text brokers=[MetadataResponseBroker(nodeId=1, host='localhost', port=9092)] When the producer connects to port 19092 for initial cluster metadata, the broker returns its configured advertised listener (e.g., localhost:9092). The producer client caches this response and routes subsequent produce requests directly to port 9092, bypassing the proxy entirely. We must ensure that our KAFKA_ADVERTISED_LISTENERS explicitly broadcasts EXTERNAL://localhost:19092 so the producer continues routing traffic through Toxiproxy. If updated, clear existing Docker volumes (docker-compose down -v) to avoid using cached listener configurations. Issue 2: Initial Metadata Fetch Blocking If high latency (e.g., 15,000ms) is applied before sending any messages, the application may time out after 60 seconds with a TimeoutException: Topic T3 not present in metadata, rather than failing on delivery.timeout.ms. The delivery.timeout.ms timer only applies once a record enters the producer's accumulator buffer. If the client hasn't fetched topic metadata yet, the call blocks on max.block.ms (defaulting to 60 seconds) while attempting to retrieve metadata through the degraded network path. We apply the fix: Send an initial message without active toxics to populate the local metadata cache before injecting network latency. Finally With latency removed, the retried request reaches the broker successfully. Because acks=1 and enable.idempotence=false, the broker writes the payload again without verifying duplicate sequence numbers. Checking Kafka UI confirms two identical records are present in the topic partition. Simulating network edge cases directly demonstrates why configurations like advertised listeners, metadata timeouts, and idempotence defaults exist in modern Kafka clients. Now we get duplicate messages across a topic partition. Duplicate Messages We can now enable back idempotence flag to see that no duplicate messages are created repeating the above steps with enable.idempotence:true, ensuring that idempotence is preserved enable.idempotence= true only ensures idempotence across a partition only and not the entire topic. We shall explore it in a next article.
Agile
Career Development
Methodologies
Team Management
AI-Assisted Refinement Preparation
November 10, 2026
by Stefan Wolpers
CORE
Every Acquisition Promises One Company. Most Deliver Two.
November 9, 2026 by Faisal Feroz
AI/ML
Big Data
Databases
IoT
When to Use AMQP, JMS, Kafka, or MQTT: Trade-Offs, Not a Winner
November 10, 2026
by Kai Wähner
CORE
Kafka vs Flink vs Spark: Do You Really Need Real-Time?
November 10, 2026
by Kai Wähner
CORE
Building a Feature Store With Neo4j and Feast
November 10, 2026
by Akmal Chaudhri
CORE
Frameworks
Java
JavaScript
Languages
Tools
When to Use AMQP, JMS, Kafka, or MQTT: Trade-Offs, Not a Winner
November 10, 2026
by Kai Wähner
CORE
Kafka vs Flink vs Spark: Do You Really Need Real-Time?
November 10, 2026
by Kai Wähner
CORE
Building a Feature Store With Neo4j and Feast
November 10, 2026
by Akmal Chaudhri
CORE
Deployment
DevOps and CI/CD
Maintenance
Monitoring and Observability
The Pipeline Failed, But the Infrastructure Changed
November 10, 2026 by Jeleel Muibi
AI-Assisted Refinement Preparation
November 10, 2026
by Stefan Wolpers
CORE
AI/ML
Java
JavaScript
Open Source
Agent Harness, Memory Indexing, and AI Governance in Test Automation
November 10, 2026 by Soumyajit Basu
Prompt Caching for LLMs, Part 1: Exact-Match Caching in Production
November 10, 2026
by Ram Ghadiyaram
CORE
AI-Assisted Refinement Preparation
November 10, 2026
by Stefan Wolpers
CORE