condition-based-waiting
Use when waiting for something async. NEVER use Start-Sleep blindly — wait on conditions.
SKILL.mdskills/condition-based-waiting/SKILL.md
GitHubCondition-Based Waiting
Time-based sleeps are a bet that the world finishes within N seconds. The bet is always wrong eventually — too short on slow machines, too long on fast ones. Wait on the condition, not the clock.
Iron Law
Do not Start-Sleep to wait for something to "be ready." Poll the actual condition, with a timeout, with backoff.
Red Flags
Start-Sleep 5after starting a server, then hitting it.Start-Sleep 60to "let things settle."- Sleeps inside retry loops with no condition check.
- Sleeps tuned to "what worked on my machine."
- Sleep duration in a comment:
# this needs to be at least 3s.
If you've written any of these, replace them now.
The Right Pattern
loop:
if condition_met(): break
if elapsed > timeout: fail("waited too long for X")
sleep(small backoff)
Three pieces — condition check, timeout guard, bounded sleep between polls.
Common Conditions and How to Wait on Them
Wait for a port to open (server starting)
$deadline = (Get-Date).AddSeconds(30)
$delay = 0.2
while ((Get-Date) -lt $deadline) {
try {
$c = New-Object System.Net.Sockets.TcpClient
$c.Connect('127.0.0.1', 8080)
$c.Close()
break
} catch {
Start-Sleep -Seconds $delay
$delay = [Math]::Min($delay * 1.5, 2.0)
}
}
if ((Get-Date) -ge $deadline) { throw "server did not open port 8080 within 30s" }
Wait for a file to exist
$deadline = (Get-Date).AddSeconds(20)
while (-not (Test-Path 'C:\path\to\sentinel.txt')) {
if ((Get-Date) -ge $deadline) { throw "sentinel.txt never appeared" }
Start-Sleep -Milliseconds 200
}
Wait for a background job
$job = Start-Job { ... }
Wait-Job $job -Timeout 60 | Out-Null
if ($job.State -ne 'Completed') { Stop-Job $job; throw "job timed out" }
Receive-Job $job
Wait-Job is event-based — no polling needed.
Wait for an HTTP endpoint to return 200
$deadline = (Get-Date).AddSeconds(30)
$delay = 0.25
while ($true) {
try {
$r = Invoke-WebRequest -Uri 'http://localhost:8080/health' -UseBasicParsing -TimeoutSec 2
if ($r.StatusCode -eq 200) { break }
} catch { }
if ((Get-Date) -ge $deadline) { throw "health never went 200" }
Start-Sleep -Seconds $delay
$delay = [Math]::Min($delay * 1.5, 2.0)
}
Wait for a process to exit
$p = Get-Process -Id $pid
$p.WaitForExit(30000) # 30s timeout, event-based
if (-not $p.HasExited) { throw "process did not exit" }
Backoff Strategy
- Start small (100-250 ms) — fast condition gets caught fast.
- Multiply by
1.5 each iteration up to a cap (2 s). - Hard timeout in the outer loop — never wait forever.
Exponential backoff balances responsiveness (quick wins detected fast) with politeness (no busy-loop hammering the dependency).
When Sleep IS Appropriate
Narrow exception: you genuinely need to delay, not wait for something. Examples:
- Rate-limiting outbound requests ("at most 10 per second").
- Animation / pacing in a UI.
- Reproducing a race-condition bug with a deterministic delay.
These are delays by design, not waits in disguise. Comment them explicitly: # delay for rate limit, not waiting on state.
MUST
- MUST have a timeout on every wait loop.
- MUST check the actual condition (port, file, response, state), not "probably ready by now."
- MUST fail loudly when timeout hits — silent timeouts hide deadlocks.
Cross-references
- [[systematic-debugging]] — race conditions found via Phase 1 reproduction usually need this pattern.
- [[verification-before-completion]] — verifying async behavior means waiting on the condition you care about.