skip to content

A Cypress cy.task() seeding a database times out after 60 seconds - what do you do?

level: seniorimportance: should knowfreq 42%

answer

  1. Sixty seconds is a default, not a law
  2. Ask whether the work ever finishes
  3. The queue is blocked while it runs
  4. Override the narrowest scope that helps
  5. Third argument to the command takes timeout

basics

~20 s

Sixty seconds is the taskTimeout default. Diagnose first: a task that never ends, or waits on a dependency, will not be fixed by a bigger number. If the work is genuinely heavy, raise the narrowest scope, ideally the per-call timeout option.

solid answer

~40 s

The 60 seconds is `taskTimeout`, the default budget for `cy.task()`. Before changing it, ask why: Cypress does not support tasks that never end, so a task that starts a server or watches files will always time out; a task waiting on a dependency that has not finished starting is a startup-ordering problem, not a timeout problem. If the work really is heavy, override the smallest scope that helps - `cy.task(event, arg, { timeout: 120000 })` for one call, a test configuration object such as `describe('...', { taskTimeout: 120000 })` for a file, `Cypress.config('taskTimeout', ...)` for a spec, and the config file only as a last resort. Remember Cypress runs no other command while a task is outstanding, so a long task costs the whole suite.

code

javascript · 14 lines
javascript
describe('reimbursement run', { taskTimeout: 120000 }, () => {
  before(() => {
    cy.task('restoreExpenseSnapshot', 'q3-closed')
  })

  it('pays out an approved report', () => {
    cy.task('seedExpenseReport', { status: 'approved' }, { timeout: 20000 })
      .its('id')
      .then((id) => cy.visit(`/reports/${id}`))

    cy.get('[data-cy="reimburse"]').click()
    cy.get('[data-cy="status"]').should('have.text', 'Reimbursed')
  })
})

go deeper

for a junior

Know that cy.task() has its own timeout, taskTimeout, defaulting to 60000 ms, and that the command's third argument accepts a per-call timeout option.

for a middle

Explain the four override scopes and how they differ in reach, and why a task that never ends can never be rescued by a larger number.

for a senior

Show the diagnosis before the fix: distinguish a genuinely heavy seed from a dependency that was not ready, and account for the wall-clock cost across every spec that runs it.

for a principal

Own the policy that setup cost is a suite-level budget, not a per-test convenience, and that a raised global timeout is a decision to hide hangs from everyone.

## What the timeout actually is `cy.task()` is bounded by `taskTimeout`, which defaults to **60000 ms**. When the handler has not settled by then, Cypress fails the test with *"`cy.task('seedExpenseLedger')` timed out after waiting `60000ms`."* Nothing is retried: `cy.task()` runs its chained assertions once and does not retry, and the timeout is not `defaultCommandTimeout` — a task has its own budget precisely because it is doing work in Node rather than re-querying the DOM. There are four places to change it, in increasing scope: 1. `cy.task('seedExpenseLedger', payload, { timeout: 120000 })` — a per-call `options` object, which is what the third argument to `cy.task(event, arg, options)` is for. 2. A test configuration object on a block: `describe('reimbursement run', { taskTimeout: 120000 }, () => { ... })`. It applies for the duration of that suite or test and reverts afterwards. 3. `Cypress.config('taskTimeout', 120000)` inside a spec, which holds for the rest of that spec. 4. `taskTimeout` in the Cypress configuration file, which changes it for every task everywhere. ## Diagnose before you raise it A number that only fails in CI is usually telling you something about the environment, not about the number. Work through this before touching the config: - **Is the work the same size?** A local database with fifty expense reports and a CI database restored from a large snapshot are not the same job. Seed the minimum the test needs. - **Is it waiting on something, not working?** A task that opens a database connection to a service that is still starting will sit there until the timeout, then blame itself. That is a startup ordering problem. - **Does it ever end?** Cypress does not support tasks that do not end — starting a web server, watching for file changes, or anything you would normally stop by hand. The documentation calls starting a web server an anti-pattern explicitly. A never-ending task will always hit the timeout, and raising the number just makes the failure slower. - **Is it one task or many?** A `beforeEach` that fires three tasks pays each timeout separately, and the wall-clock cost lands on every test in the file. - **Is it failing or hanging?** If the handler throws, Cypress reports the error with a `From Node.js Internals` section on the stack instead of a timeout. A timeout means it never settled. ## Why a long task is expensive beyond itself Cypress will not run any other command while `cy.task()` is outstanding. The command queue is drained serially, so a ninety-second seed is ninety seconds of a test doing nothing, multiplied by however many tests and however many spec files run it. The documentation is direct about this: it does not recommend executing tasks that take a long time to exit. That is the real reason to treat a raised `taskTimeout` as a last resort rather than a fix. | symptom | likely cause | what to change | |---|---|---| | times out in CI, fine locally | larger dataset or cold dependency | seed less; wait for the dependency to be ready | | times out everywhere, always | the task never ends | restructure it; do not start servers from a task | | slow but passes | genuine heavy setup | move it to a `before` hook, or raise the per-call timeout | | fails fast with a Node stack | the handler threw | fix the handler; the timeout is not involved | ## Choosing the smallest scope that works Prefer the **narrowest** override that solves the problem. A per-call `{ timeout: 120000 }` on the one snapshot-restore task documents which step is slow and leaves every other task on the default, where a genuine hang still fails in a minute. A global `taskTimeout: 300000` in the configuration file does the opposite: it hides hangs across the whole suite and turns every stuck task into a five-minute stall in CI. - Per-call for one known-slow step. - Suite-level test configuration when a whole file shares heavy setup, such as a reimbursement run. - Global only when the default is genuinely wrong for your project as a whole. ## Restructuring beats raising Where the work is genuinely large, the useful moves are structural rather than numeric: - Run the expensive restore once in Mocha's `before` for the file instead of `beforeEach`, so the cost is paid once rather than per test. - Split one do-everything task into a cheap per-test seed and a rare heavy reset. - Have the task return early when the state it would create already exists, so reruns are cheap. - If the slow part is an external tool, spawn it inside the handler with Node's `child_process.execFileSync` and return only what the spec needs, rather than streaming output through the boundary.

  • Why is starting a dev server from a Cypress cy.task() an anti-pattern?
    Because a task must end. Cypress waits for the handler to settle and fails the test at `taskTimeout` if it does not, so a process that runs until you stop it can only ever time out. Servers, file watchers and anything you would interrupt by hand belong outside the run, started before Cypress is invoked.
  • How can you tell a Cypress task that hung from one whose handler threw?
    Read the error. A hang produces *timed out after waiting 60000ms*; a throw produces *failed with the following error* plus the handler's message, with a `From Node.js Internals` section appended to the stack. Only the first is a timeout question.

saying these in an interview costs you the question

  • Raises the global taskTimeout as the first response
  • Starts a web server from inside a task handler
  • Assumes defaultCommandTimeout governs cy.task()
  • Expects Cypress to retry a task that timed out
  • Ignores that the command queue stalls while a task runs