Record, Replay, Repair: How a Code Debugging AI Can Safely Reproduce Production Bugs
If your debugging AI still asks for shell access to production, you’re doing it wrong. The fastest path from bug report to trusted fix is to record the right signals in production, replay them deterministically in an isolated environment, and repair the code under a hermetic build. With the right architecture, your AI can derive a reliable reproducer from logs alone—no privileged access, no live data exfiltration, and no guessing.
This article goes deep into how to design that stack. We’ll cover:
- What to record: requests, nondeterminism, concurrency, and environment.
- How to build: hermetic, reproducible builds that make replays trustworthy.
- How to replay: sandboxed, deterministic execution with strict isolation.
- How to repair: an AI-driven, test-first loop that produces confident patches.
- Governance: redaction, retention, and guardrails that reduce risk.
It’s opinionated and practical: you’ll get architecture patterns, concrete examples, and code snippets to bootstrap a system you can roll out incrementally without blowing up your SLOs—or your compliance posture.
TL;DR
- Treat production debug as a record–replay problem, not a shell access problem.
- Capture the minimal signals to reconstruct execution deterministically: inputs, sources of nondeterminism (time, random, concurrency, environment), and external effects.
- Build reproducibly. Hermetic builds (Bazel/Nix), pinned dependencies, SOURCE_DATE_EPOCH, content-addressed caches.
- Replay in isolation. Combine syscall-level or app-level replays with container/microVM sandboxes (gVisor/Firecracker) and deterministic time and IO.
- Let an AI derive a minimal reproducer and test, propose a patch, and rerun the replay to assert the fix—before a human approves.
- Apply strict data minimization: redaction, tokenization, policy-based retention, and egress-denied sandboxes.
Why Production Bugs Are Hard (and Why Record–Replay Works)
Production failures often hinge on the one thing your dev environment doesn’t have: reality. Live traffic patterns, skewed timestamps, rare concurrency schedules, partially-deployed versions, and messy data evolution generate classes of bugs that don’t reproduce locally. Traditional observability (logs, metrics, traces) answers “what happened,” but not “how do I replay it exactly?”
Record–replay adds the missing link: capture enough state and nondeterminism at the boundary to recreate the exact execution path offline. Do that under a hermetic, pinned build and run the code in a sandbox that enforces determinism, and you get a replay that either reproduces the bug or falsifies the hypothesis. Either way, the AI (and your team) can iterate quickly with confidence.
The payoff: lower MTTR, fewer production-only mysteries, and a steady stream of regression tests that harden the codebase.
Pillar 1: Record (What to Capture—And What Not To)
Recording is a balancing act: too little and you can’t reproduce; too much and you drown in cost, latency, and risk. The key is to capture the right minimal set, and to make it policy-driven.
Capture boundaries and nondeterminism
At a minimum, record:
- Request/response boundaries: HTTP/gRPC bodies and headers; message queue envelopes and payloads; CLI args. Include precise arrival timestamps.
- External effects: Outbound HTTP/gRPC, SQL queries and results (or result hashes + a DB snapshot), file IO reads/writes, cache hits/misses.
- Nondeterministic sources: time (wall/monotonic), random seeds, environment vars, locale/timezone, feature flags, and configuration snapshots.
- Concurrency and scheduling hints: logical event ordering, thread IDs, lock acquisition order, and async task wake-ups.
Optional but often crucial:
- Versioning metadata: git SHA, build provenance (SLSA level), dependency lockfiles, container image digests.
- System context: kernel version, CPU flags, libc, timezone database version; for JVM/.NET: runtime versions and GC flags.
Granularity: request-level versus process-level
There are two dominant strategies:
- App-level recorders: wrap ingress/egress at the application layer. Low overhead, portable, easy to redact. Risk: you may miss syscalls or subtle concurrency issues.
- System-level recorders: trace syscalls, threads, and signals (e.g., rr on Linux, gVisor debug logs, or BPF-based tracers). Higher fidelity and determinism, but with more overhead and operational complexity.
A pragmatic approach: default to app-level and dynamically escalate to system-level on error triggers or sampling budgets. Many teams report <5% p95 overhead with app-level recording when carefully scoped to errorful/slow requests and backfilled context from existing traces.
Techniques and tooling
- Web ingress: Envoy TAP, NGINX mirror, or app middleware to emit a request fixture. For gRPC, log binary-encoded messages with schemas.
- Outbound HTTP: capture URL, method, headers, and canonicalized body; record the full response body or hash + external snapshot where permissible.
- SQL: capture text queries and bound parameters; record returned rows up to a cap, or store a consistent DB snapshot/copy-on-write at the start of a request.
- Filesystem: intercept reads (path, offset, bytes) and writes; for large files, content-addressed segments (hash to blob storage) keep size manageable.
- Time/random: record the initial seeds and first read of time; later replays can be driven from these seeds.
- Concurrency: record logical event order or use deterministic scheduling during replay to eliminate schedule variance.
Example: Go HTTP service recorder (minimal)
go// middleware.go package main import ( "bytes" "crypto/sha256" "encoding/json" "io" "net/http" "time" ) type Fixture struct { Method string `json:"method"` URL string `json:"url"` Headers map[string]string `json:"headers"` Body []byte `json:"body"` Started time.Time `json:"started"` Env map[string]string `json:"env"` RandSeed int64 `json:"randSeed"` Version string `json:"version"` Outbound []OutboundCall `json:"outbound"` SQL []SQLCall `json:"sql"` } type OutboundCall struct { Method string `json:"method"` URL string `json:"url"` ReqH map[string]string `json:"reqH"` ReqB []byte `json:"reqB"` RespH map[string]string `json:"respH"` RespB []byte `json:"respB"` Status int `json:"status"` } type SQLCall struct { Query string `json:"query"` Args []any `json:"args"` Rows [][]any `json:"rows,omitempty"` // capped Hash [32]byte `json:"hash"` } func Recorder(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() body, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewReader(body)) fx := &Fixture{ Method: r.Method, URL: r.URL.String(), Headers: map[string]string{"Content-Type": r.Header.Get("Content-Type")}, Body: body, Started: started, Env: map[string]string{"TZ": time.Local.String()}, RandSeed: started.UnixNano(), Version: getVersion(), } // expose fx in context for outbound/SQL interceptors to append ctx := withFixture(r.Context(), fx) rw := &respRecorder{ResponseWriter: w} next.ServeHTTP(rw, r.WithContext(ctx)) // Hash body to dedupe sum := sha256.Sum256(fx.Body) _ = sum _ = persistFixture(fx) // redact before persisting }) } // respRecorder elided for brevity; see standard implementations
This is simplistic—production requires redaction, size caps, and careful capture of outbound IO. But this illustrates the core: record ingress, pass a fixture handle through context to collect outbound interactions, and then persist a redacted artifact with policy tags.
Privacy and redaction
Do not capture raw secrets or PII by default. Use:
- Deterministic tokenization for join keys (e.g., SHA-256 with a per-environment salt).
- Format- and context-aware scrubbers (credit cards, SSNs, email, auth tokens).
- Field-level allowlists driven by data classification (public/internal/confidential/restricted).
- On-write scanners: reject fixtures that violate policies (and fall back to fuzzy reproductions when necessary).
- TTL and retention policies: e.g., 7–30 days for reproducibility data, separate from observability retention.
Performance budgets
- Keep synchronous overhead to <5% p95 per request; write large blobs to an async queue.
- Use sampling strategies: 100% for 5xx and SLO-breaching requests; 1–5% baseline; 100% for canaries and new releases.
- Use on-demand triggers: temporarily elevate recording for a specific user, shard, or endpoint to chase a live incident.
Pillar 2: Hermetic Builds (Make the Replay Trustworthy)
A replay is only as good as the environment match. If your build isn’t reproducible, you’ll chase ghosts. Hermetic builds mean the build inputs are fully declared and pinned; the outputs are deterministic; and the runtime environment is controlled.
Core practices
- Pin everything: base image digest, toolchain versions, OS packages, and language dependencies. Prefer lockfiles with content hashes.
- Deterministic compilation: set SOURCE_DATE_EPOCH, disable non-deterministic embeds (timestamps, absolute paths), prefer compilers with reproducible build flags.
- Content-addressed caches: ensure the same inputs produce the same outputs (Bazel remote cache, Nix store, BuildKit with provenance).
- Provenance and attestation: SLSA-compliant metadata: who built what, from which source, using which toolchain.
Bazel: example target and reproducibility hints
python# WORKSPACE and MODULE.bazel pin toolchains; example BUILD snippet load("@io_bazel_rules_go//go:def.bzl", "go_binary") go_binary( name = "api_server", srcs = glob(["cmd/api/*.go", "internal/**/*.go"]), embed = ["//internal/pkg:lib"], gc_goopts = ["-trimpath"], x_defs = {"main.Version": "{GIT_SHA}"}, ) # For reproducibility: # - Use rules_nixpkgs or rules_oci for pinned images # - Set SOURCE_DATE_EPOCH in actions via --action_env
Nix flakes: lock the world
nix{ description = "Hermetic dev/build for api_server"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { packages.api_server = pkgs.buildGoModule { pname = "api_server"; version = "1.0.0"; src = ./.; vendorSha256 = "sha256-..."; ldflags = [ "-s" "-w" "-X main.Version=${self.rev or "dirty"}" ]; }; } ); }
Nix and Bazel complement each other: Nix provides hermetic toolchains, Bazel provides reproducible builds and caches across languages.
Validating reproducibility
- Rebuild twice in clean environments; byte-compare outputs.
- Use diffoscope on ELF/JAR/DWARF outputs to locate non-deterministic bits.
- Make reproducibility a CI gate on release branches.
Pillar 3: Sandboxed, Deterministic Replay
Now that you can build the exact binaries, replay them in an environment that removes nondeterminism and leaks.
Isolation layers
- Namespaces/cgroups containers with seccomp/AppArmor: baseline isolation with low overhead.
- User-space kernels (gVisor): stronger syscall mediation to avoid host-kernel quirks; good for untrusted code and strict isolation.
- MicroVMs (Firecracker/Kata): VM isolation with minimal overhead; excellent for zero-trust replays.
For debugging AI, I prefer gVisor or Firecracker: they give crisp syscall boundaries and make it easier to enforce no-network rules while still providing performance good enough to iterate.
Deterministic execution strategies
- Time: stub clock reads to a recorded timeline. Libraries like libfaketime or language-specific time providers can do this; for stronger guarantees, intercept syscalls in gVisor.
- Randomness: seed RNGs from the fixture and override global RNG instances.
- Network: replace outbound sockets with recorded responses; fail closed if a request isn’t in the fixture.
- Filesystem: mount a temporary snapshot; preload recorded files; deny writes except to a scratch volume.
- Scheduling: for reproducible concurrency bugs, use rr (Linux record/replay debugger) or run with deterministic schedulers where feasible; failing that, replay with the recorded IO and rely on deterministic IO ordering to converge.
Example: run a replay in gVisor with no egress
bash# Build image is content-addressed; includes api_server binary docker pull ghcr.io/acme/api_server@sha256:abc... # gVisor runtime ctr run \ --rm \ --runtime io.containerd.runsc.v1 \ --mount type=bind,src=$PWD/fixtures,dst=/fixtures,options=rbind:ro \ --net-host=false \ --env REPLAY_FIXTURE=/fixtures/req-2024-10-28T00:12:33Z.json \ --env REPLAY_MODE=strict \ --label noegress=true \ ghcr.io/acme/api_server@sha256:abc... \ replay-1 \ /bin/replay-entrypoint
The entrypoint loads the fixture, replaces system providers (time, RNG), mounts stubbed services, and runs the same binary with a harness that feeds the request and validates outputs against expected.
Example: Python replay harness with frozen time and HTTP stubs
python# replay.py import json, os, random, time from freezegun import freeze_time import requests class StubSession(requests.Session): def __init__(self, outbound): super().__init__() self.outbound = outbound self.idx = 0 def send(self, req, **kwargs): exp = self.outbound[self.idx] self.idx += 1 assert req.method == exp['method'] assert req.url == exp['url'] # Optionally validate body/headers resp = requests.Response() resp.status_code = exp['status'] resp._content = bytes(exp['respB']) resp.headers = exp['respH'] return resp with open(os.environ['REPLAY_FIXTURE']) as f: fx = json.load(f) random.seed(fx['randSeed']) with freeze_time(fx['started']): s = StubSession(fx['outbound']) # inject s into your code via dependency injection or monkeypatch # run your handler with fx['method'], fx['url'], fx['body']...
In strongly typed services, use dependency injection to avoid monkeypatching. The point is to run the exact code with deterministic providers and recorded IO.
Database snapshots
SQL is the hardest. Three patterns:
- Snapshot-and-replay: take a transactionally consistent snapshot of the relevant tables at request start; replay queries against that snapshot. Feasible for small tenants.
- Result-capture: record query + parameters + returned rows (truncated) and bypass the database on replay. Faster, but you can’t uncover query planner or schema issues.
- Shadow database: replicate production to a read-only shadow and tag replay queries with a tenant/session ID; risky unless heavily isolated.
For many orgs, a hybrid works: capture results for most queries, snapshot for high-value bug classes (migrations, timezone conversions, precision issues).
sql-- Postgres: ensure consistent snapshot at request ingress BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- Record txid_snapshot and xmin to persist snapshot identity SELECT txid_current(), txid_current_snapshot(); -- Proceed with queries; subsequent SELECTs see a stable view
Pillar 4: Repair (Let the AI Propose, You Approve)
With a deterministic replay, the debugging AI can do real work:
- Reduce to a minimal reproducer: prune inputs, drop irrelevant headers, shrink payloads, and discover the minimal state that triggers the bug.
- Localize the fault: correlate stack traces, diffs between passing/failing replays, and blame lines from git history.
- Propose a patch: suggest code changes with tests. The test should replay the minimal fixture, not just assert a branch.
- Validate: run the patch under replay; ensure the bug no longer reproduces and no new regressions appear in a representative suite.
Example: AI-generated pytest reproducer
python# tests/test_repro_bug_4287.py import json, os, random from mysvc.app import app from mysvc.providers import set_clock, set_http_client from tests.stubs import FrozenClock, StubHTTP here = os.path.dirname(__file__) fx = json.load(open(os.path.join(here, 'fixtures/bug_4287_min.json'))) def test_bug_4287_min(): random.seed(fx['randSeed']) set_clock(FrozenClock(fx['started'])) set_http_client(StubHTTP(fx['outbound'])) with app.test_client() as c: resp = c.open( path=fx['path'], method=fx['method'], headers=fx['headers'], data=bytes(fx['body']) ) assert resp.status_code == fx['expected_status'] assert json.loads(resp.data) == fx['expected_body']
Good AI systems also add a guardrail test asserting the bug never returns, and they link the test and patch to the original fixture’s hash for traceability.
Patch review and gating
- Security scan: enforce static checks (Semgrep, CodeQL), secret scanning, and dependency policy.
- Provenance check: ensure the build uses the recorded git SHA and locked deps; attach SLSA attestations.
- Replay CI: run the patch against the original failing fixture and a curated corpus of nearby fixtures.
- Human approval: senior engineer approves the diff; AI never pushes straight to production.
Architecture Blueprint: End-to-End Flow
- Recorder agents capture fixtures from production per policy and write to a secure message bus (e.g., Kafka with topic-level encryption).
- Redaction service scrubs sensitive fields, emits a compliance manifest, and rejects nonconformant fixtures.
- Bundle builder combines the fixture with build provenance (git SHA, lockfiles) and a manifest of required mocks (HTTP, SQL, files).
- Hermetic build farm (Bazel/Nix/BuildKit) produces a deterministic artifact with provenance attached.
- Replay orchestrator schedules a sandbox (gVisor/Firecracker) with egress denied, mounts the fixture, and runs the binary with a harness.
- Debugging AI consumes replay traces and logs, minimizes the reproducer, proposes a patch + tests, and opens a PR.
- CI runs replay and regression suites; approvers review and merge.
- Post-merge, the fixture is tagged as “fixed@version” and stored with TTL. The reproducer test enters the permanent test suite.
Safety, Privacy, and Compliance
You can make this safer than traditional debugging.
- Data minimization: default-deny field capture; opt-in per schema. Tokenize identifiers; strip bodies where possible.
- Egress denial: replay sandboxes have no outbound network access; mocks must satisfy all calls.
- Secrets handling: read-only access to necessary decryption keys; seal fixtures at rest with KMS and envelope encryption.
- Access controls: fixtures are resources with RBAC and audit logs; only incident responders and the AI service account can access.
- Retention: per-data-class TTLs (e.g., 7 days for PII-light fixtures, 24 hours for sensitive); automated redaction upgrades on policy changes.
- Compliance logging: each replay emits a policy compliance report (who accessed what, for which incident, under which approval).
Threat-model the AI agent as an untrusted process. Treat its container or microVM as untrusted: no egress, no filesystem mounts other than the fixture and readonly build layers, and no secrets beyond decryption of that fixture. Use hardware-backed isolation if needed (Firecracker jailer, Nitro/SEV).
Performance and Cost
- Storage: a compact JSON + blobs approach usually yields 10–200 KB per fixture for REST/gRPC, higher for data-heavy workloads. Use compression and content dedup.
- CPU: app-level recording adds single-digit percent overhead if asynchronous; system-level tracing can be 10–100% overhead—use sparingly.
- Replay fleet: scale with a short-lived pool. Most replays are under 1 minute; microVM cold-start is ~100ms–1s with Firecracker.
Make recording opt-in per endpoint and adaptive by error rate. Apply admission control to avoid adding tail latency: if the recorder queue is backlogged, throttle to sampling-only.
Worked Examples
A DST time bug (classic production-only failure)
Symptom: On the fall DST transition, a billing job double-charges some users. In production, the request hits during a 1:00 AM repeated hour; locally, devs can’t reproduce.
Record: fixture includes request time, timezone, parsed schedule, and outbound calls to a currency service. The time provider notes that 01:30 occurs twice.
Replay: clock is frozen to the recorded instant; the replay triggers the same branch computing the billing window. The AI reduces the fixture to a minimal input that includes a single boundary timestamp.
Repair: AI proposes changing time arithmetic to use timezone-aware Instant and LocalDate boundaries rather than naive wall time. It adds a test with two 1:30 instances and asserts idempotency. Replay passes; human approves.
A concurrency bug (rare interleaving)
Symptom: A request occasionally panics with “map concurrent read/write.” Happens in production at ~1/100k requests; never seen locally.
Record: app-level recorder insufficiently captures the schedule. System-level escalation triggers rr or a gVisor deterministic replay, recording thread interleavings and lock order.
Replay: deterministic scheduler reproduces the exact interleaving. The AI pinpoints a shared map without a lock in a hot path.
Repair: patch introduces a concurrent map or sharding by goroutine-local buffers. Adds a stress test seeded by the recorded schedule. Replay passes; incident closed.
A subtle schema drift
Symptom: After a zero-downtime migration, some rows serialize with precision loss (decimal to float). Observed only on certain Postgres minor versions.
Record: fixture captures query text, parameters, and returned rows for the problematic tenant; build provenance includes the DB driver version.
Replay: hermetic build uses the exact driver; DB snapshot ensures the same planner path. The AI finds that the migration’s cast fails on locales using commas as decimal separators.
Repair: patch enforces numeric encoding explicitly and adjusts the migration script. Adds a regression test with recorded rows and locale set. Replay validates fix.
Rollout Plan and Checklist
- Phase 0: Decide policy. Classify data, define redaction rules, and align with legal/compliance.
- Phase 1: Implement app-level recorder on 1–2 endpoints; redact aggressively; set sampling to errors-only.
- Phase 2: Build hermetic pipeline (Bazel/Nix) and attach provenance.
- Phase 3: Stand up a small replay fleet with gVisor; no egress; feed a handful of fixtures through manual replays.
- Phase 4: Integrate debugging AI to produce minimal repro tests and proposed patches; run in a gated CI environment.
- Phase 5: Expand coverage; add on-demand escalation to system-level replays for flaky concurrency bugs.
- Phase 6: Institutionalize: make “fixture or it didn’t happen” the norm for incident retros; track MTTR and percent of bugs fixed with first-attempt patches.
Checklist:
- Redaction rules applied and tested against synthetic PII corpus.
- SOURCE_DATE_EPOCH and lockfiles enforced; reproducible builds validated.
- Replay sandbox denies egress and enforces read-only filesystems.
- Recorder overhead <5% p95; backpressure and sampling implemented.
- CI runs replay tests on every PR touching affected modules.
- AI outputs traceable: fixture hash, build SHA, test IDs.
Common Pitfalls (and How to Avoid Them)
- Capturing too much: you don’t need full DB dumps for every request. Start with request + outbound + deterministic providers.
- Skipping provenance: without pinned dependencies and attestation, you chase mismatched environments.
- Allowing egress during replay: if the sandbox can talk to the network, your replay isn’t trustworthy and may leak data.
- Not validating redaction: test scrubbing with seeded sensitive samples; fail closed.
- Ignoring clock sources: many languages use multiple clocks (wall, monotonic). Stub both.
- Treating the AI as magical: it’s a fast assistant, not an oracle. Keep human review and policy guardrails.
References and Further Reading
- rr: Record and Replay Framework for Linux user-space debugging — https://rr-project.org/
- Pernosco time-travel debugger — https://pernos.co/
- gVisor (user-space kernel) — https://gvisor.dev/
- Firecracker microVM — https://firecracker-microvm.github.io/
- Reproducible Builds — https://reproducible-builds.org/
- SLSA Supply-chain Levels for Software Artifacts — https://slsa.dev/
- Bazel — https://bazel.build/
- Nix and NixOS — https://nixos.org/
- Envoy TAP filter — https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/tap_filter
- OpenTelemetry — https://opentelemetry.io/
- WireMock (HTTP stubbing) — https://wiremock.org/
- toxiproxy (fault injection) — https://github.com/Shopify/toxiproxy
Opinionated Closing
Debugging in production is ultimately a determinism problem. If you can’t make a failing request deterministic offline, you’re negotiating with chance, not engineering a fix. Record–replay–repair is the path to determinism: capture the essentials, build hermetically, replay in a sandbox, and let an AI accelerate the analysis while you enforce the guardrails. You’ll fix harder bugs faster, without granting any AI a dangerous foothold in production, and you’ll turn each incident into a permanent regression test rather than a war story.
