Postmortem: How a Bun 1.2 Runtime Bug and Node.js 22 Interop Caused Our API to Return Incorrect JSON for 30 Minutes
Postmortem: How a Bun 1.2 Runtime Bug and Node.js 22 Interop Caused Our API to Return Incorrect JSON for 30 Minutes
Current Situation Analysis
The production Payment API experienced a 30-minute SEV-1 incident where downstream clients received malformed JSON responses, triggering widespread payment processing failures. The core pain point stemmed from an unvalidated assumption that JSON.stringify() behavior and CJS-to-ESM interoperability are deterministic across JavaScript runtimes.
Failure Modes:
- Missing Required Fields: ~12% of responses lacked critical payment payload fields because the custom
toJSON()serialization method was silently skipped. - Metadata Leakage: ~8% of responses contained transpilation artifacts (e.g.,
__esModule,__BUNDLE__) that should have been stripped, causing client-side schema validation failures.
Why Traditional Methods Failed:
- Runtime-Specific Transpilation Assumptions: The shared DTO library was transpiled with
@babel/preset-envtargeting Node.js 22, relying on specific property descriptor behavior. Bun 1.2's CJS interop layer altered enumeration rules, breaking the implicit contract. - Lack of Cross-Runtime Serialization Testing: CI/CD pipelines only validated JSON output against a single runtime baseline, missing interop edge cases.
- Immediate Full Rollout: Deploying a major runtime upgrade (Node.js 22 β Bun 1.2) without a canary phase or runtime-matrix testing amplified the blast radius when the regression triggered.
WOW Moment: Key Findings
| Approach | toJSON() Invocation Rate |
Serialization Correctness | Cold Start Latency | CJS Interop Property Enumeration Accuracy |
|---|---|---|---|---|
| Node.js 22.1.0 (Baseline) | 100% | 100% | 115ms | 100% |
| Bun 1.2.0 (Buggy) | 80% | 92% | 42ms | 85% |
| Bun 1.2.1 (Patched) | 100% | 100% | 43ms | 100% |
Key Findings:
- Bun 1.2's CJS interop regression caused
toJSON()to be marked non-enumerable for ~20% of DTO instances, depending on import resolution paths. - The regression also incorrectly flipped enumeration flags on Babel-injected metadata, causing
JSON.stringify()to leak internal properties. - Patched Bun 1.2.1 restored enumeration parity with Node.js 22 while preserving the ~63% cold start latency improvement, confirming the performance benefit is viable once interop correctness is guaranteed.
Core Solution
Resolution required immediate rollback, followed by architectural and pipeline hardening to eliminate runtime-specific serialization drift.
1. Cross-Runtime JSON Schema Validation in CI/CD Implemented strict schema validation that runs against both Node.js and Bun runtimes. This catches enumeration and serialization drift before deployment.
// ci/validation/matrix-runner.js
import { execSync } from 'child_process';
import { validatePayload } from './schema-validator.js';
const runtimes = ['node', 'bun'];
const testCases = ['payment-request', 'payment-response', 'refund-dto'];
for (const runtime of runtimes) {
console.log(`[CI] Running serialization tests on ${runtime}...`);
for (const testCase of testCases) {
const output = execSync(`${runtime} ./dist/serialize-test.js ${testCase}`).toString();
const result = JSON.parse(output);
if (!validatePayload(result, testCase)) {
throw new Error(`Schema validation failed on ${runtime} for ${testCase}`);
}
}
}
2. Real-Time Validation Sidecar Architecture Deployed an Envoy-based sidecar proxy that intercepts all API responses and validates them against the OpenAPI/JSON schema. Malformed responses trigger immediate alerts and fallback routing.
# k8s/sidecar/envoy-config.yaml
static_resources:
listeners:
- name: response_validator
filter_chains:
- filters:
- name: envoy.filters.http.lua
typed_config:
inline_code: |
function envoy_on_response(response_handle)
local body = response_handle:body()
local schema = require("payment_schema")
if not schema.validate(body) then
response_handle:logWarn("MALFORMED_JSON_DETECTED")
response_handle:headers():add("X-Validation-Status", "FAIL")
end
end
3. ESM-Native Migration Strategy Deprecated Node.js 22-specific CJS transpilation for Bun-based services. Migrated shared libraries to native ESM with explicit exports, bypassing CJS interop enumeration quirks entirely.
// package.json (shared-dto-lib)
{
"type": "module",
"exports": {
".": "./dist/index.js",
"./dto": "./dist/dto.js"
}
}
4. Runtime Pinning & Mandatory Canary Periods All deployment manifests now pin exact runtime versions. Minor runtime upgrades require a 24-hour canary deployment with automated JSON schema validation and error-rate monitoring before full rollout.
Pitfall Guide
- Implicit
toJSON()Reliance Without Schema Enforcement: AssumingJSON.stringify()will always invoke custom serialization methods. Always pair DTO serialization with strict JSON schema validation at runtime and in CI. - CJS-to-ESM Interop Property Descriptor Assumptions: Different runtimes handle Babel-injected property descriptors (
enumerable,configurable) differently during module resolution. Never assume enumeration parity across Node.js, Bun, or Deno. - Single-Runtime CI/CD Validation Gaps: Testing serialization only against one runtime masks interop regressions. Implement a runtime matrix in CI to validate identical output across all supported engines.
- Skipping Canary Periods for Runtime Upgrades: Swapping JavaScript runtimes is a high-risk infrastructure change. Always deploy to a canary subset with automated response validation and rollback triggers.
- Transpilation Metadata Leakage: Babel and other transpilers inject internal markers (
__esModule,__BUNDLE__). If enumeration flags are altered by the runtime, these leak into production payloads. Configure transpilers to strip metadata or use native ESM. - Lack of Production-Side Response Validation: Relying solely on client-side error reporting delays incident detection. Deploy validation sidecars or API gateways that enforce schema compliance and alert within seconds of malformed output.
Deliverables
- Blueprint: Cross-Runtime Serialization Validation & Canary Deployment Architecture (covers Envoy sidecar routing, CI matrix configuration, and ESM migration paths)
- Checklist: Runtime Upgrade & Interop Safety Checklist (pre-deployment validation steps, canary monitoring thresholds, rollback criteria, and schema enforcement requirements)
- Configuration Templates: Ready-to-use CI/CD pipeline matrix configs, Kubernetes canary deployment manifests, and Envoy/Lua validation sidecar templates for immediate integration
