Honest split first: Polly.JS's core loop still works, and we proved it before writing a word of this page. Netflix's record-and-replay library recorded a real request through Node's https module, wrote a standard HAR 1.2 file to disk, and replayed it offline β we even tampered with the saved response body and got the tampered bytes back, so the replay demonstrably never touched the network. 10,000+ GitHub stars and ~200k weekly downloads of @pollyjs/core were earned. And storing recordings as standard HAR instead of a proprietary format is a design decision that ages beautifully β more on that below, because it's your eject hatch.
We verified all of these the week of writing (Aug 2026 β @pollyjs/core 6.0.6, adapter-node-http 6.0.6, adapter-fetch 6.0.7, Node 22) β every snippet on this page was actually run:
@pollyjs/core is 6.0.6, published July 2023 β over three years ago. The GitHub repo isn't archived, but its last push was May 2025 (a docs-era patch to adapter-fetch), with ~60 issues open. Meanwhile ~200,000 downloads a week keep flowing β a lot of teams depend on a library nobody is actively steering.fetch silently escapes adapter-node-http. The adapter patches the http/https modules, but Node's global fetch (undici) doesn't use them. We proved it in one process: with Polly in replay mode, https.get was served our (tampered) recording while fetch of the identical URL returned live network data. No error, no warning β your "replayed" test quietly talks to production. Details below.[Polly] Recording may fail because the browser is offline. Node 21+ added a global navigator, and navigator.onLine is undefined there β Polly's browser detection misfires. Cosmetic, but it's the kind of thing a maintained library would have patched.adapter-node-http isn't matched by adapter-fetch (they compute different request identifiers β the node adapter includes the host header, the fetch adapter doesn't). And when we forced a fetch-made recording through adapter-node-http with header matching disabled, the replay found the entry and then crashed with TypeError [ERR_INVALID_ARG_TYPE] β¦ Buffer.from deep in getChunksFromBody. Practical consequence: migrating your app from axios to native fetch means re-recording everything.adapter-fetch does patch Node 22's global fetch. We expected it to be browser-only; it isn't. Recordβreplay through adapters: ['fetch'] worked in plain Node. But you have to know to use it β the READMEs and a decade of blog posts reach for adapter-node-http, which is exactly the adapter native fetch escapes.This is the one that costs people an afternoon. Same process, same Polly instance, replay mode with recordIfMissing: false:
import { Polly } from '@pollyjs/core';
import NodeHttpAdapter from '@pollyjs/adapter-node-http';
import FSPersister from '@pollyjs/persister-fs';
import https from 'node:https';
Polly.register(NodeHttpAdapter);
Polly.register(FSPersister);
const polly = new Polly('demo-products', {
adapters: ['node-http'], persister: 'fs', mode: 'replay', recordIfMissing: false,
});
const URL_ = 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=2';
// served from the recording on disk (we tampered with it to prove the point):
const viaHttp = await httpsGet(URL_); // β "TAMPERED-BY-EDITOR"
// Polly never sees this one β it escapes to the real network:
const viaFetch = await (await fetch(URL_)).json(); // β live data
Our actual run printed:
https.get first name : TAMPERED-BY-EDITOR
native fetch first name: Service Name
ESCAPED_POLLY: true
If your codebase (or any dependency) uses native fetch, those calls bypass the recording entirely and hit the real service β in CI, on a plane, against production. The fix inside Polly is switching to adapters: ['fetch'] and re-recording. The fix outside Polly is making the mock a URL, so it doesn't matter which HTTP client anything uses.
Polly's persister-fs writes standard HAR 1.2 (creator: "Polly.JS"). Mockbird imports HAR files directly, so any recording you've already made can become a hosted mock API β reachable by curl, browsers, mobile apps, subprocesses, teammates, CI:
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=polly-eject" \
-H "content-type: application/json" \
--data-binary @recordings/demo-products_*/recording.har
We ran exactly that against a recording made minutes earlier: it answered 201 with a live base URL, and GET /m/<project>/products returned the recorded records verbatim β now with full CRUD, filtering, pagination and failure injection on top. The JSON responses in the HAR become real, stateful collections. (Full details in the HAR β mock API guide β the same import accepts DevTools HAR exports.)
Mockbird has record-and-replay built in, but it happens at a URL every client can reach instead of inside one JS process. Point a project's proxy at the real API and turn recording on:
# 1. create a project (no signup)
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H "content-type: application/json" -d '{"name":"recorded"}'
# β {"id":"<id>","adminKey":"<key>","baseUrl":".../m/<id>", ...}
# 2. proxy unmatched paths to the real API, and record what comes back
curl -X PUT https://mockbird.mockbird.workers.dev/api/projects/<id>/settings \
-H "x-admin-key: <key>" -H "content-type: application/json" \
-d '{"proxyBase":"https://jsonplaceholder.typicode.com","proxyRecord":true}'
First request for a path forwards to the upstream and records the 2xx response; every request after that replays the recording locally. The response headers tell you which happened β from our verification run:
$ curl -sD - https://mockbird.mockbird.workers.dev/m/<id>/todos/1
x-mockbird-proxied: jsonplaceholder.typicode.com
x-mockbird-recorded: 1 β first hit: fetched upstream + recorded
{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
$ curl -sD - https://mockbird.mockbird.workers.dev/m/<id>/todos/1
(no x-mockbird-proxied header) β second hit: replayed locally, upstream never touched
Recorded paths show up as routes you can inspect and edit β GET /api/projects/<id>/routes listed ours with "recorded": true. And because the replay is just a URL, the simulation flags work on it. Both of these ran against the recorded route above:
curl -i "https://mockbird.mockbird.workers.dev/m/<id>/todos/1?mock_status=503" # β 503
curl "https://mockbird.mockbird.workers.dev/m/<id>/todos/1?mock_delay=2000" # β measured 2.25s
That's the drill Polly can't run: replay yesterday's recording and make it fail or crawl on demand, for any client β the loading & error states guide has the full recipe set (mock_seq for scripted retry sequences, mock_chaos for probabilistic 5xx storms).
Honest scope note: Mockbird records at route granularity β method + path, replayed verbatim. Polly matches on method, URL, headers, body and request order (matchRequestsBy), which is strictly more precise for API-contract testing of request sequences. If you need order-sensitive matching, that's a genuine Polly win.
adapter-fetch if your code uses native fetch), and point integration tests, dev servers, Playwright/Cypress runs and teammates at a Mockbird base URL via an env var.GET /m/<project>/db.json exports the live dataset, GET /m/<project>/msw.js generates MSW v2 handlers with your data baked in, and openapi.json/postman.json/types.ts exports come free. No lock-in in either direction.| Polly.JS | Mockbird | |
|---|---|---|
| What it is | free OSS record/replay library (JS) | free hosted mock API service |
| Maintenance | last core release Jul 2023; repo quiet since May 2025 (verified via npm/GitHub, Aug 2026) | hosted, actively maintained |
| Reachable from | the JS process Polly runs in | anything with HTTP: curl, browser, mobile, backend, CI, agents |
| Node native fetch | escapes adapter-node-http silently (verified); works via adapter-fetch | it's a real URL β every client works by definition |
| Record & replay | core feature; order/header/body matching (more precise) | built in: proxyBase + proxyRecord, route-granularity, replayed verbatim |
| Recording format | standard HAR 1.2 (credit β and importable here) | recorded routes, editable in dashboard; exports: db.json/HAR-importable data, OpenAPI, Postman, MSW, types |
| Recordings portable across HTTP clients | no β adapter-specific identifiers; cross-adapter replay crashed (verified) | client-irrelevant β any client, any language |
| Stateful CRUD on recorded data | no β replay only | yes β HAR-imported records become real collections with writes |
| Error/latency simulation | server route API (intercept), manual | ?mock_status / ?mock_delay / ?mock_seq / ?mock_chaos on any URL, incl. recorded routes |
| Works offline | yes (replay mode) | no β it's a real network call |
| Latency | zero (in-process) | real network latency |
| Browser support | yes (xhr/fetch adapters), incl. Ember addon | yes β CORS on by default, it's just a URL |
| Request cap | none | 10,000/project/day |
Written by the Mockbird maker β bias disclosed. Where Polly.JS genuinely wins: offline, zero-latency replay with no network flakiness; automatic recording of your real API with full headers preserved; matchRequestsBy precision (method/headers/body/order) that URL-level replay can't match; browser adapters and a first-class Ember integration; standard HAR persistence (a design decision we happily exploit above); and zero request caps. If it still fits your stack and you're comfortable depending on a dormant library, replay tests that run on a plane are a real thing we can't give you. When the recording needs to be a URL β for a subprocess, another language, a teammate, a mobile client, or CI against a deployed build β that's us.
Full API reference in the docs. More guides: VCR / vcrpy alternative Β· HAR file β mock API Β· nock alternative Β· MSW alternative Β· fetch-mock alternative Β· mock API for Node.js Β· deterministic test data Β· testing loading & error states Β· free mock API tools compared. Create your API β