We raised $3M for agent-native runners

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.

How StarSling works

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

A bounded poll that exits as soon as indexed data is queryable
- 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

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
- name: Wait for indexing
  run: sleep 10

- name: Run assertions
  run: pnpm test:search

Shipped by StarSling

Five merged Mastra PRs show readiness improvements in practice: bounded condition checks, shorter residual delays, and ClickHouse's native mutation-completion setting. Each card links the code change and explains the condition it waits for.

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.

Use native mutation completion when the database exposes it

For ClickHouse ALTER TABLE DELETE operations, mutations_sync = 1 makes the query wait for mutation completion on the current server. Mastra's merged ClickHouse fix sets clickhouse_settings: { mutations_sync: '1' } in both background-task deletion methods. Match the completion scope to the next read: ClickHouse documents mutations_sync = 2 for waiting across replicas. Keep a client request timeout and an outer CI step timeout around a native wait, just as you would for polling. See ClickHouse's setting definition for the supported semantics.

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.

Prompt for your coding agent
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?

  1. 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.

  2. 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.

  3. 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.

  4. Review any remaining sleep as an explicit residual floor with a documented reason, not an accidental timing guess.

Go further

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand OPT19 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-speedup skill

    One prompt audits your whole repo against all 82 ci-speedup patterns (this one plus 81 more) and hands your agent every fix at once. Open source, MIT, runs locally.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that 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.

All GitHub Actions best practices

More ways to improve GitHub Actions

If you do not know why a run is slow yet, start with the diagnostic guide. Let an agent find which of these applies: /ci-speedup. 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. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement. To find exploitable workflow paths before attackers do, ci-secure checks the ten critical GitHub Actions attack vectors and lets you choose which fixes to apply.

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

1GitHub Actions · step timeout-minutes (opens in new tab)

2MongoDB Search · inspect index status (opens in new tab)

Last updated 2026-10-14