Skip to content

Retry Mechanisms

CodeceptJS provides flexible retry mechanisms to handle flaky tests. Use retries when dealing with unstable environments, network delays, or timing issues — not to mask bugs in your code.

CodeceptJS retry scopes from narrowest to widest: Helper Retries (built-in — every browser action retries for ~5s), Automatic Step Retry (the retryFailedStep plugin, enabled by default — failed steps retry on their own), Manual Step Retry (step.retry — explicit retry and timing for one flaky step), Retry a Block (retryTo around steps that must pass together), Scenario Retry (re-run the whole test on failure), Feature Retry (re-run every test in a feature), and Global Retry (config.retry — retry tests by grep across the whole run). Start with the smallest scope that fixes the flakiness. Retries — start narrow, widen the scope only if it's still flaky Per action · automatic Helper Retries — every action retries for ~5 s already helpers: { Playwright: { timeout: 5000 } } Every step · on by default Automatic Step Retry — failed steps retry on their own, no code change plugins: { retryFailedStep: { retries: 3 } } One step · explicit Manual Step Retry — explicit retry & timing for one flaky step I.click('Save', step.retry(3)) Step group Retry a Block — steps that must pass together await retryTo(() => { I.click('Load More'); I.see('New') }, 3) One test Scenario Retry — re-run the whole test on failure Scenario('checkout', { retries: 3 }, ...) Feature suite Feature Retry — re-run every test in a feature Feature('Payments', { retries: 2 }) All tests · config Global Retry — retry tests by grep across the whole run export const config = { retry: [{ grep: '@flaky', Scenario: 5 }] }

Plawright has a built-in retry mechanism for element interactions. When you call I.click('Button'), after the element is located Playwright keeps retrying until it is actionable — up to timeout (default 5s).

WebDriver has a different auto-retry option: smartWait

Even though the handle exists (from .all()), Playwright still waits for it to become visible, stable (not mid-animation), enabled, not covered by an overlay/modal, and not rerendering.

helpers: {
Playwright: {
timeout: 5000, // retry the action until the element is actionable
waitForAction: 100 // fixed pause AFTER click/doubleClick/pressKey
}
}

What each setting does:

find element (no wait — fails instantly if locator matches nothing)
→ wait up to `timeout` for it to become actionable ← timeout
→ perform action
→ sleep `waitForAction` ms ← waitForAction (settle pause, not a wait)

timeout covers the action. If the locator matches nothing yet, the step fails immediately. Use Failed Step Retries to cover that gap.

CodeceptJS retries all failed steps by default by using the retryFailedStep plugin.

plugins: {
retryFailedStep: {
enabled: true,
retries: 3
}
}

Steps matching amOnPage, wait*, send*, execute*, run*, have* are skipped by default.

When a scenario has its own retries, step retries are disabled by default (deferToScenarioRetries: true). This prevents excessive execution time:

Scenario('test', { retries: 2 }, ({ I }) => {
I.click('Button') // step retries disabled; scenario retries run instead
})

To disable step retries for a specific test:

Scenario('manual retries only', { disableRetryFailedStep: true }, ({ I }) => {
I.click('Button', step.retry(5))
})

Defaults: minTimeout: 150, factor: 1.5, maxTimeout: 10000.

See plugin reference for more options

Retries are calculated via this formula:

gap(N) = min(minTimeout × factor^(N-1), maxTimeout)

Practically if step fails it will trigger a retry with increasing delay until maxTimeout is reached:

retries: 2 => 0.15s-0.4s (150,225ms)
retries: 3 => 0.15s-0.7s (150,225,338ms)
retries: 3, minTimeout: 1000 => 1s-4.75s (1s,1.5s,2.25s)
retries: 3, minTimeout: 1000, factor: 2 => 1s-7s (1s,2s,4s)
retries: 5, minTimeout: 1000, factor: 2 => 1s-25s (1s,2s,4s,8s,10s)

Playwright timeout adds to each attempt only when the element is found:

  • Playwright.timeout: 5000
  • retries: 2, minTimeout: 1000
element not found => 0 + (1s+1s) = 2s
element found but not interactable => 3×5s + (1s+1s) = 17s

Retry a specific step known to be flaky:

import step from 'codeceptjs/steps'
Scenario('checkout', ({ I }) => {
I.amOnPage('/cart')
I.click('Proceed to Checkout', step.retry(5)) // retry up to 5 times
I.see('Payment')
})

Configure timing with exponential backoff:

I.click('Submit', step.retry({
retries: 3,
minTimeout: 1000, // wait 1 second before first retry
maxTimeout: 5000, // max 5 seconds between retries
factor: 1.5 // exponential backoff multiplier
}))

Pass 0 for infinite retries.

Retry a group of steps together as a single operation:

import { retryTo } from 'codeceptjs/effects'
await retryTo(() => {
I.click('Load More')
I.see('New Content')
}, 3)

If any step inside fails, the entire block retries. Use this for sequences that must succeed together — switching into an iframe and filling a form, for example.

Learn more: Effects

When a step fails, a healing recipe runs recovery actions and continues the test — without touching test code. With AI healing enabled:

Scenario('checkout', ({ I }) => {
I.click('Proceed to Checkout')
I.see('Payment')
})
  • I.click('Proceed to Checkout') fails — button was renamed or moved
    • failed step, error message, and page HTML are sent to an LLM
    • AI scans page elements and suggests valid replacement actions
    • CodeceptJS executes the suggestions until one succeeds
  • test continues with I.see('Payment')

Run with --ai to activate:

Terminal window
npx codeceptjs run --ai

You can also write custom recipes for non-UI failures — network errors, data glitches, UI migrations.

Learn more: Self-Healing Tests, AI Configuration

Retry an entire test when it fails:

Scenario('API integration', { retries: 3 }, ({ I }) => {
I.sendGetRequest('/api/users')
I.seeResponseCodeIs(200)
})

Retry all scenarios globally, or by grep pattern:

export const config = {
retry: [
{ Scenario: 3, grep: 'API' }, // retry scenarios containing "API" 3 times
{ Scenario: 5, grep: '@flaky' } // retry @flaky-tagged scenarios 5 times
]
}

Retry all scenarios within a feature:

Feature('Payment Processing', { retries: 2 })
Scenario('credit card payment', ({ I }) => { ... }) // retries 2 times
Scenario('paypal payment', ({ I }) => { ... }) // retries 2 times

Or target features by pattern in config:

export const config = {
retry: [
{ Feature: 3, grep: 'Integration' }
]
}

Retry Before/After hooks when they fail:

Before(({ I }) => {
I.amOnPage('/')
}).retry(2)

Set per feature:

Feature('My Suite', {
retryBefore: 2,
retryAfter: 1,
retryBeforeSuite: 3,
retryAfterSuite: 1
})

Or globally:

export const config = {
retry: [
{ BeforeSuite: 2, Before: 1, After: 1 }
]
}

When multiple retry configurations exist, higher-priority retries take precedence:

PriorityTypeDescription
HighestManual Step (step.retry())Explicit retries in test code
Automatic StepretryFailedStep plugin
Multiple Steps (retryTo)Retry groups of steps together
Scenario ConfigRetry entire scenarios
Feature ConfigRetry all scenarios in a feature
LowestHook ConfigRetry failed hooks

retryTo operates independently from step-level retries. If a step inside retryTo fails, the entire block retries.

  1. Understand helper retries first — Playwright/Puppeteer/WebDriver already retry actions internally
  2. Start with scenario retries — simpler and less likely to cause issues
  3. Use manual retries for known flaky steps — most predictable behavior
  4. Enable deferToScenarioRetries — prevents excessive retries (default)
  5. Don’t over-retry — if tests fail consistently, fix the root cause
  6. Use grep patterns — apply retries only where needed
  7. Retry timeouts, not bugs — retries handle environmental issues, not code defects
  8. Consider healing for complex recovery — see Self-Healing Tests
  • Confirm deferToScenarioRetries: true (the default)
  • Reduce retry counts
  • Use grep patterns to target specific tests
  • Add problematic steps to ignoredSteps
  1. Check configuration syntax
  2. Check the priority table — a higher-priority retry may be overriding
  3. Confirm disableRetryFailedStep: true is not set on the scenario
  4. Confirm the step isn’t in ignoredSteps

Debug with:

Terminal window
DEBUG_RETRY_PLUGIN=1 npx codeceptjs run

Import step from codeceptjs:

import step from 'codeceptjs/steps'