📖 The Story: The Flaky Checkout

A QA team had a test that passed 95% of the time but failed randomly in CI. The failure was always the same: the checkout button was clicked, but the success page never appeared. The team spent two weeks trying to reproduce it locally — it never failed on a developer’s machine.

When they added tracing, they saw the click happen. When they added a separate HAR recording, they saw the API call. But the two artifacts were in different files with different timestamps, and correlating “did the click happen before or after the API timeout?” required manual math across two tools.

The root cause turned out to be a race condition: the checkout API had a 4-second timeout, and the CI runner was slow enough that the page’s loading spinner sometimes overlapped with the API call, causing a double-submit guard to fire. The team only found this after a senior engineer manually aligned the HAR timestamps with the trace screenshots — a process that took 40 minutes per failure.

If the HAR and the trace had been one artifact, the engineer would have seen the overlap in 4 seconds.

🎯 Why It Matters

🔄

One Artifact

HAR and trace live in the same workflow. No more juggling two files with different timestamps.

Auto-Scoped

await using finalizes the HAR when the scope exits. No manual stop call, no leaked recordings.

🌐

Filtered

Same content, mode, and urlFilter options as recordHar. Capture only /api/** calls, skip static assets.

🔍

Debuggable

Open the trace in Trace Viewer and see network calls inline with DOM actions, screenshots, and console logs.

🌍 Real World Example

An e-commerce team runs a checkout test that hits 12 API endpoints. When it fails, they need to know which endpoint timed out. Before 1.60, they configured recordHar on the BrowserContext, which captured every network call including analytics pings and CDN asset fetches — the HAR file was 8 MB and took 30 seconds to open in Chrome’s HAR viewer. With tracing.startHar('trace.har', { urlFilter: '**/api/**' }), the HAR is 200 KB, contains only the 12 API calls, and opens inside the Trace Viewer alongside the DOM timeline.

🧒 Explain Like I'm 10

Imagine you’re a detective investigating a crime. You have a video camera that records what people do (the trace), and you have a microphone that records what people say (the HAR).

Before, the camera and microphone were on different clocks. When you played them back, the lip-sync was off, and you couldn’t tell if someone said “fire!” before or after the window broke.

Now, the camera and microphone are built into the same device with the same clock. When you play it back, everything lines up perfectly.

🎓 Professional Explanation

HAR (HTTP Archive) is a JSON-formatted log of all network requests a browser makes. Playwright has supported HAR recording since 1.0 via the recordHar context option, but it was configured separately from tracing. This meant network captures and DOM/action captures lived in different files with different timestamp origins, making correlation manual and error-prone.

Playwright 1.60 introduces tracing.startHar() and tracing.stopHar(), which fold HAR recording into the tracing API. They accept the same content, mode, and urlFilter options as recordHar, but the recording is scoped to a tracing context and can be used with the await using disposable pattern (requires Node 20.17+) for automatic finalization.

📊 The Flow

HAR Recording on Tracing Flow

1. Test calls tracing.startHar('trace.har', { urlFilter })
⬇️
2. Playwright starts both trace + HAR recording in the same context
⬇️
3. Test runs — DOM actions and network calls captured together
⬇️
4. await using scope exits → HAR is finalized automatically
⬇️
5. Open trace.zip in Trace Viewer — network calls appear inline

⚙️ The tracing.startHar() API

The API lives on context.tracing and returns a Disposable that finalizes the HAR when it goes out of scope. You need Node 20.17+ for the await using syntax; on older Node, call stopHar() explicitly.

typescript
import { test } from class="tk-str">'@playwright/test';

test(class="tk-str">'checkout API debugging', async ({ context }) => {
  class=class="tk-str">"tk-com">// Start HAR recording scoped to this test
  await using har = await context.tracing.startHar(class="tk-str">'trace.har', {
    urlFilter: class="tk-str">'**/api/**',
  });

  const page = await context.newPage();
  await page.goto(class="tk-str">'https:class="tk-com">//storedemo.testdino.com');
  await page.getByTestId(class="tk-str">'header-menu-all-products').click();

  class=class="tk-str">"tk-com">// HAR is finalized when `har` goes out of scope.
  await expect(page).toHaveURL(/.*products/);
});

The await using keyword (Node 20.17+) creates a Disposable. When the test function exits — normally or via exception — the HAR is automatically finalized and written to trace.har. No manual stopHar() call needed.

typescript
class=class="tk-str">"tk-com">// Filter by URL to capture only API calls (skip static assets)
await using har = await context.tracing.startHar(class="tk-str">'api.har', {
  urlFilter: class="tk-str">'**/api/**',
  mode: class="tk-str">'minimal',  class=class="tk-str">"tk-com">// omit request/response bodies for smaller files
});

class=class="tk-str">"tk-com">// Or capture everything (default)
await using fullHar = await context.tracing.startHar(class="tk-str">'full.har', {
  content: class="tk-str">'embed',  class=class="tk-str">"tk-com">// embed response bodies in the HAR
});

urlFilter accepts a glob pattern. mode: 'minimal' omits request/response bodies for smaller HAR files. content: 'embed' includes bodies inline (useful for debugging JSON API responses).

typescript
class=class="tk-str">"tk-com">// Without await using (Node 18 compatible)
test(class="tk-str">'legacy node version', async ({ context }) => {
  await context.tracing.startHar(class="tk-str">'trace.har', {
    urlFilter: class="tk-str">'**/api/**',
  });

  const page = await context.newPage();
  await page.goto(class="tk-str">'https:class="tk-com">//app.example.com');

  class=class="tk-str">"tk-com">// Manually finalize the HAR
  await context.tracing.stopHar();
});

If you’re on Node 18 and can’t use await using, call startHar() without assigning the result, and call stopHar() at the end. You lose the auto-finalize-on-exception safety, so wrap the test body in try/finally if you need cleanup on failure.

🔄 Before vs After

Before: Parallel recordHar on BrowserContext (1.59 and earlier)

typescript
class=class="tk-str">"tk-com">// playwright.config.ts (Playwright 1.59 and earlier)
export default defineConfig({
  use: {
    contextOptions: {
      recordHar: { path: class="tk-str">'network.har', urlFilter: class="tk-str">'**/api/**' },
    },
  },
});

The HAR file had no connection to your trace files. You opened the HAR in Chrome’s HAR viewer and the trace in Trace Viewer, then manually correlated timestamps. Network debugging lived in one place; DOM and action history in another.

After: tracing.startHar() with await using (1.60+)

typescript
import { test } from class="tk-str">'@playwright/test';

test(class="tk-str">'checkout API debugging', async ({ context }) => {
  await using har = await context.tracing.startHar(class="tk-str">'trace.har', {
    urlFilter: class="tk-str">'**/api/**',
  });

  const page = await context.newPage();
  await page.goto(class="tk-str">'https:class="tk-com">//storedemo.testdino.com');
  await page.getByTestId(class="tk-str">'header-menu-all-products').click();

  class=class="tk-str">"tk-com">// HAR is finalized when `har` goes out of scope.
  await expect(page).toHaveURL(/.*products/);
});

HAR and trace are now one workflow. Open trace.zip in Trace Viewer and you’ll see network calls inline with DOM actions, screenshots, and console logs — all on the same timeline. Filter with urlFilter to capture only the API calls you care about.

⚠️ Gotcha

await using requires Node 20.17 or newer. The Disposable pattern that auto-finalizes the HAR is a Node 20 feature. On Node 18, use the explicit startHar() / stopHar() form wrapped in try/finally. CI runners pinned to Node 18 LTS will need an upgrade before adopting this pattern.

⚠️ Common Mistakes

Mistake 1: Using await using on Node 18

The await using keyword is a Node 20.17+ feature. On Node 18, the test will fail with a syntax error. Either upgrade Node or use the explicit startHar() / stopHar() form.

typescript
class=class="tk-str">"tk-com">// Node 18 — will throw SyntaxError
await using har = await context.tracing.startHar(class="tk-str">'trace.har');

class=class="tk-str">"tk-com">// Node 18 compatible
await context.tracing.startHar(class="tk-str">'trace.har');
class=class="tk-str">"tk-com">// ... test body ...
await context.tracing.stopHar();

Mistake 2: Forgetting to start tracing itself

startHar() records network calls, but it does not start the trace. If you want DOM actions and screenshots in the same artifact, you also need context.tracing.start({ snapshots: true, screenshots: true }). The HAR recording layers on top of the trace.

typescript
class=class="tk-str">"tk-com">// Start tracing FIRST, then layer HAR on top
await context.tracing.start({ snapshots: true, screenshots: true });
await using har = await context.tracing.startHar(class="tk-str">'trace.har', {
  urlFilter: class="tk-str">'**/api/**',
});

Mistake 3: Not filtering the HAR

Without urlFilter, the HAR captures every network call including analytics pings, CDN asset fetches, and WebSocket handshakes. A 200 KB API-only HAR becomes an 8 MB mess that takes 30 seconds to open. Always filter: urlFilter: '**/api/**'.

✅ Best Practices

  1. Always pass urlFilter to capture only the API calls you care about. A filtered HAR is 10–40x smaller and opens instantly.
  2. Use await using (Node 20.17+) for auto-finalization — it prevents leaked recordings when tests throw.
  3. Start tracing first, then layer HAR on top. startHar() records network; start() records DOM actions. You need both for full debugging.
  4. Use mode: 'minimal' for CI runs to omit request/response bodies and keep the HAR small.
  5. Use content: 'embed' for local debugging to include response bodies inline (useful for inspecting JSON API responses).
  6. Combine with --trace=retain-on-failure so the HAR + trace is only kept for failing runs.

🏗️ Senior Engineer Deep Dive

Why HAR and trace were separate before 1.60

Playwright’s tracing system and its HAR recording system evolved independently. Tracing was built for DOM/action forensics — screenshots, click coordinates, console logs. HAR recording was bolted on via the BrowserContext’s recordHar option, which predated tracing. The two systems used different timestamp origins and different file formats, so correlating “did the API call happen before or after the click?” required manual math. tracing.startHar() unifies the timestamp origin so network calls appear on the same timeline as DOM actions in Trace Viewer.

When to use HAR tracing vs route interception

page.route() is for mocking network calls — you intercept a request and return a fake response. tracing.startHar() is for recording real network calls for later debugging. They are complementary: use route() to make tests deterministic, use startHar() to debug why a test that should pass is failing. You can combine them — route-mocked calls appear in the HAR with a note that they were fulfilled by Playwright.

💼 Interview Questions

Beginner
What does tracing.startHar() do?
Show Answer
It starts HAR (HTTP Archive) recording scoped to the tracing context. The HAR captures all network requests matching the urlFilter, and when used with await using, it auto-finalizes when the scope exits. The HAR appears inline with DOM actions in Trace Viewer.
Intermediate
What Node version is required for await using with startHar()?
Show Answer
Node 20.17 or newer. The await using keyword creates a Disposable that auto-finalizes the HAR when the scope exits. On Node 18, use the explicit startHar()/stopHar() form wrapped in try/finally.
Advanced
How does tracing.startHar() differ from the recordHar context option?
Show Answer
recordHar records network calls into a standalone HAR file with a different timestamp origin than the trace. startHar() layers HAR recording on top of the tracing API, so network calls appear on the same timeline as DOM actions in Trace Viewer. startHar() also supports the await using disposable pattern for auto-finalization.
Scenario
Your CI test fails intermittently. You have a trace file but no network visibility. How do you add HAR recording without losing the trace?
Show Answer
Use tracing.startHar('trace.har', { urlFilter: '**/api/**' }) alongside your existing tracing.start() call. The HAR layers on top of the trace, so both network calls and DOM actions appear in the same trace.zip when you open it in Trace Viewer. Filter by URL to keep the HAR small.

🏋️ Practical Exercise

🎯 Hands-On Practice medium

Your First HAR-in-Trace

  1. Ensure Node 20.17+ is installed: node --version
  2. Create a test that navigates to a public site like https://news.ycombinator.com
  3. Add await using har = await context.tracing.startHar('news.har', { urlFilter: '**/*' })
  4. Also start tracing: await context.tracing.start({ snapshots: true, screenshots: true })
  5. Click a few links, then stop tracing: await context.tracing.stop({ path: 'trace.zip' })
  6. Open trace.zip in Trace Viewer (npx playwright show-trace trace.zip) and verify network calls appear inline

🚀 Mini Project

🏗️ The API Debugging Suite

  1. Pick a flaky test in your suite that involves API calls
  2. Add tracing.startHar() with urlFilter: '**/api/**' to the test
  3. Configure --trace=retain-on-failure in playwright.config.ts so the trace is only kept on failure
  4. Run the suite 10 times until the flaky test fails
  5. Open the trace from the failing run in Trace Viewer
  6. Identify the API call that timed out or returned an unexpected status — it should take less than 30 seconds to find

📝 Quiz

Test your understanding. Click an option to check your answer.

1. What Node version is required for await using with startHar()?
A) Node 16+
B) Node 18+
C) Node 20.17+
D) Node 22+
Answer: C — await using is a Node 20.17+ feature. On older Node, use the explicit startHar()/stopHar() form.
2. Which option filters which network calls are captured in the HAR?
A) pathFilter
B) urlFilter
C) requestFilter
D) matchPattern
Answer: B — urlFilter accepts a glob pattern like '**/api/**' to capture only matching requests.
3. What does mode: 'minimal' do?
A) Captures only GET requests
B) Omits request/response bodies for smaller files
C) Limits the HAR to 100 entries
D) Disables WebSocket capture
Answer: B — mode: 'minimal' omits request and response bodies, significantly reducing HAR file size.
4. Do you still need to call tracing.start() when using startHar()?
A) No, startHar() starts tracing automatically
B) Yes, startHar() only records network, not DOM actions
C) Only if you want screenshots
D) Only on Chromium
Answer: B — startHar() records network calls only. For DOM actions and screenshots, you also need tracing.start({ snapshots: true, screenshots: true }).
5. What happens when the await using scope exits?
A) The HAR is deleted
B) The HAR is finalized and written to disk
C) The test fails
D) Nothing — you must call stopHar() manually
Answer: B — await using creates a Disposable. When the scope exits (normally or via exception), the HAR is automatically finalized.
6. How do you view the HAR alongside DOM actions?
A) Open the .har file in Chrome
B) Open the trace.zip in Trace Viewer
C) Convert it to JSON and grep
D) You cannot — they are separate
Answer: B — Open trace.zip in Trace Viewer (npx playwright show-trace) and network calls appear inline with DOM actions on the same timeline.
7. What is the benefit of urlFilter: '**/api/**'?
A) It speeds up the test
B) It captures only API calls, skipping static assets, making the HAR 10-40x smaller
C) It mocks the API responses
D) It bypasses CORS
Answer: B — Without urlFilter, the HAR captures every network call including analytics and CDN fetches. Filtering to /api/** keeps it small and focused.
8. Which Playwright version introduced tracing.startHar()?
A) 1.58
B) 1.59
C) 1.60
D) 1.61
Answer: C — tracing.startHar() and stopHar() shipped in Playwright 1.60 (May 11, 2026).
9. What does content: 'embed' do?
A) Embeds the HAR in the HTML report
B) Includes response bodies inline in the HAR
C) Embeds screenshots in the HAR
D) Embeds the trace in the test file
Answer: B — content: 'embed' includes request and response bodies inline in the HAR, useful for debugging JSON API responses.
10. How do you make startHar() work on Node 18?
A) It works as-is
B) Use startHar()/stopHar() explicitly with try/finally
C) Install a polyfill package
D) It is impossible on Node 18
Answer: B — On Node 18, skip await using and call startHar() without assigning, then call stopHar() at the end. Wrap in try/finally for cleanup on exception.

❓ FAQ

Q: Can I use tracing.startHar() with the HTML reporter?

A: Yes. The HAR is embedded in the trace.zip, and the HTML reporter links to the trace for each test. When you click a failing test in the HTML report, the trace opens in Trace Viewer with network calls visible inline.

Q: Does startHar() capture WebSocket frames?

A: Yes. Since Playwright 1.61, HAR and trace recordings include WebSocket requests. Earlier versions captured only HTTP. If you need WebSocket debugging, ensure you’re on 1.61+.

Q: Can I start and stop HAR recording multiple times in one test?

A: Yes. Each startHar() call returns a new Disposable. You can start, finalize, then start again with different urlFilters. Each recording writes to its own file. This is useful for isolating network calls per test phase (e.g., “auth phase” vs “checkout phase”).

📦 Summary

🎯 Key Takeaways

  • tracing.startHar() folds HAR recording into the tracing API — network and DOM on one timeline.
  • await using (Node 20.17+) auto-finalizes the HAR when the scope exits. No manual stop call.
  • Always pass urlFilter to keep the HAR small — '**/api/**' captures only API calls.
  • Start tracing first with tracing.start(), then layer HAR on top for full DOM + network debugging.
  • Use mode: 'minimal' for CI (smaller files) and content: 'embed' for local debugging (response bodies inline).