Skip to content

Engineering

Inside a behavioral diff: what to compare and what to ignore when replaying API traffic

The idea behind [traffic replay testing](/learn/traffic-replay-testing) is simple: send the same recorded requests to the old code and the new code, and compare what comes back. The hard part is the word "compare". A naive byte-for-byte diff fails on every request because of timestamps and IDs. A diff that ignores too much passes changes that break customers. The value of the technique lives entirely in the comparison rules.
By the Releo team · · 5 min read

Start by comparing everything

A behavioral diff should begin from the full response and subtract noise, rather than starting from a few fields and adding coverage. For each replayed request, capture:

  • Status code. A 200 becoming a 201, or a 404 becoming a 200 with an empty body, is a behavioral change clients branch on.
  • Headers. Content type, caching headers, pagination links, rate limit headers and Deprecation or Sunset headers all carry meaning.
  • Body. Parsed structurally, not compared as text, so that key order and whitespace do not matter but values, types and presence do.
  • Timing. Latency for each side, compared against a budget rather than for equality.
  • Side effects, where observable. Outbound calls, emitted events or database writes made while handling the request.

Everything in that list is in scope by default. Every exclusion that follows is an explicit, reviewed rule.

Normalizing values that are supposed to differ

Some values legitimately change on every request. The goal is to neutralise them without hiding changes in their shape or meaning.

Timestamps

Do not drop timestamp fields; replace them with a placeholder after checking they still parse in the same format and time zone. A field that moves from 2026-10-21T09:00:00Z to a Unix integer, or from UTC to local time, is a real change. A field whose value differs by a few milliseconds is not.

Generated identifiers

Replace generated IDs with stable tokens assigned in order of first appearance, so inv_8f2a becomes <id:1> everywhere it appears in both responses. This preserves referential structure: if the old response used the same ID in two places and the new one uses two different IDs, the diff still catches it. Check the prefix and format separately, since a change from inv_ to in_ breaks clients that parse IDs.

Nonces, ETags and request IDs

Values that exist only to be unique (request IDs, trace IDs, nonces) can be masked entirely, as long as their presence is still asserted.

Ordering: decide which collections are sets

Arrays are ordered in JSON, but many API collections are semantically unordered: the tags on a resource, the scopes on a token, the set of enabled features. If the new code returns them in a different order, a strict diff fails for no reason.

The fix is to declare, per path, which arrays are sets and sort them by a stable key before comparing. The declaration must be explicit, because the opposite mistake is worse: list endpoints usually have a documented or implied order, and a change in default sort is one of the classic breaking changes. Treat $.data on a list endpoint as ordered unless you have a reason not to.

rules:
  - path: "$.tags"
    compare: unordered
  - path: "$.data[*].line_items"
    compare: unordered
    key: "sku"
  - path: "$.data"
    compare: ordered   # default sort is part of the contract

Header noise and header signal

Headers mix pure infrastructure noise with fields clients depend on. A reasonable default split:

  • Ignore: Date, Server, Via, X-Request-Id, trace headers, Set-Cookie values for session tokens (but not their presence or attributes), and load balancer or CDN diagnostics.
  • Compare exactly: Content-Type, Location, Link (pagination), Allow, Deprecation, Sunset, Retry-After, and any X- header documented in the API reference.
  • Compare semantically: Cache-Control and Vary directives as unordered sets, and rate limit headers by name and presence rather than by remaining count.

When in doubt, compare. A header that turns out to be noise produces a false positive you can add a rule for. A header that turns out to matter, and was ignored, produces an incident.

Latency budgets, secrets and PII

Latency

Timing is never equal between two runs, so compare it against a budget. A practical rule is to flag a request when the new latency exceeds the old by both a relative margin and an absolute floor, so a jump from 2ms to 5ms does not fire but a jump from 200ms to 900ms does. Look at percentiles across the replay set as well as individual requests, since a change that adds a database query often shows up as a shifted tail rather than one slow call.

Redaction

Recorded production traffic contains credentials and personal data, so redaction is part of the design, not an afterthought. Strip or tokenise Authorization headers, API keys, cookies and known sensitive fields at capture time, before anything is stored. Use format-preserving placeholders where the format matters for behavior (an email address should still look like an email) and keep the mapping consistent within a request so that relationships survive.

Deciding pass or fail

After normalization, each request lands in one of three states: identical, divergent, or divergent but expected. The last category is what makes the system usable for real migrations, where some behavior is supposed to change.

  1. Every divergence is reported with the request, the path, the old value and the new value.
  2. Expected changes are declared up front, tied to a specific changelog entry or migration step, and scoped to specific paths. "Ignore $.amount" is too broad; "$.amount is multiplied by 100" is a rule you can check.
  3. Any divergence not covered by a declared expectation fails the run and blocks the merge.
  4. Normalization rules and expectations are versioned with the code and reviewed like code, because a loose rule is a silent hole in coverage.

This is how Releo gates its own migration PRs: replayed traffic, diffed under explicit rules, with any unexplained divergence blocking the merge. The same rules apply to any team building replay in-house: compare everything, normalise only what you can justify, and make every exception visible.

Frequently asked questions

How do you handle nondeterministic fields when diffing API responses?

Replace them with placeholders after checking their format, so timestamps and IDs stop causing false failures while changes in format, type or presence are still caught. Map generated IDs to stable tokens to preserve references.

Should latency be part of a behavioral diff?

Yes, but against a budget rather than for equality. Flag requests that exceed both a relative and an absolute threshold, and compare percentiles across the full replay set.

Is it safe to replay production traffic?

It can be, if secrets and personal data are redacted at capture time and replay targets isolated environments with side effects stubbed or sandboxed. Never replay mutating requests against production systems.

More from the blog

Coming soon

Ship API changes without breaking your customers.

Releo is in private beta. Join the waitlist for early access.