docs/retry.md
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.
Before/After hooks on failurePlawright 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: 5000retries: 2, minTimeout: 1000element 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
I.see('Payment')Run with --ai to activate:
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:
| Priority | Type | Description |
|---|---|---|
| Highest | Manual Step (step.retry()) | Explicit retries in test code |
| Automatic Step | retryFailedStep plugin | |
Multiple Steps (retryTo) | Retry groups of steps together | |
| Scenario Config | Retry entire scenarios | |
| Feature Config | Retry all scenarios in a feature | |
| Lowest | Hook Config | Retry failed hooks |
retryTo operates independently from step-level retries. If a step inside retryTo fails, the entire block retries.
deferToScenarioRetries — prevents excessive retries (default)deferToScenarioRetries: true (the default)grep patterns to target specific testsignoredStepsdisableRetryFailedStep: true is not set on the scenarioignoredStepsDebug with:
DEBUG_RETRY_PLUGIN=1 npx codeceptjs run
step.retry() is undefinedImport step from codeceptjs:
import step from 'codeceptjs/steps'