VCR Cassette Recording Limitations for Third-Party API Testing

Cassettes speed up tests but cannot verify stateful workflows or catch API changes.

· 9 min read
Cover illustration for “VCR Cassette Recording Limitations for Third-Party API Testing”
API Simulation and Mocking · October 23, 2026 · 9 min read · 1,913 words

For integration testing against stateful APIs without hitting production, use a stateful simulator that has seeded data, lets tests control failures, and has its behavior checked against the real API. VCR cassette recording is still useful for fast, offline tests of stable, single-call responses. Its central limitation is that replaying a recorded answer does not establish that an integration handles the resource lifecycle, operation order, or changing provider behavior correctly.

What VCR cassette recording actually does and why teams reach for it

A VCR tool records a live HTTP request and its response, saves the pair to a YAML or JSON cassette, and replays that interaction on subsequent test runs. Replay needs neither a real HTTP request nor an API key. It removes provider uptime from the test’s dependencies. For API-dependent suites, that directly addresses the cost, latency, and flakiness of repeated live calls.

EasyPost reported a 50x testing speed increase after adopting VCR in its Python client library. The company also described users who made so many live API calls in their tests that they hit rate-limit 429s, which disrupted the tests. Recording removes that repeated traffic while giving assertions a deterministic response. The fit is particularly strong for read-only, single-call integrations with a stable response shape and no workflow state.

Three hidden behaviors that silently corrupt recordings before tests even run

The recording setup can undermine that confidence before an assertion evaluates the integration. Three behaviors deserve attention, including a misleading missing-cassette failure, background traffic captured with the intended interaction, and request matching that selects the wrong recording. They affect different parts of the process. Their shared risk is that the visible failure can appear far from the configuration or recorded content that caused it.

pytest-recording uses none as its default recording mode to prevent unintentional network requests. When a cassette is missing, the failure can look like a connection error. A developer can therefore investigate connectivity even though the test lacks the fixture it needs and the configured mode prevents recording one.

Background traffic creates a different ambiguity because a cassette can contain more than the interaction under test. Matching adds another source of misleading replay when requests share an HTTP method and URI but have different bodies. EasyPost recommends custom matching on attributes such as host, body, headers, path, and query for suites that repeatedly call the same endpoint. Diagnosing these problems requires reading the cassette itself, rather than treating its presence as evidence of a sound fixture.

What cassettes cannot record: the sensitive data problem and protocol gaps

Even a recording that gets the exchange right can hold data that should never go into version control. VCR.py stores unencrypted YAML containing full request and response bodies, which can include API keys, authentication tokens, and personally identifiable information. VCR.py offers filtering hooks such as filter_headers and filter_query_parameters, but it provides neither built-in encryption nor automatic PII detection and masking. Sensitive content therefore needs deliberate sanitization before the cassette is committed.

EasyPost presents filter_sensitive_data and similar controls as recommended, optional safeguards. That distinction leaves the responsibility with the team recording the interaction. A cassette may be functionally correct while remaining unsafe to share. The operational task is consequently broader than obtaining a successful response, because the stored request and response must also be suitable for the repository that will hold them.

Protocol support imposes a separate boundary that sanitization cannot change. The HTTP/HTTPS cassette approach described here does not capture WebSocket connections, gRPC, or binary protocols. Streaming and bidirectional interactions fall outside that recording model. For integrations using those forms of communication, the missing coverage is not an inaccurate fixture but an interaction the cassette cannot represent.

How do you test stateful APIs when operation order matters?

In a workflow that creates, reads, updates, and deletes a resource, responses must do more than look right on their own. Each operation must observe the state left by earlier operations. A cassette instead supplies a previously recorded answer. Although recorded exchanges can reproduce a particular sequence, replay does not itself implement the resource lifecycle or establish that the sequence remains valid when the test changes.

Consider the relationship between a POST and the following GET. The ID returned by a recorded POST is not stored as a newly created resource for the next request to retrieve. Likewise, a DELETE conflict requires knowledge of whether the resource exists and whether the operation is valid in its current state. Replay has no such model. Matching another response cannot supply the missing causal relationship between those calls.

A payment example shows the practical consequence. A team mocked POST /v1/payment_intents and asserted 200, but its first real checkout in staging failed because the handler never stored the PaymentIntent ID. The next step in the webhook path read undefined. The response assertion had established only shape. Stripe’s stripe-mock has the same limit, because it validates a POST but then ignores it and does not store state for later requests.

Stateful tests must create state, assert against it, and clean it up. Uncontrolled shared state introduces test-order dependencies, disrupts parallel execution, and makes teardown part of correctness. Handwritten mocks can add storage and scripted transitions, but as they behave more like the service, they become another version of it, with their own bugs and code to maintain. At that point, a verified simulator operating on seeded data is the stronger foundation.

How cassettes drift silently and why the tests keep passing after they do

Even a cassette that accurately represents its original interaction can become obsolete. The provider may add fields, remove fields, change types, or change response formats while replay keeps returning the saved payload. Nothing in that local exchange asks the provider what changed. A passing replay test establishes only that the cassette matched and the assertions accepted its contents.

LLM APIs make the distinction visible when response shapes or streaming formats change. A replay-only CI suite can stay green while production gets something different. Drift reporting gives those differences explicit severity. An added field can be informational, a removed field a warning, and a changed type an error that causes a non-zero exit. Without that comparison, replay provides no signal that the stored contract has decayed.

EasyPost describes cassette expiration as a way to force periodic re-recording. A scheduled CI workflow can go further by re-recording interactions against live APIs and opening a pull request when their contents change. Both approaches restore contact with the provider. They still need live credentials, a working connection, and someone to review the recordings, and changes can go unchecked between refreshes. A changed file also does not establish that the application handles the change correctly.

Known future changes expose another limit because the server cannot supply a recording of behavior it has not introduced. Testing an upcoming response or an unusual case therefore requires a manually constructed cassette or a code-based mock. Contract testing addresses whether a substitute has drifted from the real API. It does not, when applied to a stateless cassette, add coverage of state-dependent behavior.

Specific provider behaviors that cassettes cannot reproduce

Stripe exposes both a state-model gap and a delivery-semantics gap. Its hardcoded stripe-mock responses are correct in type but do not provide stored PaymentIntent IDs, conflict behavior, or idempotency behavior. Webhook delivery also differs between environments. Live mode retries events for up to three days, while a sandbox retries three times over a few hours. A recorded receipt does not exercise that difference in delivery behavior.

PayPal’s sandbox simulates production behavior, but its webhook simulator generates demonstration events that are not associated with any app and do not appear in the dashboard event viewer. Those events have a narrower role than a full application workflow. PayPal retries failed webhook deliveries up to 25 times over three days. Replaying one receipt does not show how a handler behaves through all those attempts.

GitHub does not automatically redeliver failed webhook deliveries. Redelivery instead requires manual action or scripting. This makes its delivery behavior distinct from Stripe’s and PayPal’s retry policies. A handler’s idempotency across duplicate deliveries also depends on what it already processed, which a recorded request alone cannot establish.

For Kalshi, Resend, Lob, and Slack, the supplied material establishes no official mock or VCR cassette tooling. Handwritten cassettes and vendor sandboxes, where available, remain possible test inputs. Across these providers, webhook retries, duplicate delivery, event ordering, and conflict detection require behavior extending beyond a single saved HTTP exchange.

Why fault injection and agent testing push cassettes past their breaking point

Resilience testing needs failures that can be produced deliberately and repeatedly. A cassette with a successful response cannot test how code handles timeouts, connection failures, 429s, or 5xx responses. Building an error fixture lets a test make more assertions. It does not, by itself, test the full retry sequence or how state changes between attempts. The test environment must control both the failure and the behavior that follows it.

In AI agent tool-use testing, the agent also makes decisions between those calls. The test must examine parameter extraction, endpoint selection, and response parsing across a sequence of external operations. An agent can write text that makes sense while sending malformed JSON to a payment API, so checking its language alone misses the integration failure.

Search results and successful file writes provide a simpler environment than a connection timeout or a structurally valid but semantically stale third-party record. Those failures can disrupt later reasoning. Detecting the effect requires sequential execution in which later calls encounter the consequences of earlier ones, rather than a collection of independently plausible responses.

Newer VCR tooling records traffic between AI coding agents and LLM providers and replays the agent loop without calling the provider. That remains useful for cost control and determinism. But replaying the LLM exchange does not show that state transitions work correctly in the external services the agent calls. RL training environments extend that requirement to deterministic, horizontally scalable execution with controllable state across thousands of containers. A stateful, verified simulator can provide that environment and do more than save a transcript.

What the pattern of cassette failures points toward in a testing strategy

Cassettes fit stable, read-only, single-call tests, while state-dependent integration tests need a simulator that implements the relevant lifecycle. The distinction is concrete. A simulator should accept a POST, store its result, return that resource on a later GET, and produce a 409 when a conflicting operation warrants it. The criterion is whether prior operations change later outcomes as they would in the real service.

That simulator also needs verification against the live API before every release. State alone is not evidence of fidelity. Continuous verification checks for drift, and protocol coverage must include the interactions the application uses. This requires separate checks of the simulator, so every application test can run without contacting the provider.

For CI/CD, the local test environment should run without live credentials, rate-limit exposure, or dependence on provider uptime. It should behave identically on a laptop and in a container. Pre-seeded data supplies a coherent starting world. Tests can then run workflows from a known state without rebuilding every resource in an empty sandbox, and control over state makes deterministic execution possible.

Fault injection belongs in that environment as a configurable capability, including 429s, 503s, and latency spikes on demand. Agent and RL workloads need the same controls together with horizontal scalability. The substitute must preserve the relationship between actions, state, and subsequent outcomes while remaining aligned with the provider.

Sources

  1. Getting started with vcr

More in API Simulation and Mocking