Replace fixed CI sleeps with bounded readiness polling
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.
AI agents open the PR
StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.
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.12
- 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 1Avoid 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.
- name: Wait for indexing
run: sleep 10
- name: Run assertions
run: pnpm test:searchShipped 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.
- Read the Mastra customer storyMerged 2026-02-28
Customer story
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.
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.
Read the Mastra customer storyView PR #13610 in mastra-ai/mastra (opens in new tab) - Read the Mastra customer storyMerged 2026-03-05
Customer story
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.
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.
Read the Mastra customer storyView PR #13808 in mastra-ai/mastra (opens in new tab) - Read the Mastra customer storyMerged 2026-03-07
Customer story
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.
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.
Read the Mastra customer storyView PR #13866 in mastra-ai/mastra (opens in new tab) - Read the Mastra customer storyMerged 2026-03-07
Customer story
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.
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.
Read the Mastra customer storyView PR #13965 in mastra-ai/mastra (opens in new tab)
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.
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.
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.
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.
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.
Verify on your repo
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) 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, numericcy.wait,setTimeoutor delay helpers at one second or longer, and Pythontime.sleepcalls.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.
Automate this with StarSling.
Install the StarSling GitHub App. It moves supported jobs onto StarSling runners, then AI agents keep inspecting your workflows and opening optimization PRs you review.
More best practices for GitHub Actions
Where to go next in the CI best-practices catalog.
- Runner & Queue
Bound GitHub Actions runner jobs with timeout-minutes
Set
timeout-minuteson every GitHub Actions job that executes on a runner, plus hang-prone steps, so a stuck run is cancelled in minutes instead of occupying a runner for up to the 360-minute default. - Runner & Queue
Cut CI queue time in GitHub Actions
GitHub-hosted runners cap how many jobs your account can run at once (20 on Free, more on paid plans), so past that ceiling jobs queue for a free slot no matter how your workflows are written.
- Parallelization
Shard tests across parallel jobs in GitHub Actions
Split one long test job into parallel shards with a matrix so the suite finishes in a fraction of the wall-clock time.
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.
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.
Sources
Last updated 2026-07-21