---
title: "Replace fixed CI sleeps with readiness polling"
description: "Replace fixed sleeps in CI tests with bounded readiness polling that exits as soon as the exact condition is ready and fails with useful diagnostics."
url: https://starsling.dev/best-practices/github-actions/replace-fixed-sleeps-with-polling
canonicalUrl: https://starsling.dev/best-practices/github-actions/replace-fixed-sleeps-with-polling
---

# Replace fixed CI sleeps with bounded readiness polling

Best practice: Runner & Queue. Last updated: 2026-07-21

Poll the exact condition a test needs, stop as soon as it is true, and cap the wait with a diagnostic failure so fast runs do not pay a worst-case sleep and slow runs do not become flaky.

## Table of contents

- [Do this: poll the exact readiness condition](#do-this)
- [Avoid this: guess how long readiness takes](#avoid-this)
- [Shipped by StarSling](#shipped-by-starsling)
- [Poll the condition the assertion needs](#poll-the-condition-the-assertion-needs)
- [Bound the wait and explain failure](#bound-and-explain-the-wait)
- [Keep a residual sleep only when readiness is unobservable](#residual-sleep)
- [Why it matters](#why-it-matters)
- [When to replace a fixed sleep](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this: poll the exact readiness condition

A test or CI step checks the state its next assertion actually depends on, such as an index reporting ready, the expected records becoming queryable, or a service responding to its native ping. The loop has a short interval, a hard deadline, and a failure message with a sanitized summary of the last observed state. It exits immediately when ready, while a separate GitHub Actions step timeout provides a second outer bound.

_A bounded poll that exits as soon as indexed data is queryable_

```yaml
- name: Wait for indexed data
  timeout-minutes: 2
  run: |
    deadline=$((SECONDS + 55))
    last_status="probe not run"

    while (( SECONDS < deadline )); do
      if pnpm test:search-ready -- --expect-count 4; then
        exit 0
      fi
      probe_status=$?
      last_status="readiness probe exited ${probe_status}"
      sleep 2
    done

    echo "::error::Indexed data was not queryable after 55 seconds; ${last_status}"
    exit 1
```

<a id="avoid-this"></a>

## Avoid this: guess how long readiness takes

Every fast run pays the full delay, while any run that needs longer than the guessed duration still fails intermittently.

_A fixed delay that is both wasteful and still unbounded by reality_

```yaml
- name: Wait for indexing
  run: sleep 10

- name: Run assertions
  run: pnpm test:search
```

<a id="shipped-by-starsling"></a>

## Shipped by StarSling

Across four merged Mastra PRs, StarSling agents replaced observable readiness delays with bounded condition checks and reduced the remaining fixed floors where the changed state was not directly observable. The cards call out those residual sleeps rather than presenting the work as a complete elimination.

### mastra-ai/mastra#13610: test(mongodb): poll query results instead of waiting five seconds

StarSling added a bounded helper that polls MongoDB vector-query results every 200ms for up to 10 seconds and exits when the expected records are visible.

Diff: stores/mongodb/src/vector/index.test.ts replaces a fixed five-second indexing wait with waitForCondition/waitForSync and also parallelizes independent index creation.

An update path still kept a two-second floor because the expected result count did not change, so that state was not directly observable by the count probe.

Source: [mastra-ai/mastra#13610](https://github.com/mastra-ai/mastra/pull/13610), merged 2026-02-28.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

### mastra-ai/mastra#13808: test(mongodb): replace the remaining indexing delay with polling

StarSling changed the shared MongoDB indexing helper to poll the exact query-result count every 200ms with a 10-second deadline.

Diff: stores/mongodb/src/vector/index.test.ts replaces waitForIndexing's fixed sleep with a bounded query-count loop.

The helper retained a one-second minimum floor when the count matched immediately, and its timeout deferred the final failure to the following assertion instead of throwing inside the helper.

Source: [mastra-ai/mastra#13808](https://github.com/mastra-ai/mastra/pull/13808), merged 2026-03-05.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

### mastra-ai/mastra#13866: test(chroma): add a container port healthcheck and cut indexing floors

StarSling added a Chroma TCP-port healthcheck and made Docker Compose wait for it, replacing the unconditional startup delay before the tests begin.

Diff: docker-compose.yml adds the Chroma healthcheck, the test script uses docker compose up --wait, and three fixed indexing sleeps fall from two seconds to 200ms.

The healthcheck proves that Chroma accepts a TCP connection, not that indexed data is queryable. This PR also reduced three indexing sleeps rather than replacing them with condition polling; container startup readiness is covered separately in the Docker CI guide.

Source: [mastra-ai/mastra#13866](https://github.com/mastra-ai/mastra/pull/13866), merged 2026-03-07.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

### mastra-ai/mastra#13965: test(couchbase): wait on the native Search ping

StarSling replaced Couchbase's initial fixed 10-second readiness wait with a bounded SDK ping for the Search service that throws after 30 unsuccessful attempts.

Diff: stores/couchbase/src/vector/index.test.ts adds waitForFtsReady using bucket.ping({ serviceTypes: [ServiceType.Search] }) at 500ms intervals.

Other five-second sleeps in the suite were reduced to one second rather than eliminated, so the merged change is a mixed readiness improvement, not a sleep-free suite.

Source: [mastra-ai/mastra#13965](https://github.com/mastra-ai/mastra/pull/13965), merged 2026-03-07.
Customer story: [Mastra](https://starsling.dev/customers/mastra).

<a id="poll-the-condition-the-assertion-needs"></a>

## Poll the condition the assertion needs

A listening port proves only that a process accepted a connection; it does not prove a search index is queryable or that asynchronously written records are visible. Prefer the provider's native readiness API when it represents the needed state. Otherwise run the smallest read that proves the next assertion can succeed, such as checking an expected result count.

<a id="bound-and-explain-the-wait"></a>

## Bound the wait and explain failure

Cap attempts or elapsed time, use a deliberate interval, and fail nonzero when the deadline expires. Print the condition, deadline, and a sanitized status summary so a timeout is actionable, but never log response bodies, headers, tokens, secrets, or personal data. Keep `timeout-minutes` on the GitHub Actions step as an outer safety net in case the probe itself hangs.

<a id="residual-sleep"></a>

## Keep a residual sleep only when readiness is unobservable

Sometimes an update does not change a count or expose a distinct provider status. A short documented minimum delay can be honest in that narrow path, but it should remain bounded and separate from the observable readiness poll. Do not claim the fixed wait is eliminated when a residual floor remains.

<a id="why-it-matters"></a>

## Why it matters

Fixed sleeps encode a timing guess instead of a correctness condition. Polling turns the same wait into a fast path when the system is ready, a tolerant path when it is slow, and a bounded diagnostic failure when it never becomes ready.

<a id="when-to-use"></a>

## When to replace a fixed sleep

**Use it when:** Use bounded polling for eventually consistent database writes, asynchronous search indexes, background services, and UI state where the next operation has a concrete observable prerequisite.

**Be careful when:** Do not add a custom poll when the operation is synchronous or the tool already offers a native waiter. For Docker service startup, prefer a container healthcheck and `docker compose up --wait`; for rate-limited external APIs, honor `Retry-After` and use backoff instead of a tight fixed interval.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Audit this repository for fixed sleeps in tests and CI-adjacent source: Playwright waitForTimeout, numeric Cypress waits, setTimeout/sleep/delay helpers of 1000ms or more, Python time.sleep, and hand-written readiness delays. For each match, inspect the next assertion and identify the exact observable condition it needs. Replace the fixed delay with the provider's native waiter or a bounded polling helper that exits immediately when ready, caps attempts or elapsed time, and fails nonzero with a sanitized summary of the last observed state when the deadline expires. Never print response bodies, headers, tokens, secrets, or personal data to CI logs. Do not substitute a weak proxy such as an open port when the assertion needs indexed or queryable data. Keep or introduce a short residual floor only when the changed state is genuinely unobservable, document why, and report it honestly. Add or preserve a GitHub Actions timeout-minutes outer bound where the poll runs in CI. Test fast-ready, slow-ready, and never-ready behavior, show the diff and remaining sleeps, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepstimeout-minutes, https://www.mongodb.com/docs/manual/reference/operator/aggregation/listsearchindexes/. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- Search test and integration source for fixed waits such as `waitForTimeout`, numeric `cy.wait`, `setTimeout` or delay helpers at one second or longer, and Python `time.sleep` calls.
- For each wait, identify the exact state the next assertion requires and confirm the replacement probe checks that state rather than a weaker proxy such as an open port.
- Exercise a fast-ready path, a deliberately slow-ready path, and a never-ready path; confirm the first exits early, the second succeeds within the deadline, and the third fails on time with useful diagnostics.
- Review any remaining sleep as an explicit residual floor with a documented reason, not an accidental timing guess.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Bound GitHub Actions runner jobs with timeout-minutes](https://starsling.dev/best-practices/github-actions/bound-job-timeouts)
- [Cut CI queue time in GitHub Actions](https://starsling.dev/best-practices/github-actions/cut-queue-time)
- [Shard tests across parallel jobs in GitHub Actions](https://starsling.dev/best-practices/github-actions/shard-tests)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)

<a id="faq"></a>

## FAQ

### Why is polling better than a fixed sleep in CI?

Polling exits as soon as the required state is ready, tolerates legitimate slow runs up to a clear deadline, and can report what never became ready. A fixed sleep always pays the full delay and still fails when reality takes longer than the guess.

### What should a readiness poll check?

Check the narrowest observable condition the next assertion depends on: a provider readiness status, native service ping, expected query result, visible UI state, or completed background job. An open port is not enough when the test needs indexed data.

### How often should a CI readiness loop poll?

Choose an interval that is short relative to the expected wait but gentle on the service. Local services often tolerate hundreds of milliseconds to a few seconds. External APIs may require exponential backoff, jitter, and honoring Retry-After.

### Is a short fixed sleep ever acceptable?

Only when the relevant state is genuinely unobservable or a provider contract requires a minimum delay. Keep that floor short and bounded, document the reason, and report it as a residual sleep rather than claiming it was removed.

<a id="sources"></a>

## Sources

1. [GitHub Actions - step timeout-minutes](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepstimeout-minutes)
2. [MongoDB Search - inspect index status](https://www.mongodb.com/docs/manual/reference/operator/aggregation/listsearchindexes/)
