Record a video of a Playwright browser session

When an automated run fails on a page you cannot watch, the fastest way to find out what actually happened is to record it. Playwright can write a video of the whole session to disk, one file per page, and play it back frame by frame afterwards.

This page shows the two-line launch plus the recording call, what the file contains, how it behaves in headless mode, and the one caveat that matters most: the video is an observer-side artifact. It records what your automation saw, for your own review. It does not change one byte of what the site received, and it is not free, so you scope it to the runs you are actually debugging.

Turn on recording with record_video_dir

Recording is a property of the browser context, not of the page. You pass record_video_dir to new_context, and every page opened inside that context is recorded to its own .webm file. The files are finalized when the context closes, so the important line is context.close(): without it, the video may be truncated or missing.

from invisible_playwright import InvisiblePlaywright

with InvisiblePlaywright(seed=42) as browser:
    context = browser.new_context(record_video_dir="videos/")
    page = context.new_page()

    page.goto("https://example.com")
    page.click("#submit")

    context.close()   # the .webm for this session is finalized here

InvisiblePlaywright(seed=42) returns a real Playwright Browser, so new_context(record_video_dir=...) is the same documented call you would make on stock Playwright. Nothing about recording is special to this project; you are using the standard API on a browser that happens to be a patched Firefox. Passing a fixed seed means the run you record is reproducible: the same seed gives the same fingerprint, so a recorded failure can be replayed rather than guessed at.

Find the file each page wrote

One page produces one file. To get its path, read page.video.path() after the context is closed. The filename is generated by Playwright, so ask the object rather than assuming a name:

from invisible_playwright import InvisiblePlaywright

with InvisiblePlaywright(seed=42) as browser:
    context = browser.new_context(record_video_dir="videos/")
    page = context.new_page()
    page.goto("https://example.com")

    video = page.video
    context.close()          # flush the .webm to disk
    print("saved:", video.path())

If you open several pages in the same context, each gets its own .webm. If you want one video per logical session, open one context per session and close it when that session ends.

It records the same way headless

The recording path does not depend on a visible window. On the patched Firefox in headless=True mode, the video is written exactly as it is with a window on screen, which is the mode most automation actually runs in. That is the point of recording in the first place: on a server you cannot watch the browser, so the .webm is your only view of what the run did.

from invisible_playwright import InvisiblePlaywright

# headless is the default; shown here to be explicit
with InvisiblePlaywright(seed=42, headless=True) as browser:
    context = browser.new_context(record_video_dir="videos/")
    page = context.new_page()
    page.goto("https://example.com")
    context.close()

If a run behaves differently headless than it does with a window, the video is where you see it, and it pairs well with why headless mode gets detected less than people think when you are trying to work out whether the difference is the mode or the machine.

Optionally fix the frame size

By default the video matches the context viewport. You can set an explicit size with record_video_size, which is useful when you want every recording at the same resolution regardless of the viewport a run happens to use:

context = browser.new_context(
    record_video_dir="videos/",
    record_video_size={"width": 1280, "height": 720},
)

Keep the recording size sensible. A larger frame is more pixels to encode on every frame, and that cost lands on the same machine that is running the browser.

The caveat: it is for you, and it costs I/O

Two things are easy to get wrong about session video, and both come from forgetting whose side of the glass the recording is on.

It changes nothing site-side. The .webm is produced by your Playwright process from frames the browser renders locally. The site never sees it and never knows it exists. Recording a session is not a stealth feature and does not improve or harm how a run reads to a detector. What makes a run read as a real browser is the engine itself: the fingerprint, the TLS handshake and the driver layer present as a genuine Firefox, which is why most in-page and driver-layer checks pass. Recording sits entirely outside that. If you want to measure what the site actually sees, that is a different exercise: see how to test bot detection without a false pass.

It costs disk and CPU. Every frame is encoded and written while the browser runs, on the same host, competing for the same resources. On a busy server or a large fleet that overhead is real, and it can slow a run enough to matter for a tight launch and navigation budget. So scope recording to the runs you are debugging. Turn it on for the failing job, read the video, and turn it back off for the bulk of the fleet rather than recording everything by default.

And the honest boundary that applies to the whole product, not just this feature: a browser that looks real fixes the fingerprint, TLS and driver layers. It does not fix IP reputation, per-account quotas, rate limits, or behaviour and timing. Those you supply yourself, with a clean exit and human pacing. A recording will often show you exactly which of those is the real problem, because you can watch the point where the page turned against the run. Working the rest of the causes in order is the detection checklist.

Conclusion

Recording a Playwright session is a two-line addition: pass record_video_dir to new_context, close the context to finalize the file, and read page.video.path() to find it. It behaves identically on the patched Firefox in headless mode, which is where you need it most. Treat it as what it is: a private replay of what your automation saw, paid for in disk and CPU, that changes nothing about what the site received. Use it to find the failing moment, then turn it off for the runs that are already working.

Short answers to the questions that lead here

How do I record a Playwright session as a video? Pass record_video_dir="videos/" to browser.new_context(...), run your steps, then call context.close(). Each page in that context is written to its own .webm.

Where is the file saved? In the directory you passed, under a Playwright-generated name. Read page.video.path() after the context closes to get the exact path.

Does recording work in headless mode? Yes. The video is written the same way with or without a visible window, which is the whole reason to use it on a server.

Does recording a session help me avoid detection? No. The video is produced on your side from local frames; the site never sees it. What reads as a real browser is the engine’s fingerprint, TLS and driver layers, not anything about recording.

Why is my video empty or truncated? Almost always because the context was not closed. The file is finalized on context.close(); end the context cleanly before you read the path.

Is it expensive to leave on? Yes enough to care. Every frame is encoded and written on the same host as the browser, so scope recording to the runs you are debugging rather than the whole fleet.

Sources

  • Playwright’s own record_video_dir / record_video_size context options and Video.path(), used unchanged on this project’s patched Firefox.
  • This project’s own headless runs, where the .webm is written identically with and without a visible window, and the launch-budget notes on the encoding overhead.

See also: the detection checklist for working out what a recorded failure is really about, how to test bot detection without a false pass for measuring what the site sees rather than what you saw, and when a screenshot comes back as noise for the still-image cousin of this problem.


Written while maintaining invisible_playwright, a Firefox patched at the C++ level driven by stock Playwright. The recording is an honest mirror of what the automation saw; it is not a disguise, and it does not pretend to be one.


Back to top

MIT licensed. Every page here is written against the current source of the thing it describes, and several record something we got wrong first.

This site uses Just the Docs, a documentation theme for Jekyll.