<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mehmet TURAÇ</title>
    <description>The latest articles on DEV Community by Mehmet TURAÇ (@turacthethinker).</description>
    <link>https://dev.to/turacthethinker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2891163%2F4ed4212c-3d45-4e35-877f-decf97916132.png</url>
      <title>DEV Community: Mehmet TURAÇ</title>
      <link>https://dev.to/turacthethinker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/turacthethinker"/>
    <language>en</language>
    <item>
      <title>Great Stack to Doesn't Work #6 — CI/CD: "Pipeline Green, Production Red"</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Fri, 05 Jun 2026 20:42:17 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-6-cicd-pipeline-green-production-red-5a5m</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-6-cicd-pipeline-green-production-red-5a5m</guid>
      <description>&lt;p&gt;&lt;em&gt;A survival guide for when everything goes wrong in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The pipeline is green. Every stage passed. Tests: green. Lint: green. Build: green. Security scan: green. The deploy button says "Ready." You click it.&lt;/p&gt;

&lt;p&gt;Five minutes later, the error rate jumps to 15%. The pipeline is still green. It will stay green while your users can't check out, because the pipeline tests what you wrote, not what production does with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Your Pipeline Lies to You
&lt;/h2&gt;

&lt;p&gt;A green pipeline means your code compiles, your tests pass, and your container builds. It does not mean your code works in production. The gap between "works in CI" and "works in production" is where incidents live.&lt;/p&gt;

&lt;p&gt;The most common gaps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment drift.&lt;/strong&gt; CI runs on a clean container with a fresh database. Production has 3 years of accumulated data, schema migrations that ran in a different order during the early days, and environment variables that were set manually by someone who left the company.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data shape.&lt;/strong&gt; Your tests use factory-generated data with predictable shapes. Production has users who put emojis in their name field, addresses that are 4,000 characters long, and order records with null values in columns that "should never be null."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traffic patterns.&lt;/strong&gt; CI runs one test at a time, sequentially. Production handles 10,000 concurrent requests. Race conditions that never appear in CI appear within minutes in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency versions.&lt;/strong&gt; Your lock file pins exact versions, but your Docker base image pulls latest, or a system package updates between builds. The code is identical. The runtime is not.&lt;/p&gt;

&lt;p&gt;The pipeline can't test for all of this. But it can test for more than it currently does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer Caching: Cutting Build Times by 80%
&lt;/h2&gt;

&lt;p&gt;Docker builds are slow because they're rebuilding layers that haven't changed. Every &lt;code&gt;RUN&lt;/code&gt; instruction creates a layer. If the layer's inputs haven't changed, Docker can reuse the cached version.&lt;/p&gt;

&lt;p&gt;The problem: CI environments often start with an empty cache. Every build is a fresh build. 12 minutes to install dependencies that haven't changed since last week.&lt;/p&gt;

&lt;p&gt;Solutions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registry-based caching.&lt;/strong&gt; Push cache layers to your container registry. Pull them at the start of each build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-from&lt;/span&gt; myregistry/myapp:cache &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;BUILDKIT_INLINE_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; myregistry/myapp:latest &lt;span class="nb"&gt;.&lt;/span&gt;
docker push myregistry/myapp:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub Actions cache (or equivalent):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/.buildx-cache&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-buildx-${{ hashFiles('**/package-lock.json') }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Separate dependency and code layers.&lt;/strong&gt; This is Docker 101 but people still get it wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies change weekly. Code changes hourly. Separate them so the expensive &lt;code&gt;npm ci&lt;/code&gt; layer is cached across code-only changes.&lt;/p&gt;

&lt;p&gt;A team I worked with reduced their build from 14 minutes to 3 minutes by adding registry-based caching and reordering their Dockerfile. No infrastructure changes. No new tools. Just understanding how Docker layer caching works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallel Stages: Stop Running Tests Sequentially
&lt;/h2&gt;

&lt;p&gt;If your test suite takes 20 minutes, and you have 4 CI runners, split the tests into 4 parallel groups. Each group takes 5 minutes. Total wall time: 5 minutes.&lt;/p&gt;

&lt;p&gt;The naive approach — splitting by file count — creates unbalanced groups. One group might have 3 integration test files that each take 2 minutes, while another group has 50 unit test files that each take 100ms.&lt;/p&gt;

&lt;p&gt;Better: &lt;strong&gt;split by historical timing data.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions example with test splitting&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx jest --shard=${{ matrix.shard }}/4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jest's &lt;code&gt;--shard&lt;/code&gt; flag distributes tests across shards using file hashing. For more sophisticated balancing, tools like &lt;code&gt;split_tests&lt;/code&gt; (Ruby), &lt;code&gt;pytest-split&lt;/code&gt;, or CI-specific features (CircleCI's test splitting, Buildkite's parallelism) use timing data from previous runs to create balanced groups.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flaky Tests: The "This Test Passes Sometimes" Syndrome
&lt;/h2&gt;

&lt;p&gt;Flaky tests are worse than failing tests. A failing test tells you something is broken. A flaky test tells you nothing — it might be broken, or it might just be having a bad day.&lt;/p&gt;

&lt;p&gt;The damage is insidious. Engineers start re-running the pipeline when a test fails. "Oh, that test is flaky, just retry." Now you're training the team to ignore test failures. The day a real bug causes a test to fail, nobody investigates — they just retry until it passes.&lt;/p&gt;

&lt;p&gt;Detection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track test results over time. If a test fails more than 1% of the time and the failures don't correlate with code changes, it's flaky.&lt;/li&gt;
&lt;li&gt;Quarantine flaky tests into a separate suite that runs but doesn't block the pipeline. Fix them with priority.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time dependency.&lt;/strong&gt; Tests that assume a specific time or date, or that measure elapsed time with tight tolerances. A test that passes in 100ms locally might take 300ms in CI due to shared resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order dependency.&lt;/strong&gt; Test A creates data, test B reads it. When tests run in a different order (parallel execution, random seed), test B fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External dependency.&lt;/strong&gt; Tests that call a real API, read from a shared database, or depend on DNS resolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions.&lt;/strong&gt; Async operations that complete faster on your machine than in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: isolate, mock, use deterministic clocks, and clean up after every test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rollback Strategies: Choosing Your Safety Net
&lt;/h2&gt;

&lt;p&gt;When a deployment goes wrong, how fast can you get back to the previous version?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rolling update:&lt;/strong&gt; Replace pods one by one. If the new version is broken, you notice after some pods are already updated. Rolling back means deploying the previous version, which takes as long as the original deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blue-green:&lt;/strong&gt; Run two identical environments. Blue is live. Deploy to green. Test green. Switch traffic from blue to green. If green fails, switch back to blue. Rollback is instant — just change the traffic routing. Cost: you need double the infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canary:&lt;/strong&gt; Send 1% of traffic to the new version. Monitor error rates, latency, and business metrics. If everything looks good, gradually increase to 10%, 25%, 50%, 100%. If anything looks bad at any stage, route all traffic back to the stable version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags:&lt;/strong&gt; Deploy the code but don't activate it. The feature is behind a flag that defaults to off. Enable it for internal users first. Then 1% of users. Then 10%. If something breaks, flip the flag off. The code stays deployed; the feature deactivates. This is the most granular rollback mechanism — you can revert a single feature without touching the deployment.&lt;/p&gt;

&lt;p&gt;The 42-minute pipeline team's rollback strategy was "deploy the previous version," which also took 42 minutes. Their canary threshold was set to 5% error rate. By the time the canary caught the problem, 3% of real users had already been affected, and the rollback took another 42 minutes. Total incident duration: over an hour.&lt;/p&gt;

&lt;p&gt;After fixing the pipeline speed (11 minutes) and implementing feature flags, their rollback time dropped from 42 minutes to under 10 seconds — just a flag flip.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secret Management: Stop Hardcoding Credentials
&lt;/h2&gt;

&lt;p&gt;Secrets in environment variables are the minimum bar. But CI/CD pipelines have their own secret lifecycle that most teams handle poorly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token expiration.&lt;/strong&gt; CI tokens, deploy keys, API keys — they all expire. If nobody monitors expiration dates, one morning your pipeline fails and nobody can deploy until someone provisions a new token. This happened to us: a GitHub App installation token expired mid-deployment. 45 minutes of "why is git clone failing?" before someone checked the token creation date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret rotation.&lt;/strong&gt; If you rotate a database password, you need to update it in your CI secrets, your Kubernetes secrets, your application config, and your monitoring system. Miss one, and something breaks silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least privilege.&lt;/strong&gt; Your CI pipeline doesn't need admin access to your cloud account. It needs permission to push images, update deployments, and maybe run migrations. Create a dedicated CI service account with only the permissions it needs.&lt;/p&gt;

&lt;p&gt;Use a secret manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager) and pull secrets at runtime. Don't bake them into images. Don't store them in git. Don't pass them as build arguments (they end up in Docker layer metadata).&lt;/p&gt;




&lt;h2&gt;
  
  
  GitOps: Let Git Be the Source of Truth
&lt;/h2&gt;

&lt;p&gt;GitOps (ArgoCD, Flux) flips the deployment model. Instead of "CI pushes a new version to the cluster," git is the desired state and an operator pulls the desired state from git.&lt;/p&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PR changes the Kubernetes manifests or Helm values in a git repo.&lt;/li&gt;
&lt;li&gt;PR is reviewed, approved, merged.&lt;/li&gt;
&lt;li&gt;ArgoCD detects the change, compares it to the current cluster state, and applies the diff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every deployment is a git commit. Full audit trail.&lt;/li&gt;
&lt;li&gt;Rollback is &lt;code&gt;git revert&lt;/code&gt;. The operator sees the repo changed and syncs.&lt;/li&gt;
&lt;li&gt;Drift detection — if someone &lt;code&gt;kubectl apply&lt;/code&gt;s something manually, ArgoCD detects the drift and can auto-correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operational reality: GitOps adds complexity. You now have a git repo to manage, an operator to keep healthy, and a reconciliation loop that can conflict with manual interventions during incidents. It's worth it for teams with 10+ services and frequent deployments. For a team with 3 services deploying twice a week, a simple CI/CD pipeline is simpler and sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  War Story: From 42 Minutes to 11
&lt;/h2&gt;

&lt;p&gt;Monorepo. 4 services. 1 pipeline that built everything, tested everything, and deployed everything, regardless of which service changed.&lt;/p&gt;

&lt;p&gt;The 42-minute breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker build: 8 minutes (no caching)&lt;/li&gt;
&lt;li&gt;Unit tests: 12 minutes (sequential, 2,400 tests)&lt;/li&gt;
&lt;li&gt;Integration tests: 14 minutes (starting 3 databases, running sequentially)&lt;/li&gt;
&lt;li&gt;Deploy: 8 minutes (rolling update, health check wait)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 8 changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Registry-based Docker caching.&lt;/strong&gt; Build dropped from 8 minutes to 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only build changed services.&lt;/strong&gt; Used git diff to detect which service directories changed. If only service-A changed, only service-A builds and deploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel unit tests with sharding.&lt;/strong&gt; 4 shards, 3 minutes per shard (wall time: 3 minutes instead of 12).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared test database.&lt;/strong&gt; Instead of starting a fresh database per test file, start one per test shard and use schema isolation. Integration test setup dropped from 6 minutes to 45 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel integration tests.&lt;/strong&gt; With the shared database, integration tests could run in parallel. 14 minutes down to 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached dependency installation.&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt; cached by lockfile hash. &lt;code&gt;npm ci&lt;/code&gt; only runs when &lt;code&gt;package-lock.json&lt;/code&gt; changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy only changed services.&lt;/strong&gt; Same git diff approach. If service-B didn't change, don't redeploy it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary deploy with automated rollback.&lt;/strong&gt; Instead of waiting for a full rolling update, deploy canary to 1 pod, run smoke tests, then proceed. If smoke tests fail, automatic rollback in 30 seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: 11 minutes end-to-end for a single service change. 16 minutes for a full monorepo change. Developers went from deploying twice a day (because each deploy took so long) to deploying 8-10 times a day.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;A green pipeline is a necessary condition for deployment, not a sufficient one. Your pipeline tests your code. Production tests your system.&lt;/p&gt;

&lt;p&gt;Speed matters. A 42-minute pipeline doesn't just slow down deployment — it changes developer behavior. People batch changes, skip tests locally, and deploy less frequently. All of which increase risk.&lt;/p&gt;

&lt;p&gt;Feature flags are the most underrated deployment tool. They decouple deployment from release. You can deploy code any time and release features when you're ready. Rollback is a flag flip, not a redeployment.&lt;/p&gt;

&lt;p&gt;And manage your CI secrets like production secrets. They expire, they need rotation, and when they break, nobody can deploy.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;What's the longest your CI/CD pipeline has ever taken? How did you cut it down? And has anyone else been burned by an expired CI token during an incident?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>LLM-Free Multi-Agent Memory Architecture: How to Build Real Team Memory with Jira + GitHub + Commit Log</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Fri, 05 Jun 2026 11:03:01 +0000</pubDate>
      <link>https://dev.to/turacthethinker/llm-free-multi-agent-memory-architecture-how-to-build-real-team-memory-with-jira-github-commit-dpa</link>
      <guid>https://dev.to/turacthethinker/llm-free-multi-agent-memory-architecture-how-to-build-real-team-memory-with-jira-github-commit-dpa</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;One of the biggest problems in software teams is not writing code. Code eventually gets written, refactored, tested, and deployed. The real challenge, most of the time, is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why was this decision made?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a developer joins a project, they can't understand the work just by looking at the repository. They can see the code, but not the story behind it. Why was a service split this way? Why is an interface designed so oddly? Why does a test specifically check that edge case? Why has a file turned into something everyone is afraid to touch? The answers to these questions usually lie not in the code itself, but in the team's history.&lt;/p&gt;

&lt;p&gt;That history is scattered across different tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jira issues&lt;/li&gt;
&lt;li&gt;GitHub Pull Requests&lt;/li&gt;
&lt;li&gt;Review comments&lt;/li&gt;
&lt;li&gt;Commit messages&lt;/li&gt;
&lt;li&gt;Branch names&lt;/li&gt;
&lt;li&gt;Incident records&lt;/li&gt;
&lt;li&gt;Release notes&lt;/li&gt;
&lt;li&gt;Sometimes Slack/Teams conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why, for a new developer, the learning process usually goes like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Look at the code → Find something you don't understand → Search Jira → Search PR → Search Slack → Ask the old developer → Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is team memory loss. And this loss costs time, causes errors, and exhausts people.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 1: Questions That Team Memory Should Answer
&lt;/h1&gt;

&lt;p&gt;When a well-structured team memory system is in place, it should be able to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who changed this file the most?&lt;/li&gt;
&lt;li&gt;Who last touched this file?&lt;/li&gt;
&lt;li&gt;Which commits resolved this issue?&lt;/li&gt;
&lt;li&gt;Which PR was this change discussed in?&lt;/li&gt;
&lt;li&gt;Why has this component changed so frequently in the last 90 days?&lt;/li&gt;
&lt;li&gt;Has this bug occurred before?&lt;/li&gt;
&lt;li&gt;Which issues and PRs should a new developer read to learn the auth module?&lt;/li&gt;
&lt;li&gt;Who is the most suitable reviewer for a new PR?&lt;/li&gt;
&lt;li&gt;Which files carry technical risk?&lt;/li&gt;
&lt;li&gt;Which component is too dependent on a single person?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What these questions have in common: the answers are not in a single record. The answers are hidden in relationships.&lt;/p&gt;

&lt;p&gt;For example, to answer "who knows the auth module?" it's not enough to just count commits. You need to look at all of these together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People who committed to auth files&lt;/li&gt;
&lt;li&gt;People who reviewed auth PRs&lt;/li&gt;
&lt;li&gt;People who commented on auth issues&lt;/li&gt;
&lt;li&gt;People who fixed auth bugs&lt;/li&gt;
&lt;li&gt;People who have been active recently&lt;/li&gt;
&lt;li&gt;People who made changes with large churn&lt;/li&gt;
&lt;li&gt;Files with revert or incident history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So team memory is essentially a relationship problem.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 2: Why an LLM-Free Architecture?
&lt;/h1&gt;

&lt;p&gt;LLMs are powerful, but it's not always right to put an LLM at the core of every problem. For systems like team memory, the main requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accuracy&lt;/li&gt;
&lt;li&gt;Auditability&lt;/li&gt;
&lt;li&gt;Reproducibility&lt;/li&gt;
&lt;li&gt;Low cost&lt;/li&gt;
&lt;li&gt;Long-term maintainability&lt;/li&gt;
&lt;li&gt;Permission and privacy control&lt;/li&gt;
&lt;li&gt;Evidence-based response generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me also add a personal note here. Even though I write a series about AI-free life on Dev.to, this time I specifically wanted to write something on the software engineering side without AI as well. Honestly, the motivation behind this article is partly to push myself outside of repetition while also giving you some food for thought: You can build quite useful, technically clean, and maintainable systems without tying every problem to an LLM.&lt;/p&gt;

&lt;p&gt;Let me be even more direct: we're a bit tired of it. Constant AI, constant agents, constant RAG, constant prompts. These are certainly valuable topics, but sometimes you just want to see solid, old-school engineering. This article was written with exactly that motivation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why LLM-Free for Team Memory?
&lt;/h2&gt;

&lt;p&gt;An LLM-centric approach carries several risks.&lt;/p&gt;
&lt;h2&gt;
  
  
  2.1 Hallucination Risk
&lt;/h2&gt;

&lt;p&gt;An LLM might behave as if there's a relationship between an issue and a commit when no such relationship exists in the real system. Pointing to the wrong PR, showing the wrong person as an expert, or misinterpreting a past bug fix causes serious time loss.&lt;/p&gt;

&lt;p&gt;In team memory, answers should not be "educated guesses." Answers must come with evidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  2.2 Auditability Problem
&lt;/h2&gt;

&lt;p&gt;If a system says "this file is risky," it should be able to explain why:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/auth/token_service.py changed 18 times in the last 90 days.
5 of those changes are linked to bug fixes.
4 different developers have touched the file.
A race condition was discussed in the last two PRs.
The test file was not updated at the same rate.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This kind of answer is debatable, verifiable, and improvable. An LLM saying "it looked risky to me" doesn't deliver the same quality.&lt;/p&gt;
&lt;h2&gt;
  
  
  2.3 Cost and Latency
&lt;/h2&gt;

&lt;p&gt;LLMs are not needed for questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which commit resolved this issue?&lt;/li&gt;
&lt;li&gt;Who last touched this file?&lt;/li&gt;
&lt;li&gt;Which files did this PR change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are pure data queries. SQL or graph traversal solves them instantly.&lt;/p&gt;
&lt;h2&gt;
  
  
  2.4 Reproducibility
&lt;/h2&gt;

&lt;p&gt;For team memory, the same question should always produce the same answer on the same data. LLM-based systems can give different answers each time. This is unacceptable for audit and debugging.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 3: Core Architecture
&lt;/h1&gt;

&lt;p&gt;The foundation of the system is a memory store. This store holds the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git commit log&lt;/li&gt;
&lt;li&gt;Jira issues and comments&lt;/li&gt;
&lt;li&gt;GitHub PRs, reviews, and review comments&lt;/li&gt;
&lt;li&gt;File paths and components&lt;/li&gt;
&lt;li&gt;Developer identities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of this, agents query the memory store, score it, and produce explainable outputs.&lt;/p&gt;

&lt;p&gt;The basic flow:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jira / GitHub / Git
    ↓
Ingestion Layer
    ↓
Memory Store (relational + graph)
    ↓
Agents (ContextAgent, ExpertiseAgent, RiskAgent, ...)
    ↓
Explainable Output (CLI / API / Bot)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The Core Principle: Everything Is a Relationship
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROJ-1247 issue
  → linked to PR #382
  → resolved by commits f00ba47 and b91c0de
  → changed src/auth/token_service.py
  → contributed by Mehmet Turac and Ayşe Demir
  → reviewed by Burak Kaya
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this information, a new developer no longer has to search randomly.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 4: Classic Multi-Agent Logic
&lt;/h1&gt;

&lt;p&gt;I'm not using the word "agent" in the LLM agent sense here. In this architecture, an agent is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A small service with a specific task, which queries memory, makes rule-based decisions, and produces evidence-backed output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So what we call an agent is not a bot running prompts. It's a perfectly classical software component.&lt;/p&gt;
&lt;h2&gt;
  
  
  ContextAgent
&lt;/h2&gt;

&lt;p&gt;Extracts context for an issue, PR, or file.&lt;/p&gt;
&lt;h2&gt;
  
  
  ExpertiseAgent
&lt;/h2&gt;

&lt;p&gt;Calculates the most knowledgeable people for a file or component.&lt;/p&gt;
&lt;h2&gt;
  
  
  RiskAgent
&lt;/h2&gt;

&lt;p&gt;Finds risky files based on signals like high churn, bug fixes, and contributor spread.&lt;/p&gt;
&lt;h2&gt;
  
  
  ReviewRoutingAgent
&lt;/h2&gt;

&lt;p&gt;Suggests suitable reviewer candidates for a new PR.&lt;/p&gt;
&lt;h2&gt;
  
  
  OnboardingAgent
&lt;/h2&gt;

&lt;p&gt;For a new developer on a given component, lists the most valuable issues and PRs to read.&lt;/p&gt;
&lt;h2&gt;
  
  
  HygieneAgent
&lt;/h2&gt;

&lt;p&gt;Reports data quality problems in the memory store.&lt;/p&gt;

&lt;p&gt;Each agent works with a scoring and rule-based logic.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 5: Data Model
&lt;/h1&gt;

&lt;p&gt;The minimum entity set for the first version is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
Repository
Issue
Commit
File
PullRequest
Review
IssueComment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Even with this model, a powerful memory system can be built.&lt;/p&gt;
&lt;h2&gt;
  
  
  Developer
&lt;/h2&gt;

&lt;p&gt;A developer can appear with different identities across systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git author email&lt;/li&gt;
&lt;li&gt;GitHub username&lt;/li&gt;
&lt;li&gt;Jira account id&lt;/li&gt;
&lt;li&gt;Display name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These need to be linked to a single developer record.&lt;/p&gt;
&lt;h2&gt;
  
  
  Commit
&lt;/h2&gt;

&lt;p&gt;Commits are among the most reliable events in the system. Hash, message, date, author, and changed files are stored.&lt;/p&gt;
&lt;h2&gt;
  
  
  File
&lt;/h2&gt;

&lt;p&gt;Files should be stored not just as paths, but with component information.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/auth/**      → auth
src/payment/**   → payment
infra/**         → infra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Issue
&lt;/h2&gt;

&lt;p&gt;Issues give us business context. Summary, status, priority, type, component, and timestamps are stored.&lt;/p&gt;
&lt;h2&gt;
  
  
  PullRequest
&lt;/h2&gt;

&lt;p&gt;PRs show us how a change was discussed within the team. Reviewers, changed files, linked issues, and commits are among the key fields.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 6: Schema
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;developers&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;commit_files&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;commit_issues&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;pull_requests&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;pr_commits&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;pr_files&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;pr_issues&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;issue_comments&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These tables represent graph thinking in a relational model. Join tables like &lt;code&gt;commit_files&lt;/code&gt;, &lt;code&gt;commit_issues&lt;/code&gt;, &lt;code&gt;pr_files&lt;/code&gt;, &lt;code&gt;pr_issues&lt;/code&gt; serve as relationships.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 7: Agent Scores
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Expertise Score
&lt;/h2&gt;

&lt;p&gt;When finding an expert for a file, looking only at commit count can be misleading. So the score can be calculated as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expertise_score =
    commit_count * 10
  + review_count * 8
  + issue_comment_count * 2
  + churn / 20
  + recency_bonus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This score is not an absolute truth; it's a ranking signal. What matters is that the score is explainable.&lt;/p&gt;

&lt;p&gt;Bad output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ayşe is an expert on this topic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Good output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ayşe made 5 commits in this file recently, reviewed 3 PRs,
last activity was 2026-05-20, and total churn value is 320.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Risk Score
&lt;/h2&gt;

&lt;p&gt;Explainable signals are needed for risk too:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;risk_score =
    churn
  + bug_count * 100
  + contributor_count * 25
  + commit_count * 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a simple starting point. In production, signals like test coverage, incidents, revert commits, deployment failures, and code ownership can be added.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 8: Example Usage Scenario
&lt;/h1&gt;

&lt;p&gt;A new developer picks up issue &lt;code&gt;PROJ-1247&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They run this from the CLI:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;teammemory issue-context PROJ-1247
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The system produces:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issue: PROJ-1247
Summary: Token refresh race condition
Status: In Progress
Priority: High
Component: auth

Related PRs:
- #382 Fix token refresh race condition [merged]

Commits:
- f00ba47 Mehmet Turac — PROJ-1247 guard token refresh with per-session lock
- b91c0de Ayşe Demir — PROJ-1247 add regression test for refresh race

Changed files:
- src/auth/token_service.py
- src/auth/session_manager.py
- tests/auth/test_token_refresh.py

People in context:
- Mehmet Turac
- Ayşe Demir
- Burak Kaya
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This output was generated without an LLM. Because everything is based on relationships in the database.&lt;/p&gt;

&lt;p&gt;Then the developer wants to see file experts:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;teammemory file-experts src/auth/token_service.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Experts for src/auth/token_service.py

1. Ayşe Demir — score 92.0
   commits: 4, reviews: 2, comments: 1, churn: 430, last activity: 2026-05-20

2. Mehmet Turac — score 80.5
   commits: 3, reviews: 1, comments: 2, churn: 390, last activity: 2026-05-18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This answer too is not a guess — it's a calculated signal.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 9: Data Hygiene
&lt;/h1&gt;

&lt;p&gt;The success of this system depends on data quality. If commit messages don't contain issue keys, PR descriptions are empty, or issues aren't linked to the right components, the team memory stays incomplete.&lt;/p&gt;

&lt;p&gt;That's why HygieneAgent is critically important.&lt;/p&gt;

&lt;p&gt;What it reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commits that don't contain an issue key&lt;/li&gt;
&lt;li&gt;PRs not linked to an issue&lt;/li&gt;
&lt;li&gt;Empty PR descriptions&lt;/li&gt;
&lt;li&gt;Issues marked as Done but not linked to any commit&lt;/li&gt;
&lt;li&gt;Files missing component information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This report is not a blame tool — it's a tool for improving memory.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 10: Moving to Production
&lt;/h1&gt;

&lt;p&gt;The demo runs with SQLite. The recommended structure for production:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL = raw event store, audit, checkpoint, agent outputs
Neo4j/AGE   = relationship analysis and traversal
FastAPI     = controlled access layer
CLI/Bot     = developer workflow integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Things to pay attention to in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incremental sync&lt;/li&gt;
&lt;li&gt;Webhook + scheduled backfill&lt;/li&gt;
&lt;li&gt;Idempotent ingestion&lt;/li&gt;
&lt;li&gt;Rate limit management&lt;/li&gt;
&lt;li&gt;Identity resolution&lt;/li&gt;
&lt;li&gt;Permission control&lt;/li&gt;
&lt;li&gt;Audit log&lt;/li&gt;
&lt;li&gt;Token security&lt;/li&gt;
&lt;li&gt;Repository-based access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Identity resolution is especially important. If the same person appears as &lt;code&gt;mehmet@example.com&lt;/code&gt; in Git, &lt;code&gt;mturac&lt;/code&gt; on GitHub, and &lt;code&gt;Mehmet Turac&lt;/code&gt; in Jira, all of these need to be linked to a single developer record.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 11: Strengths of This Approach
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Fully auditable.&lt;/li&gt;
&lt;li&gt;Inexpensive.&lt;/li&gt;
&lt;li&gt;Produces the same answer to the same query on the same data.&lt;/li&gt;
&lt;li&gt;No LLM latency.&lt;/li&gt;
&lt;li&gt;No model dependency.&lt;/li&gt;
&lt;li&gt;No prompt brittleness.&lt;/li&gt;
&lt;li&gt;Data security is easier to control.&lt;/li&gt;
&lt;li&gt;Small agents are testable.&lt;/li&gt;
&lt;li&gt;Can be incrementally added to legacy projects.&lt;/li&gt;
&lt;li&gt;Instills engineering discipline in the team.&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  Section 12: Weaknesses
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;No natural language querying.&lt;/li&gt;
&lt;li&gt;If data quality is poor, results degrade.&lt;/li&gt;
&lt;li&gt;Informal decision sources like Slack are left out of the first version.&lt;/li&gt;
&lt;li&gt;Initial identity matching is tedious.&lt;/li&gt;
&lt;li&gt;Score design requires care.&lt;/li&gt;
&lt;li&gt;If the reason for a decision isn't written in a commit or PR, the system can't know it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These limitations are not flaws. On the contrary, they are the system's honesty. It doesn't make things up when it doesn't know.&lt;/p&gt;


&lt;h1&gt;
  
  
  Section 13: Roadmap
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Phase 1 — Local Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQLite schema&lt;/li&gt;
&lt;li&gt;Seed data&lt;/li&gt;
&lt;li&gt;CLI
~~- ContextAgent&lt;/li&gt;
&lt;li&gt;ExpertiseAgent&lt;/li&gt;
&lt;li&gt;RiskAgent~~&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Phase 2 — Real Git Ingestion
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pulling commits from a local repo&lt;/li&gt;
&lt;li&gt;Fetching file changes&lt;/li&gt;
&lt;li&gt;Extracting Jira keys from commit messages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Phase 3 — Jira/GitHub Import
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Jira JSON import&lt;/li&gt;
&lt;li&gt;GitHub PR JSON import&lt;/li&gt;
&lt;li&gt;Review records&lt;/li&gt;
&lt;li&gt;PR-issue relationships&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Phase 4 — API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI endpoints&lt;/li&gt;
&lt;li&gt;Simple dashboard&lt;/li&gt;
&lt;li&gt;GitHub Action integration&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Phase 5 — Production Memory
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL event store&lt;/li&gt;
&lt;li&gt;Neo4j graph projection&lt;/li&gt;
&lt;li&gt;Webhook sync&lt;/li&gt;
&lt;li&gt;Permission control&lt;/li&gt;
&lt;li&gt;Audit log&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;The main idea of this article is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First build the data model correctly for team memory. Don't rush to LLMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Jira, GitHub, and Git already give us an incredibly valuable event history. If we correctly link this history, we can produce reliable answers to questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who changed what?&lt;/li&gt;
&lt;li&gt;Why did they change it?&lt;/li&gt;
&lt;li&gt;Which issue was it related to?&lt;/li&gt;
&lt;li&gt;Which PR was it discussed in?&lt;/li&gt;
&lt;li&gt;Which files are risky?&lt;/li&gt;
&lt;li&gt;Which developer has current context in which area?&lt;/li&gt;
&lt;li&gt;Where should a new person start?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this system, answers don't come with "the model thought so." Answers come from commit, issue, PR, and review records.&lt;/p&gt;

&lt;p&gt;Sometimes the best engineering is not using the most impressive technology; it's correctly scoping the problem and building a simpler, more reliable, and more explainable solution.&lt;/p&gt;

&lt;p&gt;And this repo is trying to show exactly that:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No LLM.
No RAG.
No prompt.
No embedding.

There is data.
There are relationships.
There are rules.
There is evidence.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/mturac" rel="noopener noreferrer"&gt;
        mturac
      &lt;/a&gt; / &lt;a href="https://github.com/mturac/team-memory" rel="noopener noreferrer"&gt;
        team-memory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TeamMemory LLM’siz&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;TeamMemory LLM’siz&lt;/strong&gt;, yazılım ekipleri için Jira + GitHub + Git commit loglarından çalışan, tamamen deterministik bir takım hafızası örneğidir.&lt;/p&gt;

&lt;p&gt;Bu repo özellikle şunu göstermek için hazırlandı:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Her takım hafızası problemi LLM, RAG, embedding, prompt veya agentic workflow gerektirmez. Bazen doğru veri modeli, iyi ingestion, sağlam sorgular ve küçük deterministik agent’lar daha güvenilir sonuç verir.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bu örnekte &lt;strong&gt;LLM yoktur&lt;/strong&gt;.&lt;br&gt;
RAG yoktur.&lt;br&gt;
Vector database yoktur.&lt;br&gt;
Prompt yoktur.&lt;br&gt;
Model çağrısı yoktur.&lt;/p&gt;
&lt;p&gt;Bunun yerine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQLite event/memory store&lt;/li&gt;
&lt;li&gt;Git commit ingestion&lt;/li&gt;
&lt;li&gt;Jira/GitHub JSON import&lt;/li&gt;
&lt;li&gt;Deterministik agent sınıfları&lt;/li&gt;
&lt;li&gt;CLI&lt;/li&gt;
&lt;li&gt;Opsiyonel FastAPI API&lt;/li&gt;
&lt;li&gt;Seed demo datası&lt;/li&gt;
&lt;li&gt;Kanıtlı çıktılar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;vardır.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Hızlı başlangıç&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; teammemory-llmsiz
python -m venv .venv
&lt;span class="pl-c1"&gt;source&lt;/span&gt; .venv/bin/activate
python -m pip install -e .[api,dev]

teammemory init-db --reset
teammemory seed
teammemory issue-context PROJ-1247
teammemory file-experts src/auth/token_service.py
teammemory component-risk auth
teammemory onboarding auth
teammemory review-suggest 382
teammemory hygiene&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;API çalıştırmak için:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;uvicorn teammemory.api:app --reload&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Örnek endpoint’ler:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;curl http://127.0.0.1:8000/issues/PROJ-1247/context
curl &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;http://127.0.0.1:8000/files/experts?path=src/auth/token_service.py&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
curl http://127.0.0.1:8000/components/auth/risk&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mturac/team-memory" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>discuss</category>
      <category>programming</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work Bonus: REST vs GraphQL vs gRPC: When to Use What</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-rest-vs-graphql-vs-grpc-when-to-use-what-1h6i</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-rest-vs-graphql-vs-grpc-when-to-use-what-1h6i</guid>
      <description>&lt;p&gt;&lt;em&gt;The honest comparison nobody asked for but everyone needs.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  REST: The Default That's Fine
&lt;/h2&gt;

&lt;p&gt;REST works. It's been working since 2000. Every developer knows it. Every tool supports it. Every proxy, cache, CDN, and load balancer understands HTTP verbs and status codes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose REST when:&lt;/strong&gt; Your API serves multiple clients with straightforward CRUD operations. Your team is small or mixed-experience. You need HTTP caching. You want the broadest tooling ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST hurts when:&lt;/strong&gt; Mobile clients need 6 endpoints to render one screen (over-fetching). Different clients need different fields from the same resource (under-fetching/over-fetching). Your API surface is large and documentation gets stale.&lt;/p&gt;




&lt;h2&gt;
  
  
  GraphQL: The Flexible One
&lt;/h2&gt;

&lt;p&gt;GraphQL lets clients ask for exactly the data they need in a single request. No more over-fetching. No more calling 6 endpoints to build a screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose GraphQL when:&lt;/strong&gt; You have multiple client types (web, mobile, third-party) that need different data shapes. Frontend teams want to iterate without waiting for backend API changes. Your data model is a graph with complex relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL hurts when:&lt;/strong&gt; You underestimate the complexity. Query cost analysis (preventing clients from requesting deeply nested, expensive queries) is a whole discipline. Caching is harder because every query can be unique — no URL to cache against. N+1 query problems move from the client to the server-side resolver layer, and DataLoader only helps if you implement it correctly.&lt;/p&gt;

&lt;p&gt;The security surface is also larger. A careless schema can let clients request your entire database through nested relationships. Rate limiting by query complexity (not just request count) is essential and non-trivial.&lt;/p&gt;




&lt;h2&gt;
  
  
  gRPC: The Fast One
&lt;/h2&gt;

&lt;p&gt;gRPC uses Protocol Buffers (binary serialization) and HTTP/2 (multiplexed streams). It's faster than JSON over REST by a significant margin: smaller payloads, faster serialization, bidirectional streaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose gRPC when:&lt;/strong&gt; Service-to-service communication where latency matters. Streaming use cases (real-time data feeds, long-running operations). You control both ends of the connection and can generate typed clients from proto files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gRPC hurts when:&lt;/strong&gt; You need browser support (gRPC-Web exists but adds complexity). Your clients are third-party developers who expect a REST API. Debugging is harder because binary payloads aren't human-readable. Load balancers need HTTP/2 support.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Answer
&lt;/h2&gt;

&lt;p&gt;Most teams should default to REST for external APIs and consider gRPC for internal service-to-service calls. GraphQL makes sense when your frontend team is spending more time waiting for API changes than building features.&lt;/p&gt;

&lt;p&gt;The worst decision is choosing a technology because it's interesting. gRPC is fascinating. GraphQL is elegant. REST is boring. Boring wins when you're paged at 3 AM and need to debug a failed request by reading the URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;REST, GraphQL, or gRPC — what's your default choice in 2026 and why? Anyone running all three in the same platform?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work #5 — Linux: "Not a Kernel Panic, an Engineer Panic"</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Thu, 04 Jun 2026 17:52:37 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-5-linux-not-a-kernel-panic-an-engineer-panic-1bke</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-5-linux-not-a-kernel-panic-an-engineer-panic-1bke</guid>
      <description>&lt;p&gt;&lt;em&gt;A survival guide for when everything goes wrong in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The system is slow. Not crashing, not failing — just slow. Response times are 10x normal. CPU usage looks fine. Memory looks fine. Disk looks fine. Every metric on the dashboard says "normal" but nothing feels normal.&lt;/p&gt;

&lt;p&gt;The problem isn't in your application. It's three layers below, in kernel parameters you've never touched because the defaults "should be fine."&lt;/p&gt;

&lt;p&gt;The defaults are fine for a laptop. They're not fine for a server handling 50,000 concurrent connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  CPU: The Scheduler Isn't Always Fair
&lt;/h2&gt;

&lt;p&gt;Linux uses CFS (Completely Fair Scheduler). It distributes CPU time proportionally across processes based on priority (nice values) and cgroup allocations. CFS is good at being fair. It's not always good at being fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nice values&lt;/strong&gt; range from -20 (highest priority) to 19 (lowest). Your application runs at nice 0 by default. A batch job someone started with &lt;code&gt;nice -n 19&lt;/code&gt; runs at the lowest priority — it gets CPU time only when nothing else wants it.&lt;/p&gt;

&lt;p&gt;But nice values only matter under contention. If you have 16 cores and 8 processes, nice values are irrelevant — everyone gets a core. They start mattering when you have 32 processes competing for 16 cores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU pinning (taskset/cpuset):&lt;/strong&gt; For latency-sensitive workloads, pin your application to specific cores and keep everything else off them. This eliminates cache pollution — when processes bounce between cores, they lose their L1/L2 cache lines and spend cycles reloading data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pin process to cores 0-3&lt;/span&gt;
taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 0-3 ./my-application

&lt;span class="c"&gt;# Or via cgroups&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"0-3"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/cpuset/my-app/cpuset.cpus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Financial trading systems and game servers live and die by CPU pinning. For web services, it's rarely worth the operational complexity — unless you've measured and confirmed cache misses are your bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numa trap:&lt;/strong&gt; On multi-socket servers, NUMA (Non-Uniform Memory Access) means each CPU socket has "local" memory and "remote" memory. Accessing remote memory is 2-3x slower. If your application runs on socket 0 but allocates memory on socket 1's RAM, every memory access pays a latency penalty.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;numactl &lt;span class="nt"&gt;--hardware&lt;/span&gt;     &lt;span class="c"&gt;# See NUMA topology&lt;/span&gt;
numactl &lt;span class="nt"&gt;--localalloc&lt;/span&gt; ./my-application   &lt;span class="c"&gt;# Force local memory allocation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most cloud VMs abstract NUMA away, but bare metal servers? Check your topology.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory: Page Cache Is Your Best Friend
&lt;/h2&gt;

&lt;p&gt;Linux uses all free memory as page cache — buffering disk reads in RAM. When you see "10 GB used, 2 GB free" on a 16 GB server, it doesn't mean you're low on memory. It means 4 GB is page cache, and it'll be released the moment a process needs it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;free -h&lt;/code&gt; lies to you if you don't read it carefully. Look at the "available" column, not "free." Available = free + reclaimable cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swap:&lt;/strong&gt; When physical memory is exhausted, Linux moves pages to swap (disk). This prevents OOM kills but makes the system extremely slow. Disk access is 1,000x slower than RAM. A system actively swapping is a system dying slowly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vm.swappiness&lt;/code&gt; controls how aggressively the kernel swaps. Default is 60. For database servers: set it to 1 (not 0 — 0 disables swap entirely, which means the OOM killer strikes without warning). For Redis: set it to 1 and monitor closely. Redis's dataset should never touch swap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl vm.swappiness&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OOM Killer:&lt;/strong&gt; When memory is truly exhausted and swap (if any) is full, the kernel picks a process to kill. It chooses based on memory usage and &lt;code&gt;oom_score_adj&lt;/code&gt;. Critical processes should have a low score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-1000&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /proc/&lt;span class="si"&gt;$(&lt;/span&gt;pidof my-critical-app&lt;span class="si"&gt;)&lt;/span&gt;/oom_score_adj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the OOM killer: kill anything else before touching this process. But if it's the only process eating memory, even -1000 won't save it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The team that disabled swap:&lt;/strong&gt; They read a blog post saying swap hurts performance. They set &lt;code&gt;vm.swappiness=0&lt;/code&gt; and disabled the swap partition. For months, everything was fine — they had plenty of RAM. Then a memory leak in a sidecar container slowly consumed memory over 3 weeks. Without swap as a buffer, the OOM killer fired without warning at 2 AM, killing the primary database process. No graceful shutdown. Transaction log corruption. 4-hour recovery.&lt;/p&gt;

&lt;p&gt;Swap isn't the enemy. Uncontrolled swap is the enemy. A small swap partition (2-4 GB) gives the OOM killer a buffer to detect memory pressure before killing processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  I/O: The Scheduler You Didn't Know Existed
&lt;/h2&gt;

&lt;p&gt;Disk I/O has its own scheduler, separate from the CPU scheduler. It determines the order in which read/write requests reach the disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deadline:&lt;/strong&gt; Assigns a deadline to each request (500ms for reads, 5s for writes by default). Guarantees no request starves. Good for databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mq-deadline:&lt;/strong&gt; Multi-queue version of deadline. For NVMe drives with hardware multi-queue support, this is the default and correct choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;none (noop):&lt;/strong&gt; No reordering. Passes requests directly to the device. Use for NVMe SSDs where the device has its own sophisticated scheduler. Adding a kernel scheduler on top just adds latency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check current scheduler&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/block/sda/queue/scheduler

&lt;span class="c"&gt;# Change it&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/block/nvme0n1/queue/scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SSDs and NVMe: use &lt;code&gt;none&lt;/code&gt; or &lt;code&gt;mq-deadline&lt;/code&gt;. For spinning disks (if you still have them): use &lt;code&gt;deadline&lt;/code&gt; or &lt;code&gt;bfq&lt;/code&gt; (Budget Fair Queuing, good for interactive workloads).&lt;/p&gt;




&lt;h2&gt;
  
  
  Network Stack: The Parameters That Change Everything
&lt;/h2&gt;

&lt;p&gt;Default Linux network settings are conservative. They're designed for a general-purpose machine, not a server handling tens of thousands of connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;net.core.somaxconn&lt;/code&gt;:&lt;/strong&gt; The maximum number of connections that can be queued for acceptance. Default: 4096 (was 128 on older kernels). If your application can't accept connections fast enough, new connections get dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.core.somaxconn&lt;span class="o"&gt;=&lt;/span&gt;65535
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx, HAProxy, and any high-connection service should have this bumped. Also increase the application's own listen backlog to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;net.ipv4.tcp_tw_reuse&lt;/code&gt;:&lt;/strong&gt; Allows reusing sockets in TIME_WAIT state for new outgoing connections. On a server making many short-lived connections to backend services, TIME_WAIT sockets can accumulate in the thousands, exhausting ephemeral ports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.tcp_tw_reuse&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;net.core.rmem_max&lt;/code&gt; / &lt;code&gt;net.core.wmem_max&lt;/code&gt;:&lt;/strong&gt; Maximum receive and send buffer sizes. Default values are often too low for high-throughput applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.core.rmem_max&lt;span class="o"&gt;=&lt;/span&gt;16777216
sysctl net.core.wmem_max&lt;span class="o"&gt;=&lt;/span&gt;16777216
sysctl net.ipv4.tcp_rmem&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4096 87380 16777216"&lt;/span&gt;
sysctl net.ipv4.tcp_wmem&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4096 65536 16777216"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three values in &lt;code&gt;tcp_rmem&lt;/code&gt; and &lt;code&gt;tcp_wmem&lt;/code&gt; are: minimum, default, maximum. The kernel auto-tunes within this range based on available memory and connection count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;net.ipv4.tcp_keepalive_time&lt;/code&gt;:&lt;/strong&gt; How long a connection sits idle before sending keepalive probes. Default: 7200 seconds (2 hours). If a client disconnects without closing the connection (network failure, crash), the server won't notice for 2 hours. That's 2 hours of a socket slot wasted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.ipv4.tcp_keepalive_time&lt;span class="o"&gt;=&lt;/span&gt;600
sysctl net.ipv4.tcp_keepalive_intvl&lt;span class="o"&gt;=&lt;/span&gt;60
sysctl net.ipv4.tcp_keepalive_probes&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Profiling: perf, strace, eBPF
&lt;/h2&gt;

&lt;p&gt;When the metrics don't tell you enough, go deeper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;perf&lt;/code&gt;&lt;/strong&gt; — CPU profiling. Shows you where CPU time is being spent at the function level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Record 30 seconds of CPU activity&lt;/span&gt;
perf record &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pidof my-app&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;30
perf report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flame graph (generated with Brendan Gregg's scripts) makes &lt;code&gt;perf&lt;/code&gt; output readable. The widest bars are where your CPU spends the most time. If 40% of CPU time is in &lt;code&gt;malloc&lt;/code&gt;, you have a memory allocation problem. If 30% is in &lt;code&gt;pthread_mutex_lock&lt;/code&gt;, you have a contention problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;strace&lt;/code&gt;&lt;/strong&gt; — System call tracing. Shows every interaction between your application and the kernel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;strace &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pidof my-app&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network &lt;span class="nt"&gt;-T&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; follows child threads. &lt;code&gt;-e trace=network&lt;/code&gt; filters to network calls only. &lt;code&gt;-T&lt;/code&gt; shows time spent in each syscall. If &lt;code&gt;connect()&lt;/code&gt; calls are taking 50ms, your DNS resolution is slow. If &lt;code&gt;write()&lt;/code&gt; calls are taking 10ms, your disk or network is the bottleneck.&lt;/p&gt;

&lt;p&gt;Warning: &lt;code&gt;strace&lt;/code&gt; adds overhead. Don't run it on a production process during peak traffic unless you understand the impact. For production tracing, use eBPF instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;eBPF&lt;/code&gt;&lt;/strong&gt; — The modern way to observe production systems without overhead. eBPF programs run in the kernel, attached to specific events, with verified safety guarantees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Using bcc tools&lt;/span&gt;
tcplife          &lt;span class="c"&gt;# Track TCP connection lifetimes&lt;/span&gt;
biolatency       &lt;span class="c"&gt;# Disk I/O latency histogram&lt;/span&gt;
runqlat          &lt;span class="c"&gt;# CPU scheduler queue latency&lt;/span&gt;
funccount        &lt;span class="c"&gt;# Count function calls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eBPF tools like &lt;code&gt;bcc&lt;/code&gt; and &lt;code&gt;bpftrace&lt;/code&gt; give you kernel-level visibility without modifying your application or adding measurable overhead. They're the reason modern observability is possible without sampling bias.&lt;/p&gt;




&lt;h2&gt;
  
  
  The USE Method: Systematic Performance Analysis
&lt;/h2&gt;

&lt;p&gt;Brendan Gregg's USE method: for every resource (CPU, memory, disk, network), check Utilization, Saturation, and Errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilization: &lt;code&gt;mpstat -P ALL 1&lt;/code&gt; — per-core usage&lt;/li&gt;
&lt;li&gt;Saturation: &lt;code&gt;vmstat&lt;/code&gt; — check &lt;code&gt;r&lt;/code&gt; column (run queue). If it's higher than core count, CPUs are overloaded&lt;/li&gt;
&lt;li&gt;Errors: &lt;code&gt;dmesg | grep -i error&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilization: &lt;code&gt;free -h&lt;/code&gt; — check "available"&lt;/li&gt;
&lt;li&gt;Saturation: &lt;code&gt;vmstat&lt;/code&gt; — check &lt;code&gt;si&lt;/code&gt;/&lt;code&gt;so&lt;/code&gt; (swap in/out). Any non-zero value means swapping&lt;/li&gt;
&lt;li&gt;Errors: &lt;code&gt;dmesg | grep -i oom&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disk:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilization: &lt;code&gt;iostat -xz 1&lt;/code&gt; — check &lt;code&gt;%util&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Saturation: &lt;code&gt;iostat&lt;/code&gt; — check &lt;code&gt;avgqu-sz&lt;/code&gt; (average queue size). High values mean requests are waiting&lt;/li&gt;
&lt;li&gt;Errors: &lt;code&gt;smartctl -a /dev/sda&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilization: &lt;code&gt;sar -n DEV 1&lt;/code&gt; — bytes per second vs link capacity&lt;/li&gt;
&lt;li&gt;Saturation: &lt;code&gt;netstat -s | grep -i drop&lt;/code&gt; — dropped packets&lt;/li&gt;
&lt;li&gt;Errors: &lt;code&gt;ifconfig&lt;/code&gt; or &lt;code&gt;ip -s link&lt;/code&gt; — check error counters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go through this checklist when "the system is slow." Most of the time, one resource will be saturated and everything else will look fine. That's your bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 7 Kernel Parameters Story
&lt;/h2&gt;

&lt;p&gt;Production API server. Latency: 200ms average, 800ms P99. After a week of profiling, all the time was in kernel-level network and memory operations, not application code.&lt;/p&gt;

&lt;p&gt;The 7 parameters that changed everything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;net.core.somaxconn = 65535&lt;/code&gt; (was 128)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net.ipv4.tcp_tw_reuse = 1&lt;/code&gt; (was 0)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net.core.rmem_max = 16777216&lt;/code&gt; (was 212992)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net.core.wmem_max = 16777216&lt;/code&gt; (was 212992)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vm.swappiness = 1&lt;/code&gt; (was 60)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net.ipv4.tcp_keepalive_time = 600&lt;/code&gt; (was 7200)&lt;/li&gt;
&lt;li&gt;I/O scheduler to &lt;code&gt;none&lt;/code&gt; (was &lt;code&gt;cfq&lt;/code&gt; on an NVMe drive)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: average latency dropped to 20ms. P99 dropped to 85ms. No code changes. No infrastructure changes. Seven &lt;code&gt;sysctl&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;The defaults are designed for safety and generality. Production servers are neither safe nor general — they're specific, high-performance machines with specific workloads. Tune accordingly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;The kernel is not a black box. &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; expose everything. &lt;code&gt;perf&lt;/code&gt;, &lt;code&gt;strace&lt;/code&gt;, and eBPF let you look inside without guessing.&lt;/p&gt;

&lt;p&gt;When "the system is slow," use the USE method. Check utilization, saturation, and errors for every resource. The bottleneck will reveal itself.&lt;/p&gt;

&lt;p&gt;Default kernel parameters are fine for development machines. They're wrong for production. Every production server should have a tuned &lt;code&gt;sysctl.conf&lt;/code&gt; based on its workload.&lt;/p&gt;

&lt;p&gt;And never disable swap without understanding what happens when memory runs out. The OOM killer doesn't negotiate.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;Which kernel parameter change gave you the biggest performance win? Have you ever had the OOM killer strike at the worst possible moment?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>backend</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work Bonus: 10 Docker Production Traps</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Wed, 03 Jun 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-10-docker-production-traps-3kki</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-10-docker-production-traps-3kki</guid>
      <description>&lt;h1&gt;
  
  
  Great Stack to Doesn't Work — Bonus
&lt;/h1&gt;

&lt;h1&gt;
  
  
  10 Docker Production Traps
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Your Dockerfile works on your machine. Here's why it breaks everywhere else.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Your image is 2 GB because you're not using multi-stage builds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;RUN&lt;/code&gt; command creates a layer. If you install build tools, compile your app, and leave the build tools in the final image, you're shipping a toolbox alongside your application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Production stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build tools stay in the builder stage. The final image only has what it needs to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your layer cache invalidates every build because COPY order is wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker caches layers. If a layer hasn't changed, Docker reuses it. But layers are sequential — if layer 3 changes, layers 4, 5, 6 all rebuild.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# BAD: code changes invalidate npm install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# GOOD: dependencies cached separately from code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code changes every build. Your &lt;code&gt;package.json&lt;/code&gt; changes occasionally. Copy the dependency manifest first, install, then copy the code. Now dependency installation is cached unless the manifest actually changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You're running as root.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Default Docker containers run as root. If an attacker exploits your application, they have root access inside the container. With certain volume mounts or misconfigurations, that can mean root on the host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. Massive security improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. You don't have a .dockerignore file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;.dockerignore&lt;/code&gt;, &lt;code&gt;COPY . .&lt;/code&gt; sends everything to the Docker daemon: &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; files, test fixtures, IDE configs. Slower builds, larger context, and potential secrets leaked into the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;
*.&lt;span class="n"&gt;md&lt;/span&gt;
&lt;span class="n"&gt;test&lt;/span&gt;/
&lt;span class="n"&gt;coverage&lt;/span&gt;/
.&lt;span class="n"&gt;DS_Store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. You're not using health checks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker doesn't know if your application is healthy. It knows if the process is running. A Node.js server stuck in an infinite loop? Process is running. Docker says healthy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s --retries=3 \&lt;/span&gt;
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Docker can detect unhealthy containers and orchestrators can restart them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Your logs disappear because you're writing to a file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker captures stdout and stderr. If your application writes logs to &lt;code&gt;/var/log/app.log&lt;/code&gt;, Docker's logging driver never sees them. &lt;code&gt;docker logs&lt;/code&gt; returns nothing. Your centralized logging system collects nothing.&lt;/p&gt;

&lt;p&gt;Log to stdout. Let Docker handle routing. Use a logging driver (json-file, fluentd, gelf) to send logs wherever they need to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. You're using &lt;code&gt;latest&lt;/code&gt; tag in production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FROM node:latest&lt;/code&gt; means a different image every time someone builds. What worked last week might break today because &lt;code&gt;latest&lt;/code&gt; moved to a new version. Pin your versions: &lt;code&gt;FROM node:20.11-alpine&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same for your own images. Never deploy &lt;code&gt;myapp:latest&lt;/code&gt; to production. Use commit hashes or semantic versions: &lt;code&gt;myapp:1.4.2&lt;/code&gt; or &lt;code&gt;myapp:abc123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Your volume permissions break when switching between Linux and Mac.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Files created inside a container often have root ownership. When mounted to a Mac via Docker Desktop, this might work fine. On Linux, it breaks because your host user can't read root-owned files.&lt;/p&gt;

&lt;p&gt;Set ownership explicitly in your Dockerfile or use &lt;code&gt;--user&lt;/code&gt; flags to match host user IDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. You're not setting memory limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without memory limits, one container can consume all host memory and trigger the OOM killer, taking down other containers with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512m myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Kubernetes, this maps to resource limits. Set them. Always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. You're rebuilding when you should be restarting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not every configuration change requires a new image. Environment variables, mounted config files, and feature flags can change at runtime. If you're rebuilding and redeploying because someone changed a log level, your deployment pipeline is doing too much work.&lt;/p&gt;

&lt;p&gt;Separate build-time decisions (code, dependencies, base image) from run-time decisions (config, secrets, feature flags). Build less. Deploy smarter.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;Which Docker trap cost you the most debugging time? Any production Docker disaster stories you can share?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>beginners</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work #4 — Kubernetes: "Pod Is Running, App Is Dead"</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Tue, 02 Jun 2026 07:11:42 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-4-kubernetes-pod-is-running-app-is-dead-218g</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-4-kubernetes-pod-is-running-app-is-dead-218g</guid>
      <description>&lt;p&gt;&lt;em&gt;A survival guide for when everything goes wrong in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The pod is Running. STATUS says Running. kubectl says Running. The deployment shows 3/3 replicas available. Every signal says this thing is alive.&lt;/p&gt;

&lt;p&gt;But your users are getting timeouts. The health check endpoint returns 200, but the application thread pool is exhausted. The container is up. The process is running. The application is dead.&lt;/p&gt;

&lt;p&gt;Kubernetes trusts your probes. If your probes lie, Kubernetes believes the lie.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Probes: liveness, readiness, startup
&lt;/h2&gt;

&lt;p&gt;These three probes look similar but serve completely different purposes. Mixing them up is responsible for more outages than any other Kubernetes misconfiguration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liveness probe:&lt;/strong&gt; "Is this container broken beyond recovery?" If it fails, Kubernetes kills the container and restarts it. This is a last resort. If your liveness probe checks a database connection and the database is down, Kubernetes restarts your pod. The pod comes back. The database is still down. The liveness probe fails again. CrashLoopBackOff. You now have zero capacity instead of degraded capacity.&lt;/p&gt;

&lt;p&gt;Liveness probes should check if the process itself is stuck — deadlocked threads, corrupted internal state, unresponsive event loop. They should NOT check downstream dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readiness probe:&lt;/strong&gt; "Can this container handle traffic right now?" If it fails, Kubernetes removes the pod from the Service endpoints. Traffic stops flowing to it, but the container stays alive. When readiness passes again, traffic resumes.&lt;/p&gt;

&lt;p&gt;Readiness probes SHOULD check downstream dependencies. If your app can't reach the database, it shouldn't receive requests. Remove it from the load balancer, let other healthy pods handle traffic, and wait for the dependency to recover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Startup probe:&lt;/strong&gt; "Is this container still booting?" Runs only during startup. While the startup probe is running, liveness and readiness probes are disabled. This exists for applications with long initialization times — JVM warmup, large model loading, database migration runs.&lt;/p&gt;

&lt;p&gt;Without a startup probe, an application that takes 60 seconds to start will fail the liveness probe (default 10-second timeout) and get killed before it ever finishes booting. CrashLoopBackOff on a perfectly healthy app that just needs more time.&lt;/p&gt;

&lt;p&gt;The correct pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;startupProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/startup&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="c1"&gt;# 30 * 5 = 150 seconds to start up&lt;/span&gt;

&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/live&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="c1"&gt;# Only runs after startup succeeds&lt;/span&gt;

&lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health/ready&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="c1"&gt;# Can toggle on/off during lifetime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate endpoints. Three different checks. Don't make them the same URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources: The Art of Requests and Limits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Requests&lt;/strong&gt; tell the Kubernetes scheduler how much resource to guarantee. If your pod requests 500m CPU and 256Mi memory, the scheduler only places it on a node with that much available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits&lt;/strong&gt; tell the kernel how much the container is allowed to use. Exceeding the memory limit triggers an OOMKill. Exceeding the CPU limit triggers throttling.&lt;/p&gt;

&lt;p&gt;The dangerous configurations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No requests, no limits:&lt;/strong&gt; The pod is a BestEffort class. It gets whatever's available. Under node pressure, it's the first to be evicted. Never do this in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests equal to limits (Guaranteed QoS):&lt;/strong&gt; The pod gets exactly what it asks for. No bursting above, no getting evicted under pressure (unless the node itself is failing). Predictable but expensive — you're reserving resources even when idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests lower than limits (Burstable QoS):&lt;/strong&gt; The pod is guaranteed its request amount and can burst up to its limit when resources are available. This is the most common production configuration. The risk: if many pods burst simultaneously, the node runs out, and Kubernetes starts killing Burstable pods that exceed their requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CPU throttling trap:&lt;/strong&gt; CPU limits are enforced using CFS (Completely Fair Scheduler) bandwidth control. If your pod's limit is 1000m (1 core) and it needs a 200ms burst of 2 cores, it gets throttled for 100ms. Your application doesn't crash — it just gets mysteriously slow. &lt;code&gt;container_cpu_cfs_throttled_seconds_total&lt;/code&gt; in Prometheus will show you if this is happening. Many teams set CPU limits too low and spend weeks debugging intermittent latency before checking throttling metrics.&lt;/p&gt;

&lt;p&gt;My recommendation: set CPU requests but consider leaving CPU limits unset. Let pods burst on CPU. Set memory limits strictly — memory overcommit leads to OOMKills, which are worse than CPU throttling.&lt;/p&gt;




&lt;h2&gt;
  
  
  OOMKilled: Death by Memory
&lt;/h2&gt;

&lt;p&gt;When a container exceeds its memory limit, the kernel kills it instantly. No graceful shutdown. No signal. No chance to flush buffers or close connections. The process is gone.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; shows the exit code: 137 (128 + 9, where 9 is SIGKILL).&lt;/p&gt;

&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory leak:&lt;/strong&gt; Gradual growth over hours or days. The pod works fine after restart, then slowly dies again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spike under load:&lt;/strong&gt; The application allocates memory proportional to concurrent requests. During traffic spikes, memory exceeds the limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JVM heap misconfiguration:&lt;/strong&gt; The JVM's &lt;code&gt;-Xmx&lt;/code&gt; is set higher than the container's memory limit. The JVM thinks it has 4GB but the container only allows 2GB. The moment the heap grows past 2GB, OOMKill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For JVM apps, always set &lt;code&gt;-Xmx&lt;/code&gt; to roughly 75% of the container memory limit. The remaining 25% covers metaspace, thread stacks, native memory, and OS overhead.&lt;/p&gt;

&lt;p&gt;For Node.js apps, set &lt;code&gt;--max-old-space-size&lt;/code&gt; explicitly. The V8 default may exceed your container limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Evictions: When the Node Pushes Back
&lt;/h2&gt;

&lt;p&gt;Eviction happens at the node level, not the pod level. When a node runs low on resources (memory, disk, or PIDs), kubelet starts evicting pods to protect the node.&lt;/p&gt;

&lt;p&gt;Eviction order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;BestEffort pods (no requests/limits) — evicted first&lt;/li&gt;
&lt;li&gt;Burstable pods exceeding their requests&lt;/li&gt;
&lt;li&gt;Guaranteed pods — evicted last, only under extreme pressure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Priority classes&lt;/strong&gt; let you influence this order. Create PriorityClasses for your workloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduling.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PriorityClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical-service&lt;/span&gt;
&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt;
&lt;span class="na"&gt;globalDefault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Critical&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;services"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pods with higher priority evict lower-priority pods when the node is under pressure. Your core payment service survives; your internal analytics job gets evicted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Node Affinity, Taints, and Tolerations
&lt;/h2&gt;

&lt;p&gt;"Why won't my pod schedule?" is the second most common Kubernetes question (after "why is it crashing").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node affinity:&lt;/strong&gt; tells the scheduler which nodes the pod prefers or requires.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;affinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodeAffinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requiredDuringSchedulingIgnoredDuringExecution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nodeSelectorTerms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-type&lt;/span&gt;
              &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
              &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpu"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pod will only run on nodes labeled &lt;code&gt;node-type=gpu&lt;/code&gt;. If no such node exists, the pod stays Pending forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Taints:&lt;/strong&gt; nodes repel pods. A tainted node won't accept pods unless they have a matching toleration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl taint nodes gpu-node-1 &lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;:NoSchedule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now only pods that tolerate &lt;code&gt;gpu=true&lt;/code&gt; can run there. This prevents CPU-only workloads from accidentally landing on expensive GPU nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common scheduling failures:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pod is Pending with "insufficient cpu/memory" — the requested resources exceed what's available on any node. Either reduce requests or add nodes.&lt;/li&gt;
&lt;li&gt;Pod is Pending with "no nodes match pod topology spread constraints" — you have topology rules that can't be satisfied.&lt;/li&gt;
&lt;li&gt;Pod is Pending with "0/5 nodes are available: 5 node(s) had taints that the pod didn't tolerate" — every node is tainted and your pod doesn't have the right tolerations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pod Disruption Budgets: Don't Take Everything Down at Once
&lt;/h2&gt;

&lt;p&gt;During a rolling update, Kubernetes terminates old pods and creates new ones. Without a PDB, Kubernetes can terminate all pods simultaneously if it's feeling aggressive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PodDisruptionBudget&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-pdb&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;minAvailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees at least 2 api pods are always running, even during node drains, cluster upgrades, or voluntary disruptions. Kubernetes will wait to terminate a pod until it can do so without violating the budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 7 Causes of CrashLoopBackOff
&lt;/h2&gt;

&lt;p&gt;CrashLoopBackOff means the container starts, crashes, restarts, crashes again, and Kubernetes increases the delay between restarts exponentially (10s, 20s, 40s, up to 5 minutes).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Application error on startup.&lt;/strong&gt; Missing config, bad environment variable, connection refused to a required service. Check &lt;code&gt;kubectl logs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OOMKilled.&lt;/strong&gt; Memory limit too low. Check &lt;code&gt;kubectl describe pod&lt;/code&gt; for &lt;code&gt;OOMKilled&lt;/code&gt; in Last State.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liveness probe too aggressive.&lt;/strong&gt; The app takes 30 seconds to start, the liveness probe starts at 10 seconds. The probe kills the app before it's ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image pull error masquerading as crash.&lt;/strong&gt; &lt;code&gt;ImagePullBackOff&lt;/code&gt; can look like &lt;code&gt;CrashLoopBackOff&lt;/code&gt; in the events. Check events, not just status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entrypoint/command misconfiguration.&lt;/strong&gt; The CMD in the Dockerfile expects arguments that aren't passed, or the entrypoint script has a bash error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; The container runs as non-root but needs to write to a directory owned by root. Or a mounted secret has wrong permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource quota exhaustion.&lt;/strong&gt; The namespace has a ResourceQuota and the pod's requests exceed what's available in the quota. The pod keeps trying and failing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The debugging flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod &amp;lt;name&amp;gt;          &lt;span class="c"&gt;# Events section&lt;/span&gt;
kubectl logs &amp;lt;name&amp;gt;                  &lt;span class="c"&gt;# Current logs&lt;/span&gt;
kubectl logs &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--previous&lt;/span&gt;       &lt;span class="c"&gt;# Previous crash logs&lt;/span&gt;
kubectl get events &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.lastTimestamp'&lt;/span&gt;  &lt;span class="c"&gt;# Cluster events&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--previous&lt;/code&gt; is the one people forget. The current container has no logs because it just started. The previous container's logs show why it crashed.&lt;/p&gt;




&lt;h2&gt;
  
  
  War Story: The 1-Second Readiness Probe
&lt;/h2&gt;

&lt;p&gt;A payment service. 8 replicas behind a Kubernetes Service. Readiness probe checked &lt;code&gt;/health&lt;/code&gt; with a 1-second timeout. The health endpoint pinged the database.&lt;/p&gt;

&lt;p&gt;Under normal conditions: 200ms response time. Readiness passes. Traffic flows.&lt;/p&gt;

&lt;p&gt;Black Friday: database load increases. Health endpoint response time creeps up. 800ms. 900ms. 1.1 seconds. Readiness probe fails. Kubernetes removes the pod from endpoints.&lt;/p&gt;

&lt;p&gt;Now 7 pods handle the traffic that 8 were handling. Each remaining pod gets more load. Their health endpoints slow down. More probes fail. 6 pods. 5 pods. Cascading failure.&lt;/p&gt;

&lt;p&gt;Within 90 seconds, all 8 pods were removed from the Service. Zero pods receiving traffic. The application was running perfectly — every pod was healthy. But every readiness probe was timing out because the database was slow.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Increased readiness probe timeout to 5 seconds.&lt;/li&gt;
&lt;li&gt;Separated the health check from the database check. Readiness verifies the application can accept HTTP connections. A separate monitoring check verifies database connectivity.&lt;/li&gt;
&lt;li&gt;Added a circuit breaker — if the database is slow, the app returns degraded responses from cache instead of timing out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson: your readiness probe is a load balancer decision. If it's too sensitive, it amplifies problems instead of containing them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Kubernetes doesn't know your application is healthy. It knows your probes pass. Design probes that reflect real application health, not infrastructure connectivity.&lt;/p&gt;

&lt;p&gt;Set memory limits. Don't set CPU limits unless you have a specific reason. Check throttling metrics before assuming your app is slow.&lt;/p&gt;

&lt;p&gt;CrashLoopBackOff is a symptom, not a diagnosis. &lt;code&gt;kubectl logs --previous&lt;/code&gt; is your first tool. &lt;code&gt;kubectl describe pod&lt;/code&gt; is your second. The answer is almost always in the Events section.&lt;/p&gt;

&lt;p&gt;And if your readiness probe checks a downstream dependency, make sure the timeout is generous enough that temporary slowness doesn't cascade into a full outage.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;What's the sneakiest CrashLoopBackOff cause you've debugged? Have you ever had a readiness probe cascade like the one in this article?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>backend</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Stop Shipping AI Slop: Build an Anti-Slop Harness Around Your LLM</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Sat, 30 May 2026 21:48:26 +0000</pubDate>
      <link>https://dev.to/turacthethinker/stop-shipping-ai-slop-build-an-anti-slop-harness-around-your-llm-273b</link>
      <guid>https://dev.to/turacthethinker/stop-shipping-ai-slop-build-an-anti-slop-harness-around-your-llm-273b</guid>
      <description>&lt;p&gt;"AI slop" is not a model problem. It's an engineering problem you decided not to solve.&lt;/p&gt;

&lt;p&gt;The slop is the bland, off-voice, half-hallucinated, occasionally-just-an-error-message text that your LLM emits maybe 5% of the time — and that 5% is the part users screenshot. The instinct is to fix it in the prompt: add three more sentences of "be concise, be accurate, match my tone." That treats a stochastic system as if it were deterministic. It isn't. You cannot prompt your way to a guarantee.&lt;/p&gt;

&lt;p&gt;What actually works is treating the model like any other unreliable upstream dependency: wrap it in a harness that validates, rejects, and retries before anything reaches a user. The model proposes; the harness disposes. Here's how to build one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2F34.68.51.32%3A8001%2Fdiagrams%2Fd225b3e3e2149.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2F34.68.51.32%3A8001%2Fdiagrams%2Fd225b3e3e2149.svg" alt="Stop Shipping AI Slop: Build an Anti-Slop Harness Around Your LLM" width="1200" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Slop is a systems problem, not a prompt problem
&lt;/h2&gt;

&lt;p&gt;Every production LLM feature I've shipped converged on the same shape: the model is one stage in a pipeline, not the pipeline itself. You don't trust raw generation any more than you'd trust raw user input. You parse it, you validate it against constraints you can express in code, and you reject anything that fails — automatically, before a human ever sees it.&lt;/p&gt;

&lt;p&gt;The key insight is that most slop is &lt;em&gt;detectable&lt;/em&gt;. Empty output, a leaked stack trace, the wrong language, a 900-word answer when you asked for 200, a banned phrase like "in today's fast-paced world" — these are all checkable with deterministic code. You don't need a judge model to catch them (though a judge model has its place at the end). You need a gate that runs on every generation, costs microseconds, and never gets tired.&lt;/p&gt;

&lt;p&gt;Think of it as five layers, each rejecting a different class of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Structured output, not freeform text
&lt;/h2&gt;

&lt;p&gt;The single biggest reduction in slop comes from refusing to accept prose where you can demand structure. If you ask for a JSON object with named fields and a schema, the failure modes collapse from "infinite" to "a handful you can enumerate."&lt;/p&gt;

&lt;p&gt;Use the provider's native structured-output / tool-calling mode and attach a real schema — Pydantic, Zod, JSON Schema, whatever your stack speaks. This does two things. First, it forces the model to commit to a shape, which kills rambling preambles ("Sure! Here's a great answer for you..."). Second, it gives you a parse step that &lt;em&gt;fails loudly&lt;/em&gt;. If the model returns something that doesn't validate, that's not a soft warning — it's a rejected generation that triggers a retry. A parse failure is a quality signal, not an exception to swallow.&lt;/p&gt;

&lt;p&gt;The corollary: never &lt;code&gt;try/except: pass&lt;/code&gt; around your parser. A swallowed parse error is slop with the lights turned off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Reject error strings the model smuggles through
&lt;/h2&gt;

&lt;p&gt;This one surprises people. Models are trained on the entire internet, which includes a lot of error messages, apology boilerplate, and refusal language. Under pressure — ambiguous input, a retrieval miss, a truncated context — the model will sometimes emit text that is &lt;em&gt;syntactically valid&lt;/em&gt; but semantically garbage: "I'm sorry, I cannot access that file," "Error: undefined," "As an AI language model, I don't have the ability to...," or a half-rendered template with &lt;code&gt;{{variable}}&lt;/code&gt; still in it.&lt;/p&gt;

&lt;p&gt;Structured output won't catch these, because they fit the schema fine. You need an explicit denylist of error-shaped strings and patterns, checked against every field. It's crude and it works. Maintain it like you maintain a spam filter — every time a new flavor of garbage reaches production, it earns a line in the rejection list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Voice and constraint checks
&lt;/h2&gt;

&lt;p&gt;This is where you encode the things that make output &lt;em&gt;yours&lt;/em&gt; rather than generic. Most of it is deterministic and cheap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Length bounds.&lt;/strong&gt; A word or token range per field. Reject the 900-word answer and the one-liner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Banned phrases.&lt;/strong&gt; The motivational-closer clichés, the "delve," the emoji clusters, the corporate hedging. A regex pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required language.&lt;/strong&gt; If you build bilingual TR/EN tooling like I do, you check that a Turkish response is actually in Turkish — a quick script-ratio or language-ID check catches the model code-switching mid-paragraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format invariants.&lt;/strong&gt; Markdown headings present, no leaked system-prompt fragments, no placeholder tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the core of a harness that strings these layers together with a bounded retry loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="n"&gt;ERROR_SHAPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;as an ai language model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;i (?:cannot|can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t|am unable to) (?:access|comply)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\berror:\s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;undefined|null\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\{\{.*?\}\}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# leaked template tokens
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;BANNED_PHRASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in today&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s fast-paced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delve into&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unleash the power&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Deterministic checks. Returns a list of failures (empty == pass).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;fails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;length out of bounds: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ERROR_SHAPES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error-shaped string: /&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BANNED_PHRASES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;fails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banned phrase: /&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fails&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;last_fails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;feedback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last_fails&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Your previous output was rejected for: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_fails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;. Fix these and return only the schema.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# native structured mode
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last_fails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;last_fails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last_fails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slop after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_fails&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what the harness does on rejection: it feeds the &lt;em&gt;specific failures&lt;/em&gt; back into the next attempt. The model is far better at fixing a named defect than at avoiding an abstract one. And notice the loop is bounded — after &lt;code&gt;max_attempts&lt;/code&gt; it raises rather than shipping. Failing closed is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Deterministic quality gates
&lt;/h2&gt;

&lt;p&gt;Layers 1–3 catch format and surface defects. Layer 4 catches &lt;em&gt;semantic&lt;/em&gt; invariants that are specific to your task and still checkable in code. If you generate a summary, assert every cited number appears in the source. If you generate SQL, run it through a parser and an &lt;code&gt;EXPLAIN&lt;/code&gt;, not the model's confidence. If you generate code, compile it and run the linter. If you generate a translation, check that named entities survived.&lt;/p&gt;

&lt;p&gt;These gates are where domain knowledge lives. They're unglamorous &lt;code&gt;assert&lt;/code&gt; statements, and they're the difference between a demo and a product. The rule: anything you can verify mechanically, you must — because the model will eventually get it wrong, and you want the gate to catch it, not the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: Verify before ship
&lt;/h2&gt;

&lt;p&gt;The last layer is the only one that may use another model, and only for the things code genuinely can't judge: faithfulness, relevance, tone-match. A cheap judge model scoring "does this answer the question, grounded in the provided context, in the requested voice?" on a 1–5 scale, with a hard threshold below which you reject, catches the subtle slop that passes every deterministic check.&lt;/p&gt;

&lt;p&gt;Keep this layer last and keep it skeptical. A judge model is itself an LLM and can be fooled, so it's a final filter on output that has already survived four deterministic gates — never a replacement for them. And log every rejection at every layer. Your rejection logs are the highest-signal dataset you own: they tell you exactly how your model fails in production, which feeds back into prompts, denylists, and gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys you
&lt;/h2&gt;

&lt;p&gt;None of these layers is clever. That's the point. Cleverness is fragile; a denylist and a bounded retry loop are not. What the harness gives you is a &lt;em&gt;guarantee about what reaches the user&lt;/em&gt; — not a probability, a guarantee — for every failure mode you've chosen to encode. Slop stops being a vibe you argue about and becomes a set of named, logged, falsifiable conditions.&lt;/p&gt;

&lt;p&gt;The model is a brilliant, unreliable intern. You don't fix an unreliable intern by writing a longer brief. You fix it by reviewing the work before it goes out.&lt;/p&gt;

&lt;p&gt;The open question I keep circling: which of these checks genuinely belong in deterministic code, and which are you quietly outsourcing to a judge model because writing the real assertion was too hard?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work #3 — Redis: "99% Cache Hit Ratio, System Down"</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Sat, 30 May 2026 21:47:06 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-3-redis-99-cache-hit-ratio-system-down-3lh2</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-3-redis-99-cache-hit-ratio-system-down-3lh2</guid>
      <description>&lt;p&gt;&lt;em&gt;A survival guide for when everything goes wrong in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your Redis dashboard looks perfect. Hit ratio: 99.2%. Latency: sub-millisecond. Memory usage: 60% of available. Every metric says healthy.&lt;/p&gt;

&lt;p&gt;Then at 2:47 PM, your API starts returning 500s. Response times spike to 30 seconds. Users can't log in. The dashboard still shows 99% hit ratio because the cache is working — it's serving cached errors to everyone equally fast.&lt;/p&gt;

&lt;p&gt;Redis is doing exactly what you told it to do. The problem is what you told it to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Single-Threaded Is Fast (Until It Isn't)
&lt;/h2&gt;

&lt;p&gt;Redis processes commands on a single thread. No locks. No context switching. No synchronization overhead. One CPU core, fully utilized, can handle 100K+ operations per second because it never waits for another thread to release a lock.&lt;/p&gt;

&lt;p&gt;The event loop model (similar to Node.js) multiplexes thousands of client connections on a single thread using non-blocking I/O. Read a request, process it, write the response, move to the next. When your commands are simple — GET, SET, INCR — each one takes microseconds.&lt;/p&gt;

&lt;p&gt;The trap: &lt;strong&gt;slow commands block everything.&lt;/strong&gt; &lt;code&gt;KEYS *&lt;/code&gt; on a million-key database? That's a full keyspace scan on the main thread. While it runs, every other client waits. &lt;code&gt;SORT&lt;/code&gt; on a large set? Same. &lt;code&gt;LRANGE&lt;/code&gt; on a list with 10 million elements? Same.&lt;/p&gt;

&lt;p&gt;Redis 6.0 introduced I/O threading (&lt;code&gt;io-threads&lt;/code&gt; config) for reading and writing network data on multiple threads, but command execution is still single-threaded. Redis 7.0 improved this further, but the fundamental model hasn't changed. Long-running commands on the main thread stall everything.&lt;/p&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never use &lt;code&gt;KEYS&lt;/code&gt; in production. Use &lt;code&gt;SCAN&lt;/code&gt; instead — it's cursor-based and returns results incrementally.&lt;/li&gt;
&lt;li&gt;Watch out for O(N) commands on large data structures: &lt;code&gt;LRANGE&lt;/code&gt;, &lt;code&gt;SMEMBERS&lt;/code&gt;, &lt;code&gt;HGETALL&lt;/code&gt; on million-element structures.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;SLOWLOG&lt;/code&gt; to find commands that are blocking the event loop.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pipelining: The Easiest 10x You'll Ever Get
&lt;/h2&gt;

&lt;p&gt;Every Redis command involves a network round trip: send request, wait for response. If you're executing 100 commands sequentially, that's 100 round trips. At 0.5ms per round trip, you're waiting 50ms for what should take 1ms of actual processing.&lt;/p&gt;

&lt;p&gt;Pipelining batches commands into a single network write and reads all responses at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of 100 round trips, you make 1. The server processes all commands in sequence (it's single-threaded, remember) and buffers the responses. Your client sends the batch, waits once, and gets everything back.&lt;/p&gt;

&lt;p&gt;Pipelining doesn't reduce server-side processing time — each command still runs individually. It eliminates network latency, which is almost always the dominant cost for simple commands.&lt;/p&gt;

&lt;p&gt;The catch: if one command in the pipeline fails, the others still execute. Pipelining is not transactional. If you need atomicity, use MULTI/EXEC or Lua scripts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lua Scripting: Atomic Operations Without the Complexity
&lt;/h2&gt;

&lt;p&gt;Redis evaluates Lua scripts atomically. While a script runs, nothing else executes. This makes Lua scripts the right tool for read-modify-write operations that would otherwise need distributed locking.&lt;/p&gt;

&lt;p&gt;Classic example — rate limiting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- KEYS[1] = rate limit key&lt;/span&gt;
&lt;span class="c1"&gt;-- ARGV[1] = max requests&lt;/span&gt;
&lt;span class="c1"&gt;-- ARGV[2] = window in seconds&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INCR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EXPIRE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;-- rate limited&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;-- allowed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This increments a counter and sets expiry atomically. No race condition between INCR and EXPIRE. No chance of two requests both reading "0" and both thinking they're first.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;EVALSHA&lt;/code&gt; instead of &lt;code&gt;EVAL&lt;/code&gt; in production. &lt;code&gt;EVALSHA&lt;/code&gt; references the script by its SHA1 hash, avoiding sending the full script text with every call. Load the script once with &lt;code&gt;SCRIPT LOAD&lt;/code&gt;, then call it by hash.&lt;/p&gt;

&lt;p&gt;Caveat: Lua scripts block the main thread for their entire duration. Keep them short. A script that queries 10 keys is fine. A script that iterates over 100,000 keys is a production incident waiting to happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pub/Sub vs Streams: Two Very Different Tools
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pub/Sub&lt;/strong&gt; is fire-and-forget. Publisher sends a message, all connected subscribers receive it instantly. If a subscriber disconnects and reconnects, it misses everything published while it was gone. No message persistence. No consumer groups. No acknowledgment.&lt;/p&gt;

&lt;p&gt;Use Pub/Sub for: real-time notifications where missing a message is acceptable. Chat typing indicators. Cache invalidation signals. Dashboard live updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streams&lt;/strong&gt; (introduced in Redis 5.0) are persistent, append-only logs with consumer groups. Think of them as "Kafka Lite inside Redis."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;XADD&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;
&lt;span class="n"&gt;XREADGROUP&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="n"&gt;payment_processors&lt;/span&gt; &lt;span class="n"&gt;consumer_1&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="n"&gt;STREAMS&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;XACK&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;payment_processors&lt;/span&gt; &lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Streams persist messages. Consumer groups track which consumer has read what. Unacknowledged messages can be claimed by other consumers if one dies. You get at-least-once delivery semantics.&lt;/p&gt;

&lt;p&gt;Use Streams for: job queues, event sourcing, lightweight message processing where you don't want to deploy Kafka but need more than Pub/Sub.&lt;/p&gt;

&lt;p&gt;Don't use Streams to replace Kafka at scale. Redis Streams are bounded by single-node memory. Kafka is designed for multi-broker distributed throughput. Different tools, different scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory Eviction: The Policy That Saves or Kills You
&lt;/h2&gt;

&lt;p&gt;When Redis hits &lt;code&gt;maxmemory&lt;/code&gt;, it needs to decide what to delete. The eviction policy determines what goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;noeviction&lt;/code&gt;&lt;/strong&gt;: Redis returns errors for write commands. Reads still work. Use this when you absolutely cannot lose data and you'd rather fail loudly than silently corrupt your cache. Common for session stores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;allkeys-lru&lt;/code&gt;&lt;/strong&gt;: Evicts the least recently used key across all keys. The safest general-purpose policy. If you're using Redis purely as a cache, this is your default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;volatile-lru&lt;/code&gt;&lt;/strong&gt;: Only evicts keys with a TTL set. Keys without TTL are never evicted. Use this when you have a mix of permanent data (config, feature flags) and cache data (user sessions, query results). The permanent data stays; the cache data gets evicted under pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;allkeys-lfu&lt;/code&gt;&lt;/strong&gt; (Least Frequently Used): Evicts keys accessed least often, regardless of recency. Better than LRU when you have a mix of frequently-accessed hot data and occasionally-accessed warm data. A key accessed 1,000 times yesterday but not today won't be evicted as quickly as with LRU.&lt;/p&gt;

&lt;p&gt;The disaster scenario: &lt;strong&gt;&lt;code&gt;noeviction&lt;/code&gt; on a cache&lt;/strong&gt;. Redis fills up. Every write fails. Your application treats the write failure as a cache miss and hits the database directly. Now your database is handling the full load that Redis was supposed to absorb. The database slows down. API latency spikes. Cascading failure.&lt;/p&gt;

&lt;p&gt;Monitor &lt;code&gt;evicted_keys&lt;/code&gt; in Redis INFO stats. A sudden spike means you're running out of memory and eviction is kicking in aggressively. Either add memory or investigate why your keyspace is growing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Persistence: RDB vs AOF vs "I Thought Redis Was Just a Cache"
&lt;/h2&gt;

&lt;p&gt;Many teams deploy Redis without persistence, treating it as a pure cache. Then the server restarts and 6 hours of cached data vanishes. Cold cache stampede: every request hits the database simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDB&lt;/strong&gt; (snapshotting): Redis forks the process and writes the entire dataset to disk at intervals. Fast restores. Compact files. But you can lose data between snapshots — if Redis saves every 5 minutes and crashes 4 minutes after the last save, those 4 minutes are gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AOF&lt;/strong&gt; (Append Only File): Redis logs every write operation. Three sync modes: &lt;code&gt;always&lt;/code&gt; (fsync every write — safe but slow), &lt;code&gt;everysec&lt;/code&gt; (fsync every second — good balance), &lt;code&gt;no&lt;/code&gt; (let the OS decide — fastest but risky). On restart, Redis replays the log to rebuild state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDB + AOF&lt;/strong&gt;: Use both. RDB for fast restores and backups. AOF for durability. On restart, Redis prefers AOF because it's more complete.&lt;/p&gt;

&lt;p&gt;The real question: &lt;strong&gt;what happens to your system when Redis restarts with an empty cache?&lt;/strong&gt; If the answer is "everything melts," you need persistence. If the answer is "things are slow for a few minutes while the cache warms up," maybe you don't — but you should still have RDB snapshots for disaster recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thundering Herd: Cache Invalidation's True Face
&lt;/h2&gt;

&lt;p&gt;You cache a popular product page for 5 minutes. 10,000 users are viewing it. The TTL expires. All 10,000 requests simultaneously hit the database for the same data. The database buckles under the sudden spike.&lt;/p&gt;

&lt;p&gt;This is the thundering herd problem, and it's not theoretical. Any high-traffic system with TTL-based caching will encounter it.&lt;/p&gt;

&lt;p&gt;Solutions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staggered TTLs.&lt;/strong&gt; Add random jitter to expiration times: &lt;code&gt;TTL = base_ttl + random(0, 60)&lt;/code&gt;. Keys expire at different times, spreading the database load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-based refresh.&lt;/strong&gt; When a key expires, only one request acquires a lock and rebuilds the cache. All others wait or serve stale data. Implementation with Lua:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;':lock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'locked'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'NX'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EX'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;  &lt;span class="c1"&gt;-- caller rebuilds cache&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;-- wait for rebuild&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Early refresh.&lt;/strong&gt; Refresh the cache before it expires. If TTL is 5 minutes, start a background refresh at 4 minutes. The cache never actually expires under normal operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  How We Crashed Production 3 Times
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Crash #1: Hot key.&lt;/strong&gt; A flash sale product page was cached under a single key. 500,000 requests per second hit that one key. Redis can handle the throughput, but the single-threaded nature means this one key's reads were queuing behind each other. Latency spiked to 50ms — fine for one request, fatal for the 499,999 behind it.&lt;/p&gt;

&lt;p&gt;Fix: cache the hot key locally in-process with a short TTL (1-2 seconds). Application memory serves 99% of requests, Redis serves the refresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash #2: Serialization bomb.&lt;/strong&gt; Someone cached a full user object including activity history — 50MB serialized. Every time the app read that key, Redis had to send 50MB over the network. The single thread was blocked for 200ms per read. At 100 concurrent reads, the event loop was saturated.&lt;/p&gt;

&lt;p&gt;Fix: cache only what you need. User profile: 2KB. User activity: separate key, paginated, never cached as a monolith.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash #3: Cache invalidation race.&lt;/strong&gt; Service A updates a user record in the database and deletes the cache key. Service B reads the cache, gets a miss, reads the stale data from a read replica (replication lag), and writes the stale data back to cache. Now the cache has stale data and it won't refresh until the TTL expires.&lt;/p&gt;

&lt;p&gt;Fix: don't write to cache after a miss if the data might be stale. Use read-from-primary for cache rebuilds, or use a TTL short enough that stale data self-corrects quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Redis, When Memcached?
&lt;/h2&gt;

&lt;p&gt;This is a shorter decision than people make it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis when:&lt;/strong&gt; you need data structures beyond key-value (lists, sets, sorted sets, hashes, streams), persistence, pub/sub, Lua scripting, cluster mode, or any feature beyond simple caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memcached when:&lt;/strong&gt; you need a simple, multi-threaded cache with predictable memory allocation and you're caching large blobs (images, rendered HTML). Memcached's multi-threaded architecture handles large-value workloads more efficiently than Redis's single-threaded model.&lt;/p&gt;

&lt;p&gt;In practice: &lt;strong&gt;Redis, almost always.&lt;/strong&gt; The feature set is so much broader that the rare cases where Memcached wins are outweighed by Redis's versatility. The exception is if you're caching very large objects at very high throughput and you're hitting Redis's single-threaded bottleneck. Then Memcached's multi-threaded reads genuinely help.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Redis is fast by default and slow by mistake. The mistakes are predictable: slow commands on the main thread, missing pipelining, wrong eviction policy, no persistence on a critical cache, and hot keys.&lt;/p&gt;

&lt;p&gt;Monitor &lt;code&gt;commandstats&lt;/code&gt; to see which commands are running. Monitor &lt;code&gt;slowlog&lt;/code&gt; to find the ones that are too slow. Monitor &lt;code&gt;evicted_keys&lt;/code&gt; to know when you're running out of memory.&lt;/p&gt;

&lt;p&gt;The 99% hit ratio dashboard doesn't mean your cache is healthy. It means your cache is serving something fast. Whether that something is correct, fresh, and useful — that's a different question.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;What's your worst Redis incident? Hot key? Thundering herd? Wrong eviction policy? The cache invalidation race condition stories are always the best.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>redis</category>
      <category>backend</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work Bonus: SQL vs NoSQL: Which One in 2026?</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Sat, 30 May 2026 21:45:23 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-sql-vs-nosql-which-one-in-2026-3lcp</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-bonus-sql-vs-nosql-which-one-in-2026-3lcp</guid>
      <description>&lt;p&gt;&lt;em&gt;The honest decision framework, not another flame war.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The SQL vs NoSQL debate has been running for 15 years and it still generates more heat than light. Here's the framework that actually helps you decide.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Question
&lt;/h2&gt;

&lt;p&gt;It's not "SQL or NoSQL." It's: &lt;strong&gt;what does your access pattern look like?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your application is mostly reading and writing related data through well-defined queries — orders with line items, users with addresses, products with categories — relational databases are purpose-built for this. JOINs are not expensive when they're indexed. Transactions are not slow when they're scoped correctly. PostgreSQL handles 50 million rows comfortably on a single node.&lt;/p&gt;

&lt;p&gt;If your application is reading and writing self-contained documents with predictable access by a primary key, and you rarely need cross-document queries — user profiles, product catalogs, content management — a document database simplifies your code. No ORM mapping hell. No migration files for adding a field.&lt;/p&gt;

&lt;p&gt;If your application writes massive volumes and reads by partition key with eventual consistency — time-series data, IoT telemetry, activity feeds at scale — wide-column stores like Cassandra were built for this specific workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2026 Reality
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL has eaten NoSQL's lunch in many areas.&lt;/strong&gt; JSONB support means you can store and query unstructured data inside PostgreSQL with GIN indexes. You get the document model flexibility without giving up transactions, JOINs, and a 30-year ecosystem. For 80% of startups and mid-size companies, PostgreSQL is the only database you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB has gotten more relational.&lt;/strong&gt; Multi-document ACID transactions (since 4.0), schema validation, aggregation pipelines that look suspiciously like SQL. It's converging toward what PostgreSQL already does, but with a different starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DynamoDB dominates serverless.&lt;/strong&gt; If you're in AWS and your access pattern is simple key-value with known query patterns, DynamoDB's pricing model (pay-per-request) and operational simplicity are hard to beat. But the moment you need ad-hoc queries or flexible access patterns, you're fighting the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cassandra is for a specific scale problem.&lt;/strong&gt; If you don't need to write millions of rows per second across multiple data centers with tunable consistency, you don't need Cassandra. The operational overhead is significant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Tree
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with PostgreSQL&lt;/strong&gt; unless you have a specific reason not to. Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need flexible schema with primarily key-based access? → Consider MongoDB&lt;/li&gt;
&lt;li&gt;Need massive write throughput with geographic distribution? → Consider Cassandra&lt;/li&gt;
&lt;li&gt;Need serverless, pay-per-request, AWS-native? → Consider DynamoDB&lt;/li&gt;
&lt;li&gt;Need time-series at scale? → Consider TimescaleDB (PostgreSQL extension) or InfluxDB&lt;/li&gt;
&lt;li&gt;Need graph queries (social networks, recommendation engines)? → Consider Neo4j or PostgreSQL's recursive CTEs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst decision is choosing NoSQL because "we might need to scale." Scale is not a database choice. It's an architecture problem. Most applications will never outgrow a single well-configured PostgreSQL instance. And the ones that do will need to re-architect regardless of their database.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One Thing Nobody Tells You
&lt;/h2&gt;

&lt;p&gt;The database you choose determines your debugging story. When something goes wrong with PostgreSQL, you have &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, &lt;code&gt;pg_stat_statements&lt;/code&gt;, 30 years of Stack Overflow answers, and a query planner that tells you exactly what it's doing.&lt;/p&gt;

&lt;p&gt;When something goes wrong with Cassandra, you're reading GC logs and compaction stats. When DynamoDB throttles your reads, the only fix is to provision more capacity or redesign your partition key. When MongoDB's aggregation pipeline is slow, the explain output is a nested JSON document that takes 20 minutes to parse.&lt;/p&gt;

&lt;p&gt;Choose the database whose failure mode you're most equipped to handle. Because it will fail, and your ability to debug it determines your recovery time.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;SQL or NoSQL for your current project — and why? Has anyone actually migrated from one to the other mid-project? How did it go?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>database</category>
      <category>backend</category>
      <category>beginners</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Great Stack to Doesn't Work #2 — Kafka: "Where Did My Messages Go?"</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Sat, 30 May 2026 21:44:14 +0000</pubDate>
      <link>https://dev.to/turacthethinker/great-stack-to-doesnt-work-2-kafka-where-did-my-messages-go-175p</link>
      <guid>https://dev.to/turacthethinker/great-stack-to-doesnt-work-2-kafka-where-did-my-messages-go-175p</guid>
      <description>&lt;p&gt;&lt;em&gt;A survival guide for when everything goes wrong in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a moment every engineer who works with Kafka experiences. You check the producer. Messages are sending. You check the consumer. Nothing. The consumer group shows zero lag because there's nothing to lag behind — as far as the consumer knows, the topic is empty.&lt;/p&gt;

&lt;p&gt;But it's not empty. The messages are there. Somewhere. In some partition, at some offset, behind some configuration you set six months ago and forgot about.&lt;/p&gt;

&lt;p&gt;Kafka doesn't lose messages. But it's very good at hiding them from you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Consumer Lag: The Number Everyone Watches Wrong
&lt;/h2&gt;

&lt;p&gt;Consumer lag is the difference between the latest offset in a partition and the offset your consumer group has committed. Simple concept. Dangerous in practice.&lt;/p&gt;

&lt;p&gt;The mistake: treating lag as a single number. Lag is per-partition. If you have 30 partitions and one consumer is stuck on partition 17 while the others are healthy, the total lag looks manageable. But partition 17's data is hours behind, and whatever downstream system depends on that data is serving stale results.&lt;/p&gt;

&lt;p&gt;Monitor lag per partition. Tools like Burrow, Kafka Exporter for Prometheus, or even &lt;code&gt;kafka-consumer-groups.sh --describe&lt;/code&gt; break it down. If one partition's lag is growing while others are stable, you have a stuck consumer, a hot partition, or a poison message.&lt;/p&gt;

&lt;p&gt;A poison message is a record your consumer can't process — malformed data, unexpected schema, null where it shouldn't be null. The consumer throws an exception, the offset doesn't commit, and it retries the same message forever. Lag grows. The consumer looks "alive" because it's processing — just not making progress.&lt;/p&gt;

&lt;p&gt;The fix: dead letter queues. After N retries, move the message to a separate topic, commit the offset, and move on. Alert on the dead letter topic. Investigate later. Don't let one bad record block millions of good ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rebalance Storms: The Silent Killer
&lt;/h2&gt;

&lt;p&gt;Consumer rebalancing is Kafka's mechanism for redistributing partitions across consumers in a group. When a consumer joins or leaves, Kafka reassigns partitions. During rebalance, all consumers in the group stop processing. For a few seconds, nobody's doing anything.&lt;/p&gt;

&lt;p&gt;This is fine. Unless it happens every 30 seconds.&lt;/p&gt;

&lt;p&gt;Rebalance storms happen when Kafka thinks a consumer is dead, removes it from the group, triggers a rebalance, then the consumer comes back, joins the group, triggers another rebalance, and the cycle repeats.&lt;/p&gt;

&lt;p&gt;Three timeout settings control this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session.timeout.ms&lt;/code&gt;: how long Kafka waits for a heartbeat before declaring the consumer dead. Default: 45 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;heartbeat.interval.ms&lt;/code&gt;: how often the consumer sends heartbeats. Default: 3 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max.poll.interval.ms&lt;/code&gt;: how long between two poll() calls before Kafka kicks the consumer out. Default: 5 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common cause of rebalance storms: &lt;code&gt;max.poll.interval.ms&lt;/code&gt; is too short for your processing time. Your consumer polls 500 records, spends 6 minutes processing them, and by the time it polls again, Kafka has already declared it dead and rebalanced.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Increase &lt;code&gt;max.poll.interval.ms&lt;/code&gt; to match your worst-case processing time.&lt;/li&gt;
&lt;li&gt;Decrease &lt;code&gt;max.poll.records&lt;/code&gt; so each batch processes faster.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;static.group.instance.id&lt;/code&gt; — this enables static membership, which means Kafka won't immediately rebalance when a consumer temporarily disconnects. It waits for &lt;code&gt;session.timeout.ms&lt;/code&gt; to expire first.&lt;/li&gt;
&lt;li&gt;Use cooperative rebalancing (&lt;code&gt;partition.assignment.strategy = CooperativeStickyAssignor&lt;/code&gt;) — instead of stopping all consumers during rebalance, it only reassigns the affected partitions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One team I worked with had a 12-consumer group processing payment events. Every few minutes, all processing stopped for 10-15 seconds during rebalance. Twelve times an hour. That's 2 minutes of downtime every hour in a payment pipeline. The fix was adding static group instance IDs and switching to cooperative rebalancing. Total rebalance disruption dropped from 2 minutes per hour to near zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exactly-Once: The Myth and the Reality
&lt;/h2&gt;

&lt;p&gt;Kafka advertises exactly-once semantics. Here's what that actually means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent producer&lt;/strong&gt; (&lt;code&gt;enable.idempotence = true&lt;/code&gt;): Kafka deduplicates messages from the same producer session. If a network retry causes the producer to send the same message twice, the broker detects the duplicate and discards it. This prevents duplicates &lt;em&gt;within a single producer session&lt;/em&gt;. If the producer restarts, it gets a new session, and deduplication doesn't cross sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactional producer + consumer&lt;/strong&gt;: For true exactly-once across produce-and-consume workflows, you need transactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beginTransaction&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputTopic&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processedRecord&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendOffsetsToTransaction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumerOffsets&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumerGroupId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;commitTransaction&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This atomically writes the output record AND commits the consumer offset. Either both happen or neither does. If the transaction fails, the consumer re-reads the input, reprocesses it, and tries again.&lt;/p&gt;

&lt;p&gt;The reality check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exactly-once works within Kafka. The moment your consumer writes to an external database, you're back to at-least-once unless you implement idempotency on the database side.&lt;/li&gt;
&lt;li&gt;Transactions add latency. Each transaction involves coordination between the producer, the transaction coordinator, and the brokers hosting the output partitions.&lt;/li&gt;
&lt;li&gt;Most systems don't need exactly-once. If your consumer can handle duplicates (idempotent writes, upserts, deduplication at the application layer), at-least-once is simpler and faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't reach for exactly-once because it sounds correct. Reach for it when duplicate processing would cause real damage — financial transactions, inventory counts, billing events. For analytics, logging, and notifications, at-least-once with deduplication is the pragmatic choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Partition Strategies: The Decision That Haunts You
&lt;/h2&gt;

&lt;p&gt;Once you choose a partition key, changing it later means reprocessing everything. Choose carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key-based partitioning&lt;/strong&gt; (default when you set a key): all messages with the same key go to the same partition. This guarantees ordering per key. If you're processing events per user, partition by user ID and every event for a given user arrives in order.&lt;/p&gt;

&lt;p&gt;The trap: hot partitions. If one user generates 1,000x more events than average, their partition becomes the bottleneck. The consumer assigned to that partition falls behind while others are idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round-robin&lt;/strong&gt; (no key): messages distribute evenly across partitions. Maximum throughput, zero ordering guarantees. Use this for stateless processing where order doesn't matter — log aggregation, metrics collection, fan-out work queues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom partitioner&lt;/strong&gt;: when you need ordering within a logical group but want to control distribution. For example, partition by &lt;code&gt;tenant_id % num_partitions&lt;/code&gt; to ensure per-tenant ordering while distributing large tenants across multiple partitions.&lt;/p&gt;

&lt;p&gt;The question to ask: &lt;strong&gt;does your consumer need to see related messages in order?&lt;/strong&gt; If yes, use a key that groups related messages. If no, use round-robin for maximum throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compaction: Powerful and Dangerous
&lt;/h2&gt;

&lt;p&gt;Log compaction keeps only the latest value for each key. Instead of retaining messages by time or size, Kafka retains the last message per key indefinitely.&lt;/p&gt;

&lt;p&gt;Use case: a topic that represents current state. User profile updates: you only care about the latest profile, not the history. Config changes: you want the current config, not every version.&lt;/p&gt;

&lt;p&gt;The danger: if your producer accidentally sends a message with a null value (a tombstone), compaction deletes the key permanently. One bug in a producer can wipe state for thousands of keys, and because compaction runs in the background, you might not notice until downstream consumers can't find the data they expect.&lt;/p&gt;

&lt;p&gt;Guard rails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor tombstone rate. A sudden spike in null-value messages is a red flag.&lt;/li&gt;
&lt;li&gt;Separate compacted topics from retention-based topics. Don't compact your event stream.&lt;/li&gt;
&lt;li&gt;Test your producer's null-handling thoroughly. A missing field serialized as null can become a tombstone.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The 4 Problems Disguised as "My Messages Are Disappearing"
&lt;/h2&gt;

&lt;p&gt;When someone says their Kafka messages are disappearing, it's almost never data loss. Here's what's actually happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Retention expired.&lt;/strong&gt; The topic's &lt;code&gt;retention.ms&lt;/code&gt; is set to 7 days. Your consumer was down for 8 days. The messages were deleted before the consumer came back. This is not a bug. It's configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Consumer offset reset.&lt;/strong&gt; Your consumer group's committed offsets expired (controlled by &lt;code&gt;offsets.retention.minutes&lt;/code&gt;, default 7 days in older Kafka versions). When the consumer restarts, it doesn't know where it left off and uses &lt;code&gt;auto.offset.reset&lt;/code&gt; — which defaults to &lt;code&gt;latest&lt;/code&gt;, meaning it skips everything produced while it was offline. Set it to &lt;code&gt;earliest&lt;/code&gt; if you want to reprocess, or better yet, don't let your consumer stay down longer than your offset retention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Wrong topic or partition.&lt;/strong&gt; The producer is writing to &lt;code&gt;orders-v2&lt;/code&gt; and the consumer is reading from &lt;code&gt;orders&lt;/code&gt;. Or the producer changed its partition key, so messages that used to go to partition 5 now go to partition 12, and the consumer assigned to partition 5 sees nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Serialization mismatch.&lt;/strong&gt; The producer is writing Avro, the consumer expects JSON. The consumer "reads" the message but can't deserialize it, throws an exception, and depending on error handling, either crashes or silently skips it. The message is there — the consumer just can't understand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 2M Messages Per Second Story
&lt;/h2&gt;

&lt;p&gt;Payment processing platform. 6 brokers, 3 racks, 120 partitions across 4 topics. Target: sustain 2 million messages per second with P99 produce latency under 10ms.&lt;/p&gt;

&lt;p&gt;Initial state: 800K messages per second, P99 at 45ms. Rebalances every few minutes. Consumer lag growing during peak hours.&lt;/p&gt;

&lt;p&gt;What we changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increased &lt;code&gt;num.io.threads&lt;/code&gt; and &lt;code&gt;num.network.threads&lt;/code&gt; to match the core count. Default values are conservative.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;log.flush.interval.messages&lt;/code&gt; to a higher value. Letting the OS page cache handle flushing is almost always faster than forcing Kafka to fsync.&lt;/li&gt;
&lt;li&gt;Moved log directories to separate NVMe drives per mount point. Kafka is I/O bound. Spreading partitions across drives parallelizes writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Producer side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch size from 16KB to 256KB. Larger batches mean fewer network round trips.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;linger.ms&lt;/code&gt; from 0 to 5. Instead of sending immediately, the producer waits 5ms to fill the batch. Throughput jumps significantly for a tiny latency increase.&lt;/li&gt;
&lt;li&gt;Compression: &lt;code&gt;lz4&lt;/code&gt;. Reduces network bandwidth and disk usage. lz4 is fast enough that compression time is negligible compared to network savings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Consumer side:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fetch.min.bytes&lt;/code&gt; from 1 to 64KB. Don't make a network round trip for a single message.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max.poll.records&lt;/code&gt; tuned to match processing capacity. Too high means long processing between polls and rebalance risk. Too low means excessive poll overhead.&lt;/li&gt;
&lt;li&gt;Static group instance IDs + cooperative rebalancing. Eliminated rebalance storms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Partition count:&lt;/strong&gt; Increased from 120 to 360. More partitions = more parallelism, up to the point where metadata overhead becomes a problem (usually in the thousands).&lt;/p&gt;

&lt;p&gt;Result: sustained 2.1M messages per second, P99 produce latency at 7ms, zero rebalances during the 4-hour peak window. The infrastructure didn't change. Six brokers, same hardware. Just configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Kafka is a log. Everything flows from that mental model. Producers append to a log. Consumers read from a log at their own pace. Offsets are just positions in that log.&lt;/p&gt;

&lt;p&gt;When "messages disappear," trace the offset. Where did the consumer last commit? What's the earliest available offset in the partition? The gap between those two numbers tells you everything.&lt;/p&gt;

&lt;p&gt;Rebalances are the biggest operational pain in Kafka. Static membership and cooperative rebalancing aren't just nice-to-haves — they're the difference between a stable pipeline and one that hiccups every few minutes.&lt;/p&gt;

&lt;p&gt;And if someone tells you they need exactly-once semantics, ask them what happens if their consumer processes a message twice. If the answer is "nothing, we have upserts," they don't need exactly-once. They need at-least-once with idempotency. Which is simpler, faster, and what most production systems actually run.&lt;/p&gt;







&lt;h2&gt;
  
  
  Over to You
&lt;/h2&gt;

&lt;p&gt;Have you ever lost messages in Kafka — or thought you did? What was the actual root cause? I'd love to hear your rebalance storm stories.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you enjoyed this, I write about production engineering, AI systems, and the messy reality of building software at scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mehmetturac/" rel="noopener noreferrer"&gt;LinkedIn — Mehmet TURAÇ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/TuracTheThinker" rel="noopener noreferrer"&gt;X/Twitter — @TuracTheThinker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of the **Great Stack to Doesn't Work&lt;/em&gt;* series — a survival guide for when everything goes wrong in production. Follow the series to catch every episode.*&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>backend</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Harness Engineering: The Code Around the Model Is the Hard Part</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Sat, 30 May 2026 07:09:57 +0000</pubDate>
      <link>https://dev.to/turacthethinker/harness-engineering-the-code-around-the-model-is-the-hard-part-2edj</link>
      <guid>https://dev.to/turacthethinker/harness-engineering-the-code-around-the-model-is-the-hard-part-2edj</guid>
      <description>&lt;p&gt;Everyone benchmarks the model. Almost nobody benchmarks the harness — the loop, the tool dispatch, the context manager, the retry logic that wraps a raw inference call and turns it into something that can run unattended against production. In my experience building agentic platforms, swapping the model is a config change you ship in an afternoon. The harness is where the months go, and it's where reliability is actually won or lost.&lt;/p&gt;

&lt;p&gt;This is the part that doesn't show up in demos. A demo agent calls a tool, gets a clean result, and prints a tidy answer. A production agent calls a tool that times out, gets a 200 with a malformed body, hits a rate limit on retry, and now has to decide whether to keep going or give up — all while staying inside a token budget and not corrupting anything downstream. The model doesn't solve that. The harness does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsd57e5yojfszj3v6rc0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsd57e5yojfszj3v6rc0u.png" alt="Harness Engineering: The Code Around the Model Is the Hard Part" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness is the product
&lt;/h2&gt;

&lt;p&gt;When people say "we built an agent," they usually mean they wrote a prompt and a tool schema. That's the easy 20%. The other 80% is the scaffolding that decides &lt;em&gt;when&lt;/em&gt; to call the model, &lt;em&gt;what&lt;/em&gt; to put in front of it, &lt;em&gt;whether to trust&lt;/em&gt; what comes back, and &lt;em&gt;what to do&lt;/em&gt; when something fails. That scaffolding is the harness, and it's where your engineering judgment lives.&lt;/p&gt;

&lt;p&gt;The useful mental model: the LLM is a single, expensive, non-deterministic function call. Everything that makes that call safe, bounded, observable, and repeatable is your code. Treat the model as a component you don't control and the harness as the system you do, and most architecture decisions get clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a harness
&lt;/h2&gt;

&lt;p&gt;Strip away the framework branding and every agent harness has the same moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;control loop&lt;/strong&gt; that runs steps until the task is done, a stop condition fires, or a budget is exhausted.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;context manager&lt;/strong&gt; that assembles the prompt each step — system instructions, relevant history, tool specs — and decides what to drop when it won't fit.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;model call&lt;/strong&gt; wrapped in its own timeout and retry policy.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;parse-and-validate&lt;/strong&gt; stage that turns model output into a typed, checked action before anything acts on it.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tool dispatcher&lt;/strong&gt; that executes the chosen action with its own timeouts, retries, and idempotency handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt; that gate side effects — allow-lists, argument validation, rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; that records every step as structured data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frameworks give you defaults for these. The defaults are fine for prototypes and quietly wrong for production, because the right policy is domain-specific. How many steps before you bail? What's a retryable tool error versus a fatal one? What do you drop from context first? Nobody can answer those for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool calls are an untrusted boundary
&lt;/h2&gt;

&lt;p&gt;The single most common production failure I see is treating model output as if it were already valid. The model proposes a tool call; the harness executes it verbatim. Then one day the model emits an argument that's subtly out of range, or invents a tool name, or returns JSON with a trailing comment, and the dispatcher happily forwards garbage into a system that does real things.&lt;/p&gt;

&lt;p&gt;A tool call from the model is a &lt;em&gt;proposal&lt;/em&gt;, not an instruction. Validate it like input from an untrusted client, because that's exactly what it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Assemble context within budget — drop oldest observations first
&lt;/span&gt;    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remaining_tokens&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Model call is fallible: its own timeout + bounded retry
&lt;/span&gt;    &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Validate the proposal BEFORE anything acts on it
&lt;/span&gt;    &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Don't crash — feed the error back so the model can recover
&lt;/span&gt;        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: unknown tool &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: invalid args: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Guardrail: side effects must pass policy
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_side_effects&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: action blocked by policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 5. Dispatch with the tool's own failure handling
&lt;/span&gt;    &lt;span class="n"&gt;observation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_observation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;step_no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StepResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what the failure paths do: they don't raise. A bad tool name, invalid arguments, or a blocked action all become &lt;em&gt;observations fed back into context&lt;/em&gt;. The model gets to see its mistake and try again. This single pattern — turning harness-level errors into model-visible feedback — is the difference between an agent that recovers and one that dies on the first imperfect output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context is a budget, not a buffer
&lt;/h2&gt;

&lt;p&gt;The naive harness appends everything to a growing transcript and passes it back every step. This works until it doesn't: you blow the context window, latency climbs with every step, cost grows quadratically over a long task, and the model's attention degrades as the relevant signal drowns in old tool dumps.&lt;/p&gt;

&lt;p&gt;Context is a budget you spend deliberately each step. That means making active decisions: which prior observations still matter, which can be summarized, which can be dropped entirely. A 40KB API response that mattered three steps ago is now dead weight — keep a one-line summary of what it told you and discard the body. The control loop's job isn't to remember everything; it's to keep the &lt;em&gt;useful&lt;/em&gt; state in front of the model and evict the rest. Get this wrong and a task that should take eight steps either runs out of window at step twelve or costs five times what it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for failure, because it's the default
&lt;/h2&gt;

&lt;p&gt;In a system where one step is a network call to a probabilistic model and the next is a network call to a flaky third-party API, failure isn't the exception — it's the steady state. The harness has to assume every external call can time out, return malformed data, or partially succeed.&lt;/p&gt;

&lt;p&gt;The parts that earn their keep here are unglamorous: timeouts on every external call (model included), bounded retries with backoff, idempotency keys on any tool that mutates state so a retry doesn't double-charge or double-send, and a hard step ceiling so a confused agent can't loop forever burning tokens. None of this is novel — it's the same distributed-systems discipline we've applied for two decades. What's new is that one of the unreliable components is now the decision-maker itself, which means a retry can produce a &lt;em&gt;different&lt;/em&gt; decision. Your harness has to be correct under that, not just under transient errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't fix what you can't see
&lt;/h2&gt;

&lt;p&gt;A non-deterministic system you can't replay is a system you can't debug. When an agent does something wrong in production — picks the wrong tool, loops, gives up early — "it worked on my machine" is meaningless, because your machine got a different sample.&lt;/p&gt;

&lt;p&gt;So every step has to emit structured data: the assembled context, the model's decision, the tool called, the arguments, latency, token usage, and outcome. Not log lines you grep — structured spans you can query, aggregate, and replay. With that, "the agent failed" becomes "at step 7 it called the search tool with an empty query because the previous observation got evicted from context," which is an actual bug with an actual fix. Without it, you're tuning prompts by superstition. Token and cost accounting belong in the same trace, because on a long-running agent they're a production concern, not a billing footnote.&lt;/p&gt;

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

&lt;p&gt;The model gets the headlines and the harness gets the pager. As base models keep improving, the differentiator between an agent that demos well and one that survives contact with production won't be which model you picked — it'll be the engineering quality of the code wrapped around it: how it validates, how it budgets context, how it fails, and how observable it is.&lt;/p&gt;

&lt;p&gt;So here's what I keep coming back to: if you swapped your agent's underlying model tomorrow, how much of your reliability would survive the change — and how much was the harness carrying all along?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Runnable, tested example:&lt;/strong&gt; &lt;a href="https://github.com/mturac/harness-demo" rel="noopener noreferrer"&gt;https://github.com/mturac/harness-demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>productionengineering</category>
    </item>
    <item>
      <title>Dynamic Workflows in Opus 4.8: Build a Self-Verifying PR Reviewer</title>
      <dc:creator>Mehmet TURAÇ</dc:creator>
      <pubDate>Fri, 29 May 2026 21:23:49 +0000</pubDate>
      <link>https://dev.to/turacthethinker/dynamic-workflows-in-opus-48-build-a-self-verifying-pr-reviewer-55b1</link>
      <guid>https://dev.to/turacthethinker/dynamic-workflows-in-opus-48-build-a-self-verifying-pr-reviewer-55b1</guid>
      <description>&lt;h2&gt;
  
  
  You stopped being the loop
&lt;/h2&gt;

&lt;p&gt;Most people use Opus 4.8 the way they used every model before it: open a chat, type a request, watch the cursor, correct it, repeat. That's a conversation. A &lt;em&gt;dynamic workflow&lt;/em&gt; is something else entirely.&lt;/p&gt;

&lt;p&gt;The shift is this: you stop being the loop. Instead, an &lt;strong&gt;orchestrator&lt;/strong&gt; — plain code you control — spawns subagents you design, fanning out work in parallel, running steps in sequence, judging and merging results, and reporting back when the whole thing is done. Opus 4.8 can drive hundreds of parallel subagents inside a single workflow, with &lt;strong&gt;effort control&lt;/strong&gt; per node so cheap steps stay cheap and hard steps think harder.&lt;/p&gt;

&lt;p&gt;In this tutorial you'll learn the core patterns by building one concrete thing: a pull-request reviewer that fans out across correctness, security, and performance, then &lt;strong&gt;adversarially verifies&lt;/strong&gt; every finding before it reaches you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You design the shape. The orchestrator runs it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deduped&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupeByFileLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refuted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the end you'll know when to reach for &lt;code&gt;parallel()&lt;/code&gt; versus &lt;code&gt;pipeline()&lt;/code&gt;, how structured output schemas keep subagents composable, and where to set effort per node.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: it's a graph, not a prompt
&lt;/h2&gt;

&lt;p&gt;Stop thinking "I send a prompt, I get a completion." Start thinking: &lt;strong&gt;an orchestrator runs a workflow graph, and each node is an agent call.&lt;/strong&gt; The orchestrator is plain code. It decides what runs, in what order, and what to do with each result. Subagents are the leaf workers — each gets a focused prompt, a structured-output schema, and its own effort setting. The unit of work is no longer the prompt; it's the graph.&lt;/p&gt;

&lt;p&gt;Two primitives compose every graph, and the difference between them is entirely about &lt;em&gt;barriers&lt;/em&gt; — when the orchestrator blocks and waits.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;parallel()&lt;/code&gt; is a barrier
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;parallel()&lt;/code&gt; fans work out to many subagents at once and resolves only when &lt;strong&gt;all&lt;/strong&gt; of them return. Nothing downstream runs until the slowest node finishes. Use it for independent work that must be fully collected before the next decision — one subagent per review dimension, N-way verification, hundreds of concurrent checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FAN-OUT: dimensions are independent → run them together&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// barrier: every dimension has returned before we continue&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deduped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupeByFileLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// plain code, no agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;() =&amp;gt;&lt;/code&gt; thunks. &lt;code&gt;parallel()&lt;/code&gt; invokes them itself — it schedules the work; it doesn't receive already-started promises.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;pipeline()&lt;/code&gt; enforces order
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pipeline()&lt;/code&gt; chains stages where stage &lt;em&gt;N+1&lt;/em&gt; depends on stage &lt;em&gt;N&lt;/em&gt;'s output. Each stage blocks until its input exists, so the stages run strictly in sequence and the latencies add up. Reach for it when there's a true data dependency — you can't synthesize a review before findings exist, and you can't verify findings before they're deduplicated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}))),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dedupeByFileLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt; &lt;span class="p"&gt;}))),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;dedupeByFileLine&lt;/code&gt; is not an agent — deterministic work stays in code. You only spend a subagent where judgment is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The whole grammar:&lt;/strong&gt; &lt;code&gt;parallel&lt;/code&gt; for independence, &lt;code&gt;pipeline&lt;/code&gt; for dependency. Real workflows alternate between the two, fanning out for breadth and chaining where order matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured outputs: typed, not parsed
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;agent()&lt;/code&gt; call above passes a &lt;code&gt;schema&lt;/code&gt;. The model returns data shaped to that contract — &lt;code&gt;FINDINGS&lt;/code&gt;, &lt;code&gt;VERDICT&lt;/code&gt;, &lt;code&gt;REVIEW&lt;/code&gt; — so you index fields instead of regexing prose. This is what lets the dedup and filter steps be &lt;em&gt;plain code&lt;/em&gt; rather than yet another LLM call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refuted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schemas are the seams that keep subagents composable. A node's output is machine-readable, so the next node — agent or code — consumes it without a parsing layer in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worked example: a self-verifying PR reviewer
&lt;/h2&gt;

&lt;p&gt;Most "AI code review" is one model, one prompt, one pass. It finds plausible bugs and reports them with equal confidence — including the ones that aren't real. Dynamic workflows let you do better: fan out across review dimensions in parallel, then make the model &lt;em&gt;attack its own findings&lt;/em&gt; before reporting them. Here's the full pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Fan out across dimensions
&lt;/h3&gt;

&lt;p&gt;Run one subagent per review dimension. They don't depend on each other, so they execute concurrently behind a barrier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;correctness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;correctnessPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;securityPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;performance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;perfPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;agent()&lt;/code&gt; call is an isolated subagent with its own context window — the security reviewer never sees the performance reviewer's noise. &lt;code&gt;{ schema: FINDINGS }&lt;/code&gt; forces a structured output: an array of &lt;code&gt;{ file, line, severity, claim }&lt;/code&gt;, not prose you have to regex later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Dedup (plain code, not an agent)
&lt;/h3&gt;

&lt;p&gt;Three reviewers will flag the same line. Merging is deterministic set logic — don't spend a model on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deduped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dedupeByFileLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flatMap&lt;/code&gt; flattens the per-dimension arrays into one list; &lt;code&gt;dedupeByFileLine&lt;/code&gt; collapses entries sharing a &lt;code&gt;(file, line)&lt;/code&gt; key. Use code wherever the answer is mechanical. Agents are for judgment, not joins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Adversarially verify
&lt;/h3&gt;

&lt;p&gt;This is the step that kills false positives. For each surviving finding, spawn a skeptic subagent whose only job is to &lt;strong&gt;refute&lt;/strong&gt; it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refuted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;refutePrompt(f)&lt;/code&gt; instructs the subagent: "Here is a claimed bug. Prove it's wrong — find the guard, the caller, the type that makes it safe." &lt;code&gt;VERDICT&lt;/code&gt; is &lt;code&gt;{ refuted: boolean, reason: string }&lt;/code&gt;. A finding that survives a dedicated attacker is worth reporting; one that doesn't, isn't.&lt;/p&gt;

&lt;p&gt;For higher-stakes findings, fan out &lt;em&gt;N&lt;/em&gt; skeptics per finding and keep only what a majority can't refute — verification scales independently of review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;survivesQuorum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verdicts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refutals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verdicts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refuted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refutals&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a majority could not refute it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a judge pattern: refutation is &lt;em&gt;adjudication&lt;/em&gt;, kept separate from the &lt;em&gt;generation&lt;/em&gt; in step 1. Asking a model to merely re-summarize its own findings launders the weak ones into the report. Refutation is a sharper filter than agreement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Synthesize
&lt;/h3&gt;

&lt;p&gt;One agent turns confirmed findings into the review a human reads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;synthesisPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REVIEW&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Wiring it together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}))),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dedupeByFileLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt; &lt;span class="p"&gt;}))),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;synthesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// keep only refuted === false, then write&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pipeline()&lt;/code&gt; is sequential — each stage's output feeds the next. &lt;code&gt;parallel()&lt;/code&gt; is the barrier inside stages 1 and 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Effort control per node
&lt;/h3&gt;

&lt;p&gt;Not every node deserves the same compute. Set effort per call: skeptics run cheap because refutation is a narrow question; synthesis runs at high effort because it's the artifact a human trusts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refutePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VERDICT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;synthesisPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REVIEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You spend reasoning where judgment is hard and conserve it where the work is mechanical — and a human still approves the final review before anything posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls and best practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Match the primitive to the dependency
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;parallel()&lt;/code&gt; returns when the slowest node finishes; &lt;code&gt;pipeline()&lt;/code&gt; runs stages in sequence and accumulates their latency. Mismatching them is the most common cost mistake. Your review dimensions are independent, so fan them out — don't chain them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good: 3 dimensions run concurrently, wall-time ≈ slowest dimension&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;

&lt;span class="c1"&gt;// Bad: same work, ~3x the latency for no reason&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIMENSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FINDINGS&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserve &lt;code&gt;pipeline()&lt;/code&gt; for true data dependencies — verify &lt;em&gt;needs&lt;/em&gt; dedup's output, so that edge stays sequential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dedup before you verify
&lt;/h3&gt;

&lt;p&gt;Verification is the expensive phase: it can spawn N skeptics per finding. If correctness and security both flag &lt;code&gt;auth.js:42&lt;/code&gt;, verifying twice burns budget for nothing. Collapse duplicates first with plain code — no agent required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep a human at the merge
&lt;/h3&gt;

&lt;p&gt;The synthesize step is your human-in-the-loop checkpoint. Confirmed findings are a recommendation, not an auto-commit — a person approves before anything lands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amplify signal, not noise
&lt;/h3&gt;

&lt;p&gt;Fan-out multiplies whatever your base node produces, so the base node's reliability matters. Anthropic reports Opus 4.8 makes roughly 4x fewer silent code bugs than its predecessor; the more trustworthy each leaf reviewer is, the safer it is to run many of them in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for a workflow
&lt;/h2&gt;

&lt;p&gt;A single agent is the right default. Reach for a dynamic workflow only when the task has &lt;em&gt;structure you can name&lt;/em&gt;: independent dimensions that fan out in parallel, a verification step that must be adversarial rather than self-graded, or a synthesis pass that depends on confirmed inputs.&lt;/p&gt;

&lt;p&gt;The PR-review example earns its workflow because each stage has a different shape — fan out, collapse in code, fan out again to refute, then synthesize. &lt;code&gt;parallel()&lt;/code&gt; is the barrier; &lt;code&gt;pipeline()&lt;/code&gt; enforces order; schemas keep the seams machine-readable; effort goes high on synthesis and low on the mechanical passes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open question:&lt;/strong&gt; which of your "trust me" agent steps is actually an unverified claim waiting for a skeptic?&lt;/p&gt;

</description>
      <category>opus</category>
      <category>ai</category>
      <category>workflows</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
