Resource library

QA How-To

How to Use Playwright test retry annotation (2026)

Learn how playwright test retry annotation works in 2026, including retries config, scoped policies, testInfo.retry, metadata, traces, and flaky test control.

16 min read | 2,899 words

TL;DR

Configure retries through Playwright config, projects, the CLI, or test.describe.configure(). A custom retry-policy annotation can explain an exception, while testInfo.retry identifies the attempt, but neither annotation metadata nor test.slow() enables a rerun.

Key Takeaways

  • Playwright has retry configuration, but no built-in annotation that independently enables retries.
  • Use retries, --retries, or test.describe.configure() to control reruns.
  • Use declaration and runtime annotations for ownership and context, not execution control.
  • testInfo.retry is zero on the initial run and increments on each retry attempt.
  • A failed worker is replaced, so external test data must be unique or cleaned idempotently.
  • Treat a pass after retry as a flaky result that requires investigation.

The phrase playwright test retry annotation usually refers to marking or handling a test that may be retried, but Playwright does not provide a built-in @retry annotation that enables retries for one test. Retries are configured with retries, --retries, or test.describe.configure({ retries }). Annotations can document the policy, while testInfo.retry tells runtime code which attempt is executing.

That distinction matters. Treating metadata as execution control can leave a test with zero retries, while treating retries as a general cure can hide real defects. This guide shows the supported 2026 APIs, a safe configuration model, useful annotations, retry-aware cleanup, and reporting practices that expose flaky tests instead of normalizing them.

TL;DR

Goal Supported Playwright API What it does
Enable retries globally retries in playwright.config.ts Applies a maximum retry count to the run or project
Enable retries from CLI npx playwright test --retries=2 Overrides retry count for that command
Scope retries to a suite or file test.describe.configure({ retries: 2 }) Applies to tests in that describe scope
Detect a retry attempt testInfo.retry Returns the zero-based retry index
Add visible metadata annotation or testInfo.annotations.push() Documents context but does not enable retries
Capture retry diagnostics trace: 'on-first-retry' Records a trace for the first retry

Use retries as a diagnostic safety net, keep the count low, and fail CI or track debt when Playwright classifies a test as flaky.

1. What Playwright Test Retry Annotation Means in 2026

Playwright has two concepts that search results often blur: retry execution and test annotations. Retry execution reruns a failed test in a fresh worker until it passes or exhausts the configured maximum. Annotations add metadata such as an issue URL, owner, risk, or reason to the test result. An arbitrary annotation type can be named retry-policy, but the runner does not interpret it as permission to retry.

Built-in annotations include skip, fail, fixme, and slow. None is a retry switch. test.slow() triples the test timeout, which is not the same as a second attempt. test.fail() changes the expected outcome and verifies that the test fails, which is also not a retry policy.

Playwright starts with zero retries by default. When retries are enabled, a first-run pass is reported as passed. A test that fails and later passes is reported as flaky. A test that fails the first run and all allowed retries is failed. That three-way classification is valuable quality data and should remain visible in CI.

The practical answer is simple: configure retries through the runner, use annotations only to explain policy, and inspect testInfo.retry when setup genuinely must differ on later attempts. If failures are caused by ambiguous locators rather than infrastructure, the strict mode violation troubleshooting guide is a better next step than another retry.

2. Configure Retries Globally and Per Project

A production configuration often disables local retries for faster feedback and enables one or two attempts in CI, where transient infrastructure failures are more likely and diagnostic artifacts are retained.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI
    ? [['line'], ['html', { open: 'never' }]]
    : 'list',
  use: {
    baseURL: 'https://example.test',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'webkit',
      retries: process.env.CI ? 1 : 0,
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

The top-level value is inherited unless a project supplies its own retries. Project-specific counts are defensible when environments have materially different risk or cost, but unexplained differences make dashboards harder to compare. Document why WebKit, mobile, or a remote-browser project has another policy.

The CLI can set the count for an individual run:

npx playwright test --retries=2

Do not confuse retry count with total attempts. retries: 2 allows the initial run plus as many as two retry attempts, for a maximum of three executions of a failing test. Keep that cost in mind when a suite performs expensive data provisioning.

3. Scope a Retry Policy With test.describe.configure

Playwright supports retry configuration for a file or a describe group through test.describe.configure(). There is no supported test('name', { retries: 2 }, ...) details property. For one exceptional test, place it in a focused describe block and annotate the reason.

// eventual-search.spec.ts
import { test, expect } from '@playwright/test';

test.describe('eventually indexed search', {
  annotation: {
    type: 'retry-policy',
    description: 'QA-1842: search index can lag in the test environment',
  },
}, () => {
  test.describe.configure({ retries: 2 });

  test('new product becomes searchable', async ({ page }) => {
    await page.goto('/search');
    await page.getByRole('searchbox').fill('Copper Travel Mug');
    await expect(page.getByRole('link', { name: 'Copper Travel Mug' }))
      .toBeVisible();
  });
});

The annotation appears in supported reports and reporter data. The configure call changes execution. Keeping both in the same block makes the exception discoverable without inventing an API.

Use scoped retries sparingly. A special retry count can be appropriate for a known eventually consistent boundary, a controlled third-party sandbox, or a migration period with an owner and removal date. It is not appropriate for a randomly failing locator, shared test account, or missing assertion wait. Those are test design defects.

If every test in a file gets the same policy, call test.describe.configure({ retries: 2 }) at file scope. Do not call it from inside a test or hook because suite configuration is declared while tests are collected.

4. Add a Playwright Test Retry Annotation as Metadata

Custom annotations give reviewers and reporters structured context. A declaration-time annotation is known before the test starts. Runtime annotations can be pushed to testInfo.annotations after fixtures or environment checks reveal more information.

import { test, expect } from '@playwright/test';

test('payment status is eventually confirmed', {
  tag: ['@payments', '@eventual-consistency'],
  annotation: [
    { type: 'owner', description: 'payments-quality' },
    { type: 'issue', description: 'https://tracker.example.test/QA-1842' },
    { type: 'retry-policy', description: 'uses project retry setting' },
  ],
}, async ({ page }, testInfo) => {
  testInfo.annotations.push({
    type: 'attempt',
    description: String(testInfo.retry),
  });

  await page.goto('/payments/latest');
  await expect(page.getByTestId('payment-status')).toHaveText('Confirmed');
});

This code does not change the number of attempts. The project or surrounding suite must still configure retries. The runtime attempt annotation is zero on the initial run, one on the first retry, and so on.

Choose stable annotation types that reporting tools can aggregate. owner, issue, risk, and retry-policy are clearer than free-form comments. Do not put credentials, access tokens, raw customer data, or confidential URLs in annotations because reports may be uploaded as CI artifacts.

Tags are optimized for filtering, such as --grep @payments. Annotations carry richer descriptions. Test titles should describe behavior. Keeping these roles separate produces cleaner searches and reports.

5. Use testInfo.retry Safely at Runtime

testInfo.retry is the number of the current retry. The first execution has value 0, the first retry has 1, and the second retry has 2. Check it when a later attempt needs extra cleanup or diagnostics, not to make assertions easier.

import { test, expect } from '@playwright/test';

test('updates a customer preference', async ({ page, request }, testInfo) => {
  if (testInfo.retry > 0) {
    const reset = await request.post('/test-api/reset-preferences', {
      data: { customerId: 'qa-customer-17' },
    });
    expect(reset.ok()).toBeTruthy();

    testInfo.annotations.push({
      type: 'retry-action',
      description: 'reset customer preferences before this attempt',
    });
  }

  await page.goto('/customers/qa-customer-17/preferences');
  await page.getByLabel('Email summaries').check();
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByRole('status')).toHaveText('Preferences saved');
});

The reset endpoint is application-specific, but the Playwright APIs and testInfo.retry use are valid. A better long-term design provisions unique customer data for every attempt, eliminating the need for conditional cleanup. The sample is appropriate when a controlled test API owns a fixed sandbox record.

Never write if (testInfo.retry) { skip the assertion } or loosen an expected value after failure. Every attempt must verify the same behavior. Retry-aware code may restore isolation, increase observability, or select a fresh resource. It must not change the product requirement being tested.

Remember that Playwright discards a failed worker and starts a new one. Module-level memory, beforeAll state, browser instances, and worker fixtures are recreated. Persist only the information intentionally stored outside the worker.

6. Understand Worker Restart and Test Isolation

When a test fails, Playwright shuts down its worker process to prevent contaminated state from affecting later work. With retries enabled, a new worker starts, relevant beforeAll hooks run again, and the failed test is attempted again. This is why retries work best with independent tests.

State or lifecycle element What happens after a failure
Built-in page and context Discarded with the failed worker
Worker-scoped fixture Torn down, then created in the new worker
beforeAll Runs again in the new worker
External database record Remains unless your suite cleans it
Module variable Reinitialized in the new process
Retry index Increments for the failed test

External state is the main hazard. If the first attempt creates an order and fails before cleanup, the next attempt might find a duplicate. Solve this with unique identifiers, idempotent setup, and teardown that tolerates partial work. Avoid relying on in-memory flags to record that setup happened because the new worker cannot see them.

Serial groups have different cost and behavior. If a test in a serial group fails, later tests are skipped and the complete group is retried together. Playwright documentation recommends isolated tests in most cases. A checkout flow split across dependent tests may appear readable, but a single independent test with well-named Playwright test.step usage usually produces safer retries and equally useful reporting.

7. Capture Traces and Artifacts on Retry

Retries are most valuable when they produce evidence. trace: 'on-first-retry' records the first retry, letting Trace Viewer show DOM snapshots, network activity, console events, actions, and test steps for that attempt. It is a practical CI default because it avoids tracing every passing test.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure-and-retries',
  },
});

Artifact modes answer different questions. A screenshot gives one visual state. Video shows the user-visible sequence but offers less technical detail. A trace supports action-by-action inspection and is usually the strongest artifact for a flaky browser test. Retention costs vary by suite size, so choose a policy based on actual CI storage and triage needs rather than saving everything forever.

If the initial failure is important, a mode such as retain-on-failure-and-retries for traces can preserve failed attempts as well as retries in current Playwright configurations. Confirm the chosen mode in your installed version when maintaining an older repository. Keep examples version-agnostic if the suite cannot upgrade.

Attach application-specific evidence through testInfo.attach(), such as a sanitized API response or correlation ID. Do not attach an entire database dump. Focused evidence shortens triage and reduces security risk.

8. Distinguish Retries From Auto-Waiting and Assertion Retries

There are three different retry layers in common Playwright conversations:

Layer Example Scope Worker restart?
Action auto-waiting locator.click() waits for actionability One action No
Assertion retrying await expect(locator).toHaveText(...) One assertion until its timeout No
Test retry retries: 2 Entire failed test Yes

Use the narrowest mechanism that matches the problem. When a status changes asynchronously, a web-first locator assertion should wait for the expected value. Retrying the entire test repeats login, navigation, and data creation, which is slower and can add side effects.

import { test, expect } from '@playwright/test';

test('job reaches completed state', async ({ page }) => {
  await page.goto('/jobs/qa-run-42');
  await expect(page.getByTestId('job-status')).toHaveText('Completed', {
    timeout: 20_000,
  });
});

Do not replace the assertion with a manual loop and fixed waits. Playwright's web-first assertion re-resolves the locator and waits within a bounded timeout. If the test exceeds its overall timeout, investigate the Playwright timeout 30000ms exceeded fixes instead of raising retries automatically.

Test retries are justified for intermittent failures that remain after synchronization, isolation, and environment controls are sound. Even then, a flaky classification is a signal to investigate.

9. Report and Govern Flaky Outcomes

A retry policy is incomplete without governance. Playwright reports a test that failed initially and passed later as flaky. Teams should surface that category in CI summaries, trend it by owner, and set a threshold appropriate to release risk. A green job containing many flaky tests is not equivalent to a clean pass.

A custom reporter can observe the retry index and status without changing execution:

// retry-reporter.ts
import type {
  Reporter,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

export default class RetryReporter implements Reporter {
  onTestEnd(test: TestCase, result: TestResult) {
    if (result.retry > 0) {
      console.log(
        `[retry ${result.retry}] ${test.title}: ${result.status}`,
      );
    }
  }
}

Configure it alongside a standard reporter, or let your CI parse JSON reporter output. The reporter should not decide whether a test passes. It observes results and sends metrics to the approved destination.

Good governance records the test identity, owning team, first-failure signature, retry outcome, browser project, and issue link. Grouping by error signature prevents one infrastructure outage from being counted as hundreds of unrelated test defects. Define an expiration for scoped retry exceptions. Once the underlying issue is fixed, remove both the annotation and the elevated count.

Release criteria should state how flaky results affect a decision. One team may block a critical checkout test after any first-attempt failure, while another may permit a flaky noncritical visual check but require a ticket. The important property is that the rule is explicit and risk-based. A reporter can supply data, but it should not silently reinterpret a flaky outcome as a clean pass.

Review trends at both suite and test level. A stable overall rate can hide one test that fails half its runs, while a short infrastructure incident can make many healthy tests fail once. Compare error signatures, projects, workers, and time windows before choosing remediation. Fixing the shared environment may be more valuable than editing dozens of tests, and fixing one unstable locator may be more valuable than changing the global retry count.

10. A Safe Playwright Test Retry Annotation Workflow

Use this decision sequence when a test fails intermittently:

  1. Reproduce with the same project, data, and concurrency settings.
  2. Inspect the first failure, not only the passing retry.
  3. Replace fixed sleeps with locator actions and web-first assertions.
  4. Remove shared mutable state and generate unique test data.
  5. Confirm that external services have explicit readiness checks and bounded timeouts.
  6. Enable a small CI retry count if residual infrastructure risk remains.
  7. Add an owner and issue annotation for any exceptional scoped policy.
  8. Capture a trace on the first retry and retain the first failure when needed.
  9. Track flaky classifications and remove the exception after the cause is fixed.

This workflow keeps retry configuration honest. The annotation is evidence and ownership metadata, not a magic decorator. The actual execution setting remains visible in configuration or the surrounding describe block.

Add a removal condition to the workflow, not only a creation condition. For example, require the affected test to complete a representative number of clean CI runs under normal parallel load before deleting a temporary exception. The number should come from team risk policy, not from a universal rule. Record the decision in the linked issue so a passing week does not erase the investigation history.

Also compare the same test across projects. A failure isolated to one browser can point to a product compatibility defect, while failures concentrated on one CI executor can suggest capacity, network, or service health. Do not copy a browser-specific retry value to every project until the evidence shows the risk is shared. An annotation can record the observed scope, but the configuration should enforce the narrowest justified policy.

Finally, make retry changes reviewable. A pull request that increases retries should include the first-failure evidence, expected execution-cost impact, owner, and planned follow-up. This prevents an innocent configuration change from doubling or tripling the worst-case duration of a large shard. Retry governance is part of test architecture because it changes both confidence and resource use.

For suites that use custom setup, review fixture idempotency with Playwright fixture override examples. A retry creates new fixture lifecycles, so fragile provisioning often appears as a second, different failure.

Interview Questions and Answers

Q: Does Playwright have a built-in retry annotation?

No. Retries are enabled with the config or project retries property, the --retries CLI option, or test.describe.configure({ retries }). An annotation can document a retry policy, but it does not change execution.

Q: What does testInfo.retry return?

It returns the zero-based retry index for the current test execution. The initial attempt is zero, the first retry is one, and later retries increment from there. It is available in tests, hooks, and fixtures that receive TestInfo.

Q: How many times can a test run with retries: 2?

It can execute up to three times: one initial attempt and two retries. Playwright stops early as soon as an attempt passes. A pass after an earlier failure is classified as flaky.

Q: What happens to the worker after a test failure?

Playwright discards the worker process and its browser, then starts a fresh worker. With retries enabled, the failed test runs again after applicable setup. External state persists unless the test system cleans or replaces it.

Q: Should a retry attempt use weaker assertions?

No. Every attempt must validate the same requirement. Retry-aware code may restore isolation or collect additional diagnostics, but changing expected behavior on a later attempt hides defects.

Q: What is the difference between assertion retrying and test retrying?

A web-first assertion polls for its condition within the current test and worker. A test retry reruns the entire failed test in a new worker. Prefer the assertion layer for asynchronous UI state and reserve full retries for residual intermittent failures.

Q: Why use trace: 'on-first-retry'?

It captures detailed evidence for the first retry without tracing every normal pass. Trace Viewer can reveal actions, DOM snapshots, network activity, and step structure. Teams may also retain the initial failure if their artifact policy requires it.

Common Mistakes

  • Adding { annotation: { type: 'retry' } } and assuming it enables retries.
  • Using test.slow() as a retry mechanism. It changes timeout, not attempt count.
  • Setting a large global retry count so CI appears green despite widespread flaky outcomes.
  • Cleaning state only in module memory, which disappears when the failed worker is replaced.
  • Skipping assertions or accepting alternate results when testInfo.retry > 0.
  • Retrying a fixed sleep instead of using web-first assertions and observable readiness signals.
  • Ignoring the first failure because a later attempt passed.
  • Sharing an account across parallel tests and treating collisions as unavoidable flakiness.
  • Storing secrets or customer data in annotations and retained artifacts.
  • Leaving scoped retry exceptions without an owner, issue, or removal condition.

Conclusion

The correct Playwright test retry annotation pattern separates metadata from behavior. Configure retry attempts with retries, --retries, or test.describe.configure(). Use declaration or runtime annotations to explain ownership and exceptions, and use testInfo.retry only for isolation or diagnostics that preserve the same assertion contract.

Adopt one small CI retry allowance, capture a first-retry trace, and treat every flaky classification as work to triage. Retries should make failures observable and runs resilient, never make instability invisible.

Interview Questions and Answers

How are retries enabled in Playwright Test?

They are enabled through the retries property at config or project level, through --retries on the command line, or with test.describe.configure() for a file or group. Retries are disabled by default. A custom annotation does not change the count.

How would you explain testInfo.retry in an interview?

It is the current zero-based retry index. Zero identifies the initial execution, and positive values identify later attempts. I use it for safe cleanup or extra diagnostics, never to weaken the assertion contract.

What happens after a Playwright test fails?

Playwright discards the worker process and browser to prevent contamination. If retries are enabled, a new worker performs applicable setup and reruns the failed test. External database or service state remains unless the automation cleans or replaces it.

What is the difference between an annotation and a tag?

A tag is optimized for filtering and begins with an at sign. An annotation carries a type and optional description for richer report context. Neither arbitrary metadata mechanism configures retries.

How do test retries differ from assertion retries?

A web-first assertion retries its condition inside the current test and worker until its timeout. A test retry reruns the entire failed test in a fresh worker. I choose the narrowest layer that matches the asynchronous boundary.

How would you govern flaky tests in CI?

I would preserve the first failure, capture a retry trace, report flaky outcomes separately, and assign an owner and issue. Scoped retry exceptions would have a removal condition. Passing on retry would not erase the instability signal.

Why can a large retry count be harmful?

It increases worst-case runtime, setup calls, data creation, and artifact storage. It can also make unstable suites appear healthy. A small count with visible flaky reporting is safer than repeated attempts without ownership.

Frequently Asked Questions

Is there a retry annotation in Playwright?

There is no built-in annotation that enables retries. You can add a custom annotation such as retry-policy for reporting, but the retry count must come from configuration, the CLI, or test.describe.configure().

How do I retry one test in Playwright?

Place the test in a dedicated test.describe() block and call test.describe.configure({ retries: n }) in that scope. Add an owner and issue annotation if the exception is temporary.

What does testInfo.retry mean?

It is the zero-based retry index for the current execution. The initial attempt is 0, the first retry is 1, and the second retry is 2.

Does retries 2 mean two or three total attempts?

It allows up to three total executions: the initial attempt plus two retries. Playwright stops as soon as an attempt passes.

Does test.slow retry a Playwright test?

No. test.slow() triples the test timeout and adds slow-test semantics. It does not create another test attempt.

What trace setting is useful with retries?

trace: on-first-retry is a practical CI setting because it records detailed evidence for the first retry without tracing every normal pass. Choose a broader retention mode when the initial failure must also be preserved.

Should CI pass when a test succeeds on retry?

Playwright reports that test as flaky rather than a clean first-attempt pass. A team can keep the run usable while still tracking, owning, and remediating the flaky outcome according to release policy.

Related Guides