A waiter is the SDK's built-in poller: it calls a describe operation on a schedule until the resource reaches the state you want, then returns. This page covers the built-in waiters in both SDKs, how to tune them, and the polling anti-patterns they replace.
## Recipe
> Get the waiter for the target state, call wait with the resource id.
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
import boto3
ec2 = boto3.client("ec2")
waiter = ec2.get_waiter("instance_running")
waiter.wait(InstanceIds=["i-0123456789abcdef0"])
print("instance is running")
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { EC2Client, waitUntilInstanceRunning } from "@aws-sdk/client-ec2";
const ec2 = new EC2Client({});
await waitUntilInstanceRunning(
{ client: ec2, maxWaitTime: 300 },
{ InstanceIds: ["i-0123456789abcdef0"] },
);
console.log("instance is running");
```
</CodeGroup>
**When to reach for this:**
- You just created, started, stopped, or deleted a resource and must wait for it to settle.
- The API responded with a transitional state (`pending`, `CREATING`, `modifying`).
- The next step in your workflow needs the resource in a specific final state.
- You would otherwise write a `sleep` loop by hand.
## Working Example
Launch an instance, wait until it is running, then confirm. The waiter replaces all the polling logic.
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
import boto3
ec2 = boto3.client("ec2")
run = ec2.run_instances(
ImageId="ami-0abcdef1234567890",
InstanceType="t3.micro",
MinCount=1, MaxCount=1,
)
instance_id = run["Instances"][0]["InstanceId"]
waiter = ec2.get_waiter("instance_running")
waiter.wait(
InstanceIds=[instance_id],
WaiterConfig={"Delay": 15, "MaxAttempts": 40}, # poll every 15s, up to 10 min
)
print(f"{instance_id} is running")
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { EC2Client, RunInstancesCommand, waitUntilInstanceRunning } from "@aws-sdk/client-ec2";
const ec2 = new EC2Client({});
const run = await ec2.send(new RunInstancesCommand({
ImageId: "ami-0abcdef1234567890",
InstanceType: "t3.micro",
MinCount: 1, MaxCount: 1,
}));
const instanceId = run.Instances?.[0]?.InstanceId as string;
const result = await waitUntilInstanceRunning(
{ client: ec2, maxWaitTime: 600, minDelay: 15 }, // up to 10 min, ~15s between polls
{ InstanceIds: [instanceId] },
);
console.log(instanceId, result.state); // "SUCCESS"
```
</CodeGroup>
**What this demonstrates:**
- boto3's `WaiterConfig` sets `Delay` (seconds between polls) and `MaxAttempts` (poll count).
- SDK v3 uses `maxWaitTime` (total seconds budget) plus optional `minDelay` / `maxDelay`.
- boto3 raises `WaiterError` on timeout or a failure state; v3 returns a `state` and does not throw for `TIMEOUT`/`FAILURE` unless you check.
- The waiter polls the correct describe operation for you; you never call `DescribeInstances` yourself.
## Deep Dive
### How Waiters Work
- A waiter is defined by an acceptor list: which describe operation to call, and which response states count as success, failure, or retry.
- On each poll it runs the describe call, matches the response against the acceptors, and returns on a success acceptor or raises/returns on a failure acceptor.
- If neither matches, it sleeps for the configured delay and polls again, up to the attempt or time budget.
- Because failure states are encoded, a waiter stops early when a resource enters a terminal bad state rather than waiting out the full timeout.
### Tuning the Schedule
| boto3 `WaiterConfig` | SDK v3 config | Meaning |
|----------------------|---------------|---------|
| `Delay` | `minDelay` / `maxDelay` | Seconds between polls |
| `MaxAttempts` | `maxWaitTime` | Poll budget (count vs total seconds) |
boto3 thinks in attempts times delay; v3 thinks in a total wait-time budget with a min/max delay. Both back off within the budget.
### Handling the Outcome
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
from botocore.exceptions import WaiterError
waiter = ec2.get_waiter("instance_running")
try:
waiter.wait(InstanceIds=[instance_id], WaiterConfig={"Delay": 15, "MaxAttempts": 40})
except WaiterError as err:
print("did not reach running in time:", err)
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { waitUntilInstanceRunning } from "@aws-sdk/client-ec2";
const result = await waitUntilInstanceRunning(
{ client: ec2, maxWaitTime: 600 },
{ InstanceIds: [instanceId] },
);
if (result.state !== "SUCCESS") {
console.log("did not reach running:", result.state); // "TIMEOUT" | "FAILURE"
}
```
</CodeGroup>
## Gotchas
- **Hand-rolled sleep loops** - `while not ready: sleep(1)` hammers the API and burns request cost. **Fix:** use the built-in waiter with a sane delay.
- **Assuming success immediately after create** - the create call returns a transitional state. **Fix:** wait for the target state before using the resource.
- **No timeout budget** - an unbounded poll can hang forever if the resource never settles. **Fix:** always set `MaxAttempts` / `maxWaitTime`.
- **Ignoring the v3 result state** - `waitUntilXxx` returns `TIMEOUT`/`FAILURE` without throwing. **Fix:** check `result.state !== "SUCCESS"`.
- **Polling too fast** - a tiny `Delay` re-throttles the describe call. **Fix:** poll on the order of seconds, not milliseconds.
- **Waiting on the wrong waiter** - `instance_running` is not `instance_status_ok`. **Fix:** pick the waiter whose success state matches what you actually need.
## Alternatives
| Alternative | Use When | Don't Use When |
|-------------|----------|----------------|
| Built-in waiter | A named waiter exists for your target state | No waiter matches and the state is custom |
| Custom / manual poll with backoff | You need a state no waiter covers | A built-in waiter already fits |
| Event-driven (EventBridge, SNS, SQS) | Waits are long or many, and cost matters | You need a synchronous, in-line wait in a short script |
| Step Functions `waitForTaskToken` | Orchestrating long multi-step workflows | A single short-lived wait in application code |
## FAQs
<details>
<summary>What does a waiter actually poll?</summary>
A describe operation for the resource, such as `DescribeInstances`. The waiter matches the response against known success and failure states on each poll until one matches or the budget runs out.
</details>
<details>
<summary>How is boto3's timeout configured versus SDK v3's?</summary>
boto3 uses `WaiterConfig` with `Delay` (seconds) and `MaxAttempts` (count), so total wait is roughly their product. SDK v3 uses `maxWaitTime` in total seconds, with optional `minDelay` and `maxDelay`.
</details>
<details>
<summary>Does a waiter throw on timeout?</summary>
In boto3, yes - it raises `WaiterError`. In SDK v3, no - `waitUntilXxx` returns a result whose `state` is `TIMEOUT` or `FAILURE`, so you must check it.
</details>
<details>
<summary>Why not just sleep and re-describe myself?</summary>
Waiters encode the correct success and failure acceptors, back off within a budget, and stop early on terminal failures. A hand-written loop usually lacks the failure detection and the jitter.
</details>
<details>
<summary>Can waiters detect failure states, not just success?</summary>
Yes. Their acceptors include failure states, so a waiter stops immediately if the resource enters a terminal bad state instead of waiting the whole timeout.
</details>
<details>
<summary>What if there is no waiter for the state I need?</summary>
Write a bounded polling loop yourself: call the describe operation, check the state, sleep with backoff, and cap the total attempts. Keep the same discipline the built-in waiters use.
</details>
<details>
<summary>Are waiter polls subject to throttling?</summary>
Yes. Each poll is a real describe call using the client's retry config. Polling too aggressively can throttle you, which is why a reasonable delay matters.
</details>
<details>
<summary>Should long waits use polling at all?</summary>
For long or numerous waits, prefer an event-driven approach (EventBridge, SNS, SQS) or Step Functions, so you are notified on state change instead of repeatedly asking.
</details>
## Related
- [Why AWS SDKs Need Paginators, Waiters & Retry Logic](./why-aws-sdks-need-paginators-waiters-and-retry-logic.md) - why async settling needs waiters.
- [Paginators in boto3 & SDK v3](./paginators-in-boto3-and-sdk-v3.md) - the SDKs' other built-in loop.
- [Exponential Backoff & Jitter Configuration](./exponential-backoff-and-jitter-configuration.md) - backoff for manual polling and retries.
- [Pagination, Waiters & Retries Best Practices](./best-practices.md) - defaults to set explicitly.
> **Stack versions:** This page was written for **boto3 1.43.x** (Python 3.10+) and the **AWS SDK for JavaScript v3** (Node.js 18+).
</content>