Skip to content

Case Study: Prototype to Production ​

Background ​

Queuebird — async PDF report generation for mid-market analytics teams. Founder + two contractors built a prototype in ten days to close a pilot: Express API, BullMQ workers, Postgres, S3 for artifacts. It worked in demos. The pilot customer planned a rollout to ~200 seats in six weeks. The prototype had: no structured logging, errors as res.status(500).send(e.message), retries that duplicated side effects, secrets in .env committed once (rotated, but the pattern remained), and a worker that console.log'd stack traces containing customer query SQL.

Mandate: harden to production before the 200-user date. Agents encouraged for speed.

Initial vague prompt ​

text
Harden this prototype for production. Add error handling, logging,
retries, validation, rate limiting, and security best practices across
the API and workers. Keep behavior the same.

Claude Code opened a PR titled "production hardening" — 89 files, ~3.1k lines. Description claimed: "Added comprehensive error handling, pino logging, zod validation, rate limits, and safer BullMQ retries."

Why it failed ​

The PR was too large to review. They merged to a staging branch and probed:

ClaimReality
Error handlingCentral middleware on HTTP; workers still threw into BullMQ with default retry that re-sent emails
Loggingpino on API; workers still console.log; SQL still in logs on the failure path in reportWorker.ts
ValidationZod on 4 of 11 routes — the ones the agent touched while "exploring"
Rate limitingGlobal IP limiter; missing on the expensive POST /reports that the pilot would hammer
Retriesattempts: 5 added; no idempotency key on S3 put or email send

"Added error handling" in a PR title is a cross-cutting claim. Agents assert them globally; they implement them locally on the files in context. Reviewing per-PR narrative fails; you must verify per surface.

The improved spec ​

They reverted the mega-PR and wrote a hardening matrix in specs/prod-harden.md:

markdown
## Surfaces × Controls

| Surface | Errors | Logs | Validation | Authz | Idempotency | Rate limit |
|---|---|---|---|---|---|---|
| HTTP routes (11) | middleware + typed AppError | pino redaction | zod per route | session | — | per-route |
| reportWorker | no throw after side effect | pino child | job schema | — | jobId = reportId | concurrency cap |
| emailSender | swallow+metric | no PII body | — | — | provider key | — |
| s3Artifact | retry only on 5xx | key names only | — | — | if-none-match | — |

### Process
- One control column per PR (or one surface row) — never "all hardening"
- Done when the matrix cell is checked by a human with evidence (file:line)

Each agent session filled cells, not vibes. Example prompt:

text
Read specs/prod-harden.md. Implement ONLY the Validation column for HTTP
routes. List every route file. For each, add zod and a test. Do not change
logging, retries, or workers. End with a table of route → schema file.

Agent workflow ​

Failure injection mattered: they killed Redis mid-job and proved emailSender did not double-send after the idempotency cell was filled. Before that cell, it double-sent — despite the earlier mega-PR's "safer retries."

They also kept a running "evidence log" in the PR description:

markdown
| Cell | Evidence |
|---|---|
| HTTP validation | routes listed in PR; each has `*.schema.ts`; test names … |
| reportWorker idempotency | reportWorker.ts:88 uses jobId; chaos test `worker-redis-kill.test.ts` |

Without the evidence log, reviewers fell back to trusting the PR title again — the exact failure mode they were escaping.

Human intervention points ​

  1. Matrix authorship — humans define surfaces; agents don't get to invent the inventory from a partial tree walk. First agent inventory missed emailSender entirely (it lived under lib/notify/).
  2. Evidence review — every green cell needs path:line or a test name, not a PR adjective.
  3. Secret/PII pass — human grepped logs for SQL and email bodies after logging work. Found authorization headers still logged on a debug branch the agent left in.
  4. Load test + chaos — not delegated; agents can write the script, humans watch the outcome. Target: 50 concurrent POST /reports without duplicate emails.
  5. Accept residual risk — they explicitly deferred request signing for webhooks to post-pilot; wrote it down so the next agent wouldn't "finish" it mid-hotfix.

Mistakes made ​

  • Merging the 89-file hardening PR to a long-lived branch. Even unmerged, it poisoned reviews and wasted 1.5 days of bisecting which parts to keep. Cost: calendar slip.
  • Believing worker error handling came free with API middleware. Classic prototype lie; cost: duplicate pilot emails in staging (lucky: not prod).
  • Rate limiter on the wrong dimension. IP limits don't protect expensive per-account report generation. Fixed after the pilot's CI job got a 429 from a shared NAT — two hours of confusion.
  • Redaction config that redacted password but not token or sql. Agent used a common pino redaction snippet. Human expanded the list after reading one real error log.
  • Closing matrix cells based on "looks done" in review. One cell (S3 idempotency) was marked done because the code comment said if-none-match; the header wasn't set. Chaos test would have caught it — they had skipped writing that test to "save time." Cost: 4 hours later under load.

Final outcome ​

Hardening took 13 engineer-days against a 10-day fantasy. Matrix ended with 3 deferred cells documented. At 200 seats: one Sev-3 (S3 throttle, mitigated by backoff cell that had been marked done with evidence). No duplicate emails in prod. The mega-PR's only lasting gift was a couple of zod schemas they cherry-picked (~200 LOC). Final hardened surface: ~8k LOC app + workers, with the matrix checked into docs/prod-harden-matrix.md and updated when new surfaces appear.

Lessons ​

  1. Cross-cutting claims ("added error handling") must be verified per surface via a matrix, not accepted from a PR description.
  2. Split hardening into column-or-row PRs; mega-hardening PRs are unreviewable and falsely complete.
  3. Workers are a separate surface from HTTP — prototype debt concentrates there.
  4. Idempotency is part of retries; retries without it are a duplicate-side-effect machine.
  5. Grep production-like logs for PII after any logging change; default redaction snippets are incomplete.
  6. A cell is not done without evidence a hostile reviewer can check in under a minute.

A field manual for AI-native software engineering.