Skip to content
KoishiAI
ไทย
← Back to contents
Chapter 12 / 12 · April 24, 2026

Loops, Error Handling and Incidents

Finding the root cause of a loop with 5 Whys, anti-loop patterns, profiling, error taxonomy, retry, circuit breakers, graceful shutdown, and handling a production incident

This guide is All Rights Reserved — free to read, copying/republication requires permission.

Chapter 12 | Loops, Error Handling and Incidents Written for: developers at every level who want a system that recovers when it fails, and does not fail the same way twice

Deep loop debugging — root cause analysis

Chapter 4 taught the protocol for stopping a loop. This chapter goes further: how to analyse the root cause systematically, and how to design code up front so that loops happen less often.

The 5 Whys framework for loop debugging

The “5 Whys” technique from the Toyota Production System works very well for debugging with Claude. You do not have to ask “why” a full five times every session — you ask until you reach the actual root cause.

Example: fixing a real bug with 5 Whys

# Situation: payments fail intermittently
 
> Why 1: why does the payment fail?
  → Stripe API returns the error "card_declined"
 
> Why 2: why is the card declined?
  → Stripe says insufficient_funds, but the user says the balance is fine
 
> Why 3: why does Stripe see insufficient_funds when the balance is fine?
  → Looking at the request payload, the amount sent is 1000
     but it should be 10.00 (different units: baht vs satang)
 
> Why 4: why is the amount sent in the wrong unit?
  → The code in checkout.ts never converts baht → satang
     before sending to Stripe (Stripe uses the smallest unit of the currency)
 
> Why 5: why is the conversion missing?
  → The developer misread the doc and assumed Stripe uses the major unit,
     and there is no unit test verifying the amount
 
# Root cause: missing unit test + unclear documentation
# Real fix: correct the conversion + add a test + add a note in CLAUDE.md

Prompting Claude to run 5 Whys

> Run a 5 Whys analysis for this bug:
  [describe the symptom]
 
  Do it like this:
  1. Why 1: describe the symptom you can see
  2. Why 2-5: keep asking, each one building on the previous answer
  3. Stop when you reach the real root cause (fewer than 5 is fine)
  4. Separate "fix the symptom" from "fix the root cause"
  5. Propose a solution for the root cause
  6. Propose a way to prevent a recurrence (systematic fix)

Debugging playbook — a guide per bug class

Instead of describing the problem in vague terms, identify the class of bug first, then use the matching playbook:

Playbook A: Logic bug — wrong result, no error

The hardest kind to debug, because there is no error message to follow. Claude tends to loop most on this class.

> Logic bug analysis:
  What I see: [input] → [actual output]
  What I want: [input] → [expected output]
 
  Do the following:
  1. Trace the execution path: describe step by step what the code does
     from receiving the input to returning the output
 
  2. Find the divergence point: where do actual and expected start to differ?
 
  3. Add assertion checks along the way:
     console.assert(value === expected, `Step N: got ${value}, want ${expected}`)
 
  4. Run it and see which step the assertion fails at
 
  Do not change any code until you can identify the divergence point
Playbook B: Performance bug — unusually slow
> Performance investigation:
  Symptom: [which part is slow] takes about [X seconds/ms]
  Target: must be faster than [Y seconds/ms]
 
  Follow this order:
 
  Phase 1 — measure before changing anything:
  Add timing in @[file]:
  - console.time("total")
  - console.time("db-query")
  - console.time("data-transform")
  - console.time("response")
  Then run it 3 times and record the average
 
  Phase 2 — identify the bottleneck:
  Which step takes the most time?
 
  Phase 3 — optimise the bottleneck only:
  Do not optimise the parts that are already fast
 
  Phase 4 — measure again:
  Compare before/after; it must hit the [Y ms] target
Playbook C: Memory leak — RAM creeps up until the system slows
> Memory leak investigation:
  Symptom: memory keeps climbing during long runs
 
  Step 1 — confirm there really is a leak:
  Add memory monitoring:
  setInterval(() => {
    const mem = process.memoryUsage();
    console.log(`RSS: ${Math.round(mem.rss/1024/1024)}MB`,
                `Heap: ${Math.round(mem.heapUsed/1024/1024)}MB`);
  }, 5000);
 
  Step 2 — find the pattern:
  Which action makes memory grow?
  Every request? After an upload? After a WebSocket connect?
 
  Step 3 — analyse @[file] for these patterns:
  - Event listeners never removed with removeEventListener
  - setTimeout/setInterval never cleared with clearTimeout/clearInterval
  - Global variables holding an array/object forever
  - A cache with no eviction policy
  - Closures capturing a large object
 
  Step 4 — fix it, then verify memory is flat after 30 minutes of running
Playbook D: Concurrency bug — only wrong under high traffic
> Concurrency analysis:
  Symptom: fine normally, but breaks with concurrent users
 
  Step 1 — reproduce locally:
  Write a load test script that sends concurrent requests:
  for (let i = 0; i < 50; i++) {
    promises.push(fetch("/api/[endpoint]"));
  }
  await Promise.all(promises);
 
  Step 2 — analyse shared state:
  Find variables in @[file] that are:
  - module-level (not request-level)
  - read and written in the same handler
  - without any locking/synchronisation
 
  Step 3 — propose a fix:
  - Database-level locking: SELECT FOR UPDATE
  - Optimistic locking: version field + retry
  - Queue: serialise concurrent operations
  - Idempotency key: prevent duplicate processing
 
  Explain the tradeoffs of each option before choosing

Anti-loop patterns — designing so loops happen less

The best way to avoid loops is to design the code and the prompt well from the start:

Pattern 1: Contract-first development

Define the interface and the contract clearly before implementing, so Claude has a precise specification and does not have to guess:

# Instead of saying "build a payment service"
# write the contract first, then have Claude implement it
 
> This is the contract I want:
 
  // Input
  interface ProcessPaymentInput {
    userId: string;
    amount: number;        // unit: baht (e.g. 99.50)
    currency: "THB" | "USD";
    description: string;
    idempotencyKey: string; // prevents duplicate charges
  }
 
  // Success Output
  interface ProcessPaymentSuccess {
    success: true;
    transactionId: string;
    amount: number;         // confirms the amount actually charged
    timestamp: Date;
  }
 
  // Error Output
  interface ProcessPaymentError {
    success: false;
    errorCode: "CARD_DECLINED" | "INSUFFICIENT_FUNDS" |
               "INVALID_CARD" | "PROCESSING_ERROR";
    message: string;       // for logs (not shown to the user)
    userMessage: string;   // for display to the user (in Thai)
    retryable: boolean;    // tells the client whether a retry is possible
  }
 
  implement processPayment(input: ProcessPaymentInput)
  : Promise<ProcessPaymentSuccess | ProcessPaymentError>
  Use Stripe as the payment processor
  Convert amount from baht → satang before sending to Stripe

Pattern 2: Error budget strategy

Decide in advance which classes of error Claude may retry on its own, and which ones require it to stop and ask:

# Add to CLAUDE.md
## 🔄 Error Handling Strategy
 
### Claude may retry on its own (no need to ask):
- TypeScript type error → correct the type
- Wrong import path → fix the path
- Missing await → add await
- Unused variable → remove it
 
### Claude must ask before retrying:
- Logic error (result does not match expectations) → explain your understanding first
- Database schema mismatch → show the schema you see, then ask
- External API error → show the error code + ask about intent
 
### Claude must stop and report:
- Anything security-related → never fix it unilaterally
- Two rounds of fixes with no success → summarise what was tried + ask for more information
- A change needed outside the agreed scope → get approval first

Pattern 3: Checkpoint system

Split the work into small checkpoints and verify each one before moving on. This prevents the snowball effect of accumulating bugs:

> Build a user registration system using checkpoints:
 
  Checkpoint 1: Database schema
  - Create the Prisma schema for User
  - Run the migration
  - ✓ verify: npx prisma db pull and inspect the schema
  STOP → wait for approval before Checkpoint 2
 
  Checkpoint 2: Validation layer
  - Create the Zod schema for the registration input
  - Write unit tests covering valid/invalid cases
  - ✓ verify: npm test must pass in full
  STOP → wait for approval before Checkpoint 3
 
  Checkpoint 3: Service layer
  - Create the registerUser() function
  - Hash the password, check for a duplicate email
  - ✓ verify: integration test against the test database
  STOP → wait for approval before Checkpoint 4
 
  Checkpoint 4: API route
  - Create POST /api/auth/register
  - ✓ verify: curl test for every case
 
  If any checkpoint fails → stop, do not move to the next one

Using Claude to debug Claude

A very powerful technique: have a fresh Claude analyse the problem without knowing which Claude wrote the code.

# Session 1: Claude A builds the code and hits a bug
# Claude A cannot fix it in 2 rounds
 
# Open a new session (Claude B)
> [Fresh session - no prior context]
 
  I was handed this code and it has a bug:
  [paste the whole file]
 
  Symptom: [describe]
  Error: [copy the error]
 
  Analyse this code from the outside:
  1. What is this code trying to do?
  2. Do you see any design issues?
  3. Where is the bug likely to be?
  4. If you were writing this code from scratch,
     what would you change about the approach?
 
# Claude B often sees what Claude A overlooked,
# because it carries no cognitive bias from having written the code

💡 When to use Claude to debug Claude
Use it when: Claude has looped for more than 2 rounds without a fix
Use it when: the code is complex enough to be hard to explain
Use it when: you want a second opinion before merging
Skip it for: easy bugs, or anything fixed on the first try

Production quality — from the first line to going live

Production code does not just mean “it runs”. It means code that is reliable, maintainable, observable and recoverable. This section walks every stage from design to real users.

Stage 1 — Requirements and design

Most work fails at this stage, not at implementation. Always have Claude clarify the requirements first.

# Prompt for clarifying requirements
> Before implementing, I want to clarify the requirements:
 
  Feature wanted: [describe]
 
  Ask me the questions that need answering before implementation:
  1. Happy path: what does the normal flow look like?
  2. Edge cases: which special cases must be handled?
  3. Error cases: what happens if X fails?
  4. Performance: how many concurrent users must it support?
  5. Security: who can access it? What is the data sensitivity level?
  6. Rollback: if this feature breaks, how do we roll it back?
 
  Once clarified, summarise the acceptance criteria
  used to verify that the feature is complete and correct

A design document Claude helps you write

> Create a design document for [Feature] covering:
 
  ## Summary
  Describe the feature in 2-3 sentences
 
  ## Architecture
  - Data flow diagram (text format)
  - Components involved
  - External dependencies
 
  ## API Contract
  - New endpoints (input/output types)
  - Error responses
 
  ## Database Changes
  - Tables/columns added or changed
  - Migration strategy
  - Rollback plan
 
  ## Testing Strategy
  - Unit tests to write
  - Integration tests
  - E2E scenarios
 
  ## Risks & Mitigations
  - Risks you can see
  - How to reduce each one
 
  ## Acceptance Criteria
  ✓ [criterion 1]
  ✓ [criterion 2]

Stage 2 — Implementation with quality gates

Every implementation phase has a quality gate it must pass before moving on. Claude can help verify each gate.

PhaseWorkQuality gateHow Claude helps
Data layerSchema, migrations, indexesMigration runs and rolls backWrites the migration + tests
Business logicCore functions, validationUnit tests pass 100%Writes tests + implements
API layerRoutes, middleware, authIntegration tests passWrites tests + routes
UI layerComponents, forms, statesE2E tests pass on key flowsWrites Playwright tests
PerformanceQuery optimisation, cachingLoad test hits the targetAnalyses the bottleneck
SecurityAuth checks, input validationSecurity checklist passesRuns the OWASP checklist

Stage 3 — A complete testing strategy

Testing is the part vibe coders skip most often, and it is exactly what separates production code from a prototype.

Unit testing with Vitest + Claude
# Have Claude write comprehensive unit tests
> Write unit tests for @src/lib/pricing.ts
  covering ALL cases:
 
  1. Happy paths (valid input → expected output)
  2. Edge cases:
     - boundary numbers: 0, negative, MAX_SAFE_INTEGER
     - null/undefined inputs
     - empty arrays/objects
  3. Error cases:
     - invalid input type
     - business rule violations
  4. Precision cases:
     - floating point (0.1 + 0.2 ≠ 0.3)
     - currency rounding
 
  Use Vitest, organised into describe blocks
  Test names must be readable: "should return X when Y"
 
# Example of good output
describe("calculateDiscount", () => {
  describe("percentage discount", () => {
    it("should apply 10% discount to 1000", () => {
      expect(calculateDiscount(1000, { type: "percent", value: 10 }))
        .toBe(900);
    });
    it("should handle 0% discount", () => {...});
    it("should cap at 100% discount", () => {...});
  });
  describe("fixed discount", () => {...});
  describe("edge cases", () => {...});
});
Integration testing with Supertest
> Write integration tests for POST /api/orders
  using Supertest + a test database
 
  Test cases:
  1. ✅ Success: order created correctly → 201 + order object
  2. ❌ Unauthorized: no token → 401
  3. ❌ Validation: incomplete data → 400 + error details
  4. ❌ Business rule: product out of stock → 422 + user message
  5. ⚡ Idempotency: same request sent twice → only one order created
  6. 🔒 Authorization: user views someone else's order → 403
 
  Setup:
  - Start each test from a clean test database
  - Clear the database after each test
  - Mock the Stripe API (never call the real API in a test)
 
# Example
describe("POST /api/orders", () => {
  beforeEach(async () => {
    await db.cleanDatabase();
    await db.seed.basicProducts();
  });
 
  it("should create order and return 201", async () => {
    const res = await request(app)
      .post("/api/orders")
      .set("Authorization", `Bearer ${testToken}`)
      .send({ items: [{ productId: "p1", quantity: 2 }] });
 
    expect(res.status).toBe(201);
    expect(res.body.id).toBeDefined();
    expect(res.body.total).toBe(200);
  });
});
E2E testing with Playwright
> Write E2E tests for the checkout flow
  using Playwright + the staging environment
 
  Critical flows that must be tested:
  1. Happy path: pick a product → checkout → payment → confirmation
  2. Failed payment: wrong card details → error message → retry
  3. Out of stock: product sells out mid-checkout → error + redirect
  4. Session timeout: idle too long → session expires → redirect to login
 
# Example Playwright test
test("complete checkout flow", async ({ page }) => {
  await page.goto("/products");
 
  // pick a product
  await page.click("[data-testid=product-1]");
  await page.click("[data-testid=add-to-cart]");
 
  // go to checkout
  await page.click("[data-testid=cart-icon]");
  await page.click("[data-testid=checkout-btn]");
 
  // fill in payment
  await page.fill("[data-testid=card-number]", "4242424242424242");
  await page.fill("[data-testid=card-expiry]", "12/25");
  await page.fill("[data-testid=card-cvc]", "123");
  await page.click("[data-testid=pay-btn]");
 
  // verify confirmation
  await expect(page.locator("[data-testid=order-confirmed]"))
    .toBeVisible({ timeout: 10000 });
});

Stage 4 — Security checklist

Security is what vibe coders forget most often. Run this checklist every time before you deploy:

# Have Claude run a security audit
> Run a security audit on the code just written
  Check the relevant items from the OWASP Top 10:
 
  □ Injection:
    - SQL: are all queries parameterised?
    - NoSQL: are MongoDB operators sanitised?
    - Command: does exec() or any shell command take input directly?
 
  □ Authentication:
    - Does every sensitive route have an auth check?
    - Is JWT validation correct (verify signature, expiry)?
    - Is there rate limiting on the login endpoint?
 
  □ Authorization (IDOR):
    - Can a user only view and edit their own data?
    - Is ownership checked before data is returned?
 
  □ Sensitive Data:
    - Are passwords hashed with bcrypt (cost >= 10)?
    - Are sensitive fields (password, token) kept out of responses?
    - Are logs free of PII and credentials?
 
  □ Input Validation:
    - Is every input validated and sanitised?
    - File upload: is type checked, size limited, content scanned?
    - URL parameters: is the format validated?
 
  □ Error Handling:
    - Do error messages avoid leaking internal information?
    - Are stack traces kept from reaching the client?
 
  For each item: ✅ pass / ❌ problem found (explain)

Stage 5 — Performance testing and benchmarking

Before deploying you need to know how much load the code can take, and where the bottleneck sits.

Load testing with k6
# Have Claude write a k6 load test script
> Write a k6 load test for the critical API
 
  Target: /api/products/search
  Expected: p95 latency < 500ms at 100 concurrent users
 
// the k6 script Claude will produce
import http from "k6/http";
import { check, sleep } from "k6";
 
export const options = {
  stages: [
    { duration: "30s", target: 10 },   // warm up
    { duration: "1m",  target: 100 },  // ramp to target
    { duration: "3m",  target: 100 },  // steady state
    { duration: "30s", target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ["p(95)<500"],  // 95% must be < 500ms
    http_req_failed: ["rate<0.01"],    // error rate < 1%
  },
};
 
export default function() {
  const params = {
    headers: { Authorization: `Bearer ${__ENV.TEST_TOKEN}` },
  };
 
  const searchTerms = ["shirt", "pants", "shoes", "hat"];
  const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];
 
  const res = http.get(`${__ENV.BASE_URL}/api/products/search?q=${term}`, params);
 
  check(res, {
    "status 200": (r) => r.status === 200,
    "has products": (r) => JSON.parse(r.body).products.length >= 0,
    "response time OK": (r) => r.timings.duration < 500,
  });
 
  sleep(1);
}
 
# run with: k6 run -e BASE_URL=https://staging.myapp.com \
#          -e TEST_TOKEN=xxx script.js
Database query performance with EXPLAIN ANALYZE
> Analyse the performance of this query:
  [paste the Prisma query]
 
  Produce the raw SQL EXPLAIN ANALYZE version
  so we can read the execution plan:
  1. Seq Scan or Index Scan?
  2. Is there a large gap between estimated and actual rows?
  3. Which step burns the most memory?
 
  Then propose:
  - Indexes worth adding
  - A query rewrite if necessary
  - A caching strategy if appropriate
 
# Example EXPLAIN ANALYZE output for Claude to analyse
EXPLAIN ANALYZE
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL "30 days"
GROUP BY u.id;
 
# Claude will spot the Seq Scan on users and propose:
# CREATE INDEX idx_users_created_at ON users(created_at DESC);

Stage 6 — Observability: logging, metrics, tracing

Good code has to be observable. When something breaks in production you need to find the cause in minutes, not hours.

Structured logging
> Add structured logging in @src/lib/logger.ts
  Requirements:
  - Use pino (10x faster than winston)
  - Log format: JSON (so it can be parsed)
  - Log levels: error, warn, info, debug
  - Every log must carry: timestamp, level, service,
    requestId, userId (when available), message, data
  - Never log: passwords, tokens, credit cards
  - Production: info level and above only
 
# Example of a good logger call
logger.info({
  event: "payment.processed",
  requestId: ctx.requestId,
  userId: user.id,
  orderId: order.id,
  amount: order.total,
  duration: timer.elapsed(),
  // no card number!
}, "Payment processed successfully");
 
> Add request ID middleware that injects requestId
  into every request context so calls can be traced across services
Health check endpoint
> Build a /api/health endpoint for monitoring
  It must check:
  □ Database: does it respond to a ping? Does a simple query run?
  □ Redis (if used): does it respond to a ping?
  □ External APIs: are Stripe and SendGrid reachable?
  □ Disk space: more than 20% free?
  □ Memory: heap usage under 80%?
 
  Response format:
  {
    "status": "healthy" | "degraded" | "unhealthy",
    "timestamp": "2025-01-15T10:00:00Z",
    "version": "1.4.2",
    "checks": {
      "database": { "status": "ok", "latency": 5 },
      "redis":    { "status": "ok", "latency": 1 },
      "stripe":   { "status": "ok", "latency": 120 }
    }
  }
 
  - 200: healthy or degraded (some services slow but still working)
  - 503: unhealthy (a core service has failed)
  - Timeout per check: 2 seconds

Stage 7 — Deployment strategy

A good deployment needs a clear rollback plan and as little downtime as possible.

Zero-downtime deployment checklist
> Build a deployment checklist for [feature]
  with Claude filling in:
 
  ## Pre-deployment
  □ All tests pass (unit, integration, e2e)
  □ Database migration reviewed (backward compatible?)
  □ New environment variables set in production?
  □ Feature flags configured?
  □ Monitoring alerts set up?
 
  ## Migration Strategy
  (important: a migration must stay backward compatible for at least 1 version)
  Step 1: Deploy the migration (add the new column, nullable)
  Step 2: Deploy the app (start using the new column)
  Step 3: Backfill the old data (background job)
  Step 4: Make the column NOT NULL (next release)
 
  ## Deployment Steps
  1. Deploy to staging → smoke test
  2. Deploy to production (canary: 5% of traffic)
  3. Monitor for 15 minutes (error rate, latency)
  4. Expand to 50% → monitor
  5. Full rollout → monitor for 1 hour
 
  ## Rollback Plan
  Trigger: error rate > 1% or p95 > 2s
  Time to rollback: < 5 minutes
  Command: [the actual rollback command]

QA testing — checking before production

QA (quality assurance) is the process of verifying that a feature behaves correctly in every situation. Claude can help you design and run QA systematically.

A QA test plan with Claude

> Build a QA test plan for [Feature Name]
 
  Feature details:
  [describe the feature you built]
 
  Produce a test plan covering:
 
  ## Functional Testing
  - Happy path scenarios (every user journey)
  - Negative scenarios (bad input, insufficient permissions)
  - Boundary testing (min/max values)
 
  ## Browser/Device Compatibility
  - Chrome, Firefox, Safari (latest)
  - Mobile: iOS Safari, Android Chrome
  - Responsive: 320px, 768px, 1280px, 1920px
 
  ## Accessibility Testing
  - Is keyboard navigation complete?
  - Is it screen reader compatible?
  - Colour contrast to WCAG 2.1 AA?
  - Are focus indicators visible?
 
  ## Data Integrity
  - Is the data saved correctly?
  - Does it display correctly after a refresh?
  - Concurrent users: any race condition?
 
  ## Format: Test Case Table
  | ID | Scenario | Steps | Expected | Priority |

An automated QA checklist for vibe coding

Run this checklist every time before merging a PR:

# .claude/commands/qa-check.md
# QA Pre-merge Checklist
 
# Run every automated check and report the results
 
1. Run: npm run typecheck
   ✓ no TypeScript errors allowed
 
2. Run: npm run lint
   ✓ no ESLint errors (warnings are OK)
 
3. Run: npm test
   ✓ must pass 100% (0 failures)
   ✓ coverage must be >= 70% for new code
 
4. Run: npm run build
   ✓ the build must succeed with no errors
 
5. Run the security check:
   npm audit --audit-level=high
   ✓ no high/critical vulnerabilities
 
6. Inspect git diff --stat
   ✓ no .env committed
   ✓ no stray console.log
   ✓ no TODO that should not be in prod
 
7. Manual testing on localhost:
   - The most important happy path
   - The error case most likely to occur
 
Report the result as: ✅ pass / ❌ fail (with details)

Visual regression testing

Checks that the UI has not changed unintentionally after a refactor or a dependency upgrade:

# install Playwright screenshot testing
npm install @playwright/test
 
> Write visual regression tests for the important pages:
  - Homepage
  - Product listing
  - Checkout flow (3 steps)
  - User profile
 
// visual.test.ts
import { test, expect } from "@playwright/test";
 
test("homepage visual", async ({ page }) => {
  await page.goto("/");
  await page.waitForLoadState("networkidle");
  await expect(page).toHaveScreenshot("homepage.png", {
    maxDiffPixels: 100,  // allow up to 100 differing pixels
  });
});
 
# first time: run to create the baseline
npx playwright test visual --update-snapshots
 
# afterwards: compare against the baseline
npx playwright test visual
# if the UI changed → the test fails → approve it or fix it

A/B testing and feature flags

A/B testing means running two versions of a feature side by side with real users to measure which is better. Feature flags control who sees which version.

Feature flags — controlling the release

Feature flags let you deploy the code first and enable it for users later, which lowers the risk of any single deployment.

# Have Claude implement the feature flag system
> Build a feature flag system for the project
  Requirements:
  - Store flags in the database (changeable without a redeploy)
  - Target: all users / named users / a percentage
  - Admin UI: turn a flag on or off immediately
  - Cache: 1 minute (do not hit the DB on every request)
 
  Schema:
  model FeatureFlag {
    id          String   @id @default(cuid())
    key         String   @unique  // "new-checkout"
    enabled     Boolean  @default(false)
    rolloutPct  Int      @default(0)   // 0-100%
    userIds     String[] // specific users
    description String
    createdAt   DateTime @default(now())
    updatedAt   DateTime @updatedAt
  }
 
  Usage in code:
  if (await featureFlag.isEnabled("new-checkout", user.id)) {
    // show new checkout
  } else {
    // show old checkout
  }

An A/B test framework

An A/B test measures which variant produces the better conversion rate.

> Build an A/B test framework that:
  1. Assigns a user to a variant consistently
     (the same user always gets the same variant)
  2. Tracks the events that matter (the conversion goal)
  3. Calculates statistical significance
  4. Shows the results on a dashboard
 
  Example A/B test: CTA button
  Variant A (control): "Add to cart" (blue)
  Variant B (treatment): "Buy now" (red)
  Goal: click rate
  Duration: 2 weeks, or n=1000 per variant
 
// ab-test.ts
export async function getVariant(
  testId: string,
  userId: string
): Promise<"control" | "treatment"> {
  // deterministic hash so the assignment stays consistent
  const hash = crypto
    .createHash("sha256")
    .update(`${testId}:${userId}`)
    .digest("hex");
  const num = parseInt(hash.substring(0, 8), 16);
  return num % 2 === 0 ? "control" : "treatment";
}
 
export async function trackEvent(
  testId: string,
  userId: string,
  event: "impression" | "conversion"
) {
  const variant = await getVariant(testId, userId);
  await db.abTestEvent.create({
    data: { testId, userId, variant, event }
  });
}

Analysing A/B test results with Claude

# after two weeks of collecting data
> Analyse the results of the "cta-button-color" A/B test
  Data:
  Control:   1,024 impressions, 87 conversions (8.5%)
  Treatment: 1,031 impressions, 112 conversions (10.9%)
 
  Please:
  1. Calculate statistical significance (p-value)
     using a Chi-square test or Fisher exact test
  2. Calculate the confidence interval for the difference
  3. Tell me whether the result is significant (p < 0.05)
  4. If significant: what do you recommend doing?
  5. If not significant: how large a sample would we need?

Benchmarking and profiling

Benchmarking means measuring performance systematically; profiling means finding which part of the code is slow. Both are essential skills for making production code fast and resource-efficient.

Micro-benchmarks with Vitest

> Write a benchmark comparing algorithm A with algorithm B:
 
  Algorithm A: Array.filter().map()
  Algorithm B: a single Array.reduce()
 
  Test scenarios:
  - Arrays of 100, 1,000, 10,000, 100,000 elements
  - Run each scenario 1000 times
  - Report: mean, median, p99, memory usage
 
// benchmark.test.ts
import { bench, describe } from "vitest";
 
describe("search algorithms", () => {
  const data = Array.from({ length: 10000 },
    (_, i) => ({ id: i, value: Math.random() })
  );
 
  bench("filter + map", () => {
    data.filter(x => x.value > 0.5).map(x => x.id);
  });
 
  bench("reduce", () => {
    data.reduce((acc, x) => {
      if (x.value > 0.5) acc.push(x.id);
      return acc;
    }, [] as number[]);
  });
});
 
# run with: npx vitest bench

Node.js profiling with —prof

> Help me set up profiling for a Node.js app
  to find the CPU bottleneck
 
  # run the app with the profiler
  node --prof dist/server.js
 
  # send traffic (load test)
  k6 run profile-test.js
 
  # stop the app, then process the profile
  node --prof-process isolate-*.log > profile.txt
 
  # then have Claude analyse it
> Here is a Node.js CPU profile:
  [paste the top functions from profile.txt]
 
  Analyse:
  1. Which function burns the most CPU?
  2. Is it a real bottleneck or a false positive?
  3. What is the right way to optimise it?
 
  # Alternative: use clinic.js (easier)
  npm install -g clinic
  clinic doctor -- node dist/server.js
  # opens the browser automatically and shows a flame graph

Database query profiling

> Build a database profiling setup:
  1. Turn on query logging in Prisma:
  const prisma = new PrismaClient({
    log: [
      { level: "query", emit: "event" },
      { level: "error", emit: "stdout" },
    ],
  });
 
  prisma.$on("query", (e) => {
    if (e.duration > 100) {  // log queries > 100ms
      logger.warn({
        query: e.query,
        params: e.params,
        duration: e.duration,
      }, "Slow query detected");
    }
  });
 
  2. Add pg_stat_statements in PostgreSQL
     to track query statistics:
  CREATE EXTENSION pg_stat_statements;
 
  -- view the slow queries
  SELECT query, calls, mean_exec_time, total_exec_time
  FROM pg_stat_statements
  WHERE mean_exec_time > 100
  ORDER BY total_exec_time DESC
  LIMIT 10;

Error handling and recovery strategy

A production system has to handle failure gracefully. The goal is not merely “no errors” — it is knowing what to do when they happen.

Error taxonomy — classifying errors correctly

Error classExampleHow to handleRetry?
TransientNetwork timeout, DB connection blipRetry with backoffYes
ValidationBad email format, missing fieldReturn 400 with detailsNo
Business ruleExpired coupon, product out of stockReturn 422 with a user messageNo
AuthExpired token, insufficient rightsReturn 401/403Sometimes
DependencyStripe down, SendGrid failingFall back or queueYes
SystemOOM, disk full, a bug in the codeAlert the team, log in detailSometimes

The retry pattern with exponential backoff

> Build a retry utility for use across the project
 
// retry.ts
export async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxAttempts?: number;   // default: 3
    initialDelay?: number;  // default: 1000ms
    maxDelay?: number;      // default: 30000ms
    backoffFactor?: number; // default: 2 (exponential)
    retryIf?: (error: Error) => boolean;
    onRetry?: (attempt: number, error: Error) => void;
  } = {}
): Promise<T> {
  const {
    maxAttempts = 3,
    initialDelay = 1000,
    maxDelay = 30000,
    backoffFactor = 2,
    retryIf = () => true,
    onRetry = () => {},
  } = options;
 
  let lastError: Error;
 
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
 
      if (attempt === maxAttempts || !retryIf(lastError)) {
        throw lastError;
      }
 
      const delay = Math.min(
        initialDelay * Math.pow(backoffFactor, attempt - 1),
        maxDelay
      );
      const jitter = delay * 0.1 * Math.random(); // ±10% jitter
 
      onRetry(attempt, lastError);
      await new Promise(resolve => setTimeout(resolve, delay + jitter));
    }
  }
 
  throw lastError!;
}
 
// Usage
const result = await withRetry(
  () => stripe.paymentIntents.create(params),
  {
    maxAttempts: 3,
    retryIf: (err) => err.message.includes("network"),
    onRetry: (attempt, err) => logger.warn({ attempt, err }, "Retrying payment"),
  }
);

The circuit breaker pattern

Stops the system from repeatedly calling an external service that is already failing. It lets you fail fast and recover sooner.

> Implement a circuit breaker for external API calls
  States:
  - CLOSED: normal operation
  - OPEN: stop sending requests (fail fast)
  - HALF_OPEN: try one request; if it succeeds → CLOSED
 
  Config:
  - failureThreshold: 5 failures → OPEN
  - successThreshold: 2 successes → CLOSED (from HALF_OPEN)
  - timeout: 30 seconds before HALF_OPEN
 
// using the opossum library
npm install opossum
 
import CircuitBreaker from "opossum";
 
const stripeBreaker = new CircuitBreaker(
  (params) => stripe.paymentIntents.create(params),
  {
    timeout: 5000,          // 5s timeout per request
    errorThresholdPercentage: 50,  // 50% errors → OPEN
    resetTimeout: 30000,    // 30s before HALF_OPEN
    volumeThreshold: 10,    // needs 10+ requests before it can open
  }
);
 
stripeBreaker.on("open",     () => logger.error("Circuit OPEN: Stripe"));
stripeBreaker.on("halfOpen", () => logger.warn("Circuit HALF_OPEN: Testing Stripe"));
stripeBreaker.on("close",    () => logger.info("Circuit CLOSED: Stripe recovered"));
 
// Usage (transparent to caller)
const intent = await stripeBreaker.fire(params);

Graceful shutdown

When a server has to restart, the requests already in flight must finish first rather than being dropped halfway.

> Implement graceful shutdown for an Express server
  Requirements:
  - Catch the SIGTERM signal
  - Stop accepting new requests
  - Wait for in-flight requests to finish (timeout: 30s)
  - Close the database connection
  - Exit with code 0
 
// shutdown.ts
let isShuttingDown = false;
 
export function gracefulShutdown(server: Server) {
  process.on("SIGTERM", async () => {
    logger.info("SIGTERM received, starting graceful shutdown");
    isShuttingDown = true;
 
    server.close(async () => {
      try {
        await prisma.$disconnect();
        await redis.quit();
        logger.info("Graceful shutdown complete");
        process.exit(0);
      } catch (err) {
        logger.error(err, "Error during shutdown");
        process.exit(1);
      }
    });
 
    // force shutdown after 30s
    setTimeout(() => {
      logger.error("Forced shutdown after timeout");
      process.exit(1);
    }, 30000);
  });
}
 
// middleware: reject new requests during shutdown
app.use((req, res, next) => {
  if (isShuttingDown) {
    res.set("Connection", "close");
    return res.status(503).json({ error: "Server shutting down" });
  }
  next();
});

Production incident management

When something goes wrong in production, how you respond has a large effect on the resulting downtime. Claude helps with both the debugging and the communication.

Incident response playbook

1Detect — know that something is wrong
An alert from the monitoring you set up, or a wave of user reports. Claude Code can query the logs immediately
# check the state quickly
> Query the logs for the last 15 minutes:
  Run: grep "ERROR" /var/log/app/app.log | tail -50
  Or query Cloudwatch:
  aws logs filter-log-events --log-group-name /app/production
    --start-time $(date -d "15 minutes ago" +%s000)
    --filter-pattern "ERROR"
 
  Then summarise:
  1. Which class of error occurs most?
  2. When did it start?
  3. Which endpoints are affected?
2Triage — assess the severity
Give Claude what you have; it helps assess impact and priority
> Incident assessment:
  What we know:
  - Error rate: [X%] (normally < 0.1%)
  - Affected users: [N]
  - Affected features: [list]
  - Duration: [X minutes]
 
  Assess:
  1. Severity: P1 (system down) / P2 (a key feature broken) / P3 (minor impact)
  2. Who needs to be told: team lead, CTO, users?
  3. Timeline for a fix: hours or days
  4. Any quick mitigation we can apply right now?
3Mitigate — reduce the impact first
Apply a workaround to cut downtime, then go looking for the root cause
# mitigations you can apply fast
> We have found that the payment feature is broken
  and we do not yet know why
  Propose mitigation options we can apply within 5 minutes:
  - Feature flag: turn payments off temporarily
  - Maintenance mode: redirect to a notice page
  - Rollback: return to the previous version
  - Scale up: add servers if it is a load issue
  Give the actual command for each option
4Root cause analysis
Once the impact is contained, find the root cause systematically
5Post-mortem
After the fix, write a post-mortem so it does not happen again
> Write a post-mortem document for this incident:
  Details:
  - Started: [time]
  - Resolved: [time]
  - Duration: [X minutes]
  - Impact: [N users, N transactions affected]
 
  Template:
  ## Timeline
  [time] what happened
 
  ## Root Cause
  Describe the real root cause (not the symptom)
 
  ## Contributing Factors
  What made the problem worse or slowed the fix
 
  ## What Went Well
  What the response got right
 
  ## Action Items
  | Action | Owner | Due Date | Priority |
  |--------|-------|----------|----------|
  | Add a test that catches this bug | @john | 2025-01-20 | P1 |
  | Add an alert for this error pattern | @jane | 2025-01-18 | P1 |
 
  ## Prevention
  How will we stop this from recurring?

Summary: the production-ready checklist

Everything above, collected into one checklist to run before a real production deploy:

# .claude/commands/prod-ready.md
# Production Readiness Checklist
 
## Code Quality
□ TypeScript strict mode: no any
□ Tests: unit + integration + e2e all pass
□ Test coverage: >= 70% for new code
□ Linting: no errors
□ Build: succeeds with no significant warnings
 
## Security
□ Authentication: every protected route has an auth check
□ Authorization: ownership verified before data is returned
□ Input validation: every input goes through validation
□ SQL: no raw string concatenation
□ Secrets: no hardcoded credentials
□ Dependencies: npm audit reports no high/critical
 
## Performance
□ Database: indexes cover the frequently used queries
□ N+1: no N+1 query in a critical path
□ Load test: meets the performance target
□ Caching: expensive operations are cached
 
## Observability
□ Logging: structured logs in place
□ Error tracking: Sentry configured
□ Health check: /api/health works
□ Alerts: monitoring alerts set up
 
## Reliability
□ Error handling: every async call has try/catch
□ Retry logic: transient errors are retried
□ Graceful shutdown: SIGTERM handled
□ Circuit breaker: external APIs are protected
 
## Deployment
□ Migration: backward compatible
□ Rollback: plan is clear and tested
□ Feature flags: configured
□ Runbook: updated
 
□ All items: ✅ ready to deploy | ❌ must be fixed first

Glossary for this chapter

5 WhysA technique for finding the root cause by asking “why” repeatedly until you reach the real origin
Root causeThe actual cause of a problem, not the symptom you can see
Contract-firstDefining the interface/API contract before implementing, to reduce ambiguity
Circuit breakerA pattern that stops sending requests to a failing service so you fail fast
Exponential backoffRetrying with a longer wait each time, so a recovering service is not hammered
Graceful shutdownA shutdown that waits for in-flight requests to finish rather than dropping them
Feature flagA switch that turns a feature on or off without a redeploy
A/B testRunning 2 variants side by side with real users to measure which is better
Statistical significanceThe statistical confidence that a difference is not down to chance
p-valueA number saying how unlikely the observed result is under chance alone (< 0.05 = significant)
BenchmarkMeasuring performance systematically for comparison
ProfilingFinding which part of the code consumes the most CPU/memory
Flame graphA graph of the call stack by CPU usage, used to find bottlenecks
EXPLAIN ANALYZEThe SQL command that shows a query’s real execution plan
Load testTesting a system under heavy load to find its breaking point
Canary deploymentDeploying to a small % of users first, then expanding
Post-mortemThe analysis after an incident, to find the cause and prevent a repeat
MTTRMean Time To Recovery — the average time taken to resolve an incident
Error budgetThe amount of error/downtime an SLA tolerates (e.g. 0.1% per month)