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.

Browser automation helpers (Playwright, Puppeteer, WebDriver) have built-in retry mechanisms for element interactions. When you call I.click('Button'), Playwright automatically waits for the element to exist, be visible, stable, and enabled — retrying for up to 5 seconds.

Configure the timeout in your helper settings:

helpers: {
Playwright: {
timeout: 5000, // retry actions for up to 5 seconds
waitForAction: 100 // wait 100ms before each action
}
}

Learn more: Playwright Helper, Timeouts

When helper retries aren’t enough, CodeceptJS adds retry layers on top.

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.

Automatically retry all failed steps without modifying test code:

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))
})

Full plugin options:

OptionDefaultDescription
retriesRetries per step
minTimeoutMilliseconds before first retry
maxTimeoutInfinityMax milliseconds between retries
factorExponential backoff multiplier
randomizefalseRandomize timeout intervals
ignoredSteps[]Patterns/regex of steps to never retry
deferToScenarioRetriestrueDisable step retries when scenario retries exist
when() => trueFunction receiving error; return true to retry

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'