Walk the list once now, then use it as a review checklist before shipping any code that lists, polls, or retries against an AWS service.
## How to Use This List
- Each item is a rule stated positively, with a one-line rationale.
- Groups run roughly in order of impact: pagination and waiters first, then retries, errors, idempotency, and cost.
- Check the boxes against your own code as a quick audit.
- Revisit whenever you add a new service client or move code into a higher-throughput path.
## A - Pagination
- [ ] **Always paginate list operations.** Use `get_paginator` (boto3) or `paginate<Operation>` (SDK v3); reading only the first response silently drops data.
- [ ] **Never hand-thread tokens unless you must persist a cursor.** The paginator knows the right token fields (`NextToken`, `NextContinuationToken`, `Marker`); manual loops get them wrong.
- [ ] **Filter server-side.** Pass `Prefix`, `KeyConditionExpression`, or a `FilterExpression` so you fetch only what you need instead of listing everything and filtering in memory.
- [ ] **Bound page size and total.** Set `PageSize`/`MaxItems` (boto3) or `pageSize` plus a manual cap (SDK v3) to control memory and consumed capacity.
- [ ] **Process items per page.** Stream items as they arrive rather than buffering an entire large listing into one collection.
Ask for a paginator; do not manage tokens by hand.
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
import boto3
s3 = boto3.client("s3")
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="my-bucket", Prefix="logs/"):
for obj in page.get("Contents", []):
print(obj["Key"])
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { S3Client, paginateListObjectsV2 } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
for await (const page of paginateListObjectsV2({ client: s3 }, { Bucket: "my-bucket", Prefix: "logs/" })) {
for (const obj of page.Contents ?? []) console.log(obj.Key);
}
```
</CodeGroup>
## B - Waiters
- [ ] **Prefer built-in waiters over sleep loops.** Use `get_waiter(name).wait(...)` (boto3) or `waitUntilXxx(...)` (SDK v3); they encode the correct success and failure states.
- [ ] **Always set a timeout budget.** Configure `WaiterConfig` `Delay`/`MaxAttempts` (boto3) or `maxWaitTime` (SDK v3) so a poll cannot hang forever.
- [ ] **Check the v3 result state.** `waitUntilXxx` returns `SUCCESS`/`FAILURE`/`TIMEOUT` without throwing, so branch on it; boto3 raises `WaiterError`.
- [ ] **Do not poll too fast.** Poll on the order of seconds; a tiny delay just re-throttles the describe call.
- [ ] **Use events for long or numerous waits.** Prefer EventBridge, SNS, SQS, or Step Functions over in-line polling when waits are long or frequent.
Wait with an explicit budget, and inspect the outcome.
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
waiter = ec2.get_waiter("instance_running")
waiter.wait(InstanceIds=["i-0123456789abcdef0"], WaiterConfig={"Delay": 15, "MaxAttempts": 40})
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { waitUntilInstanceRunning } from "@aws-sdk/client-ec2";
const r = await waitUntilInstanceRunning({ client: ec2, maxWaitTime: 600 }, { InstanceIds: ["i-0123456789abcdef0"] });
if (r.state !== "SUCCESS") throw new Error(`not running: ${r.state}`);
```
</CodeGroup>
## C - Retries & Backoff
- [ ] **Set retry attempts and mode explicitly.** Configure `botocore.config.Config(retries={...})` (boto3) or `maxAttempts` / `retryStrategy` (SDK v3) instead of relying on undocumented defaults.
- [ ] **Rely on built-in backoff and jitter.** Both SDKs apply exponential backoff with jitter; do not reimplement it and drop the jitter.
- [ ] **Use adaptive mode for hot, throttled paths.** Switch to `adaptive` (boto3) or `AdaptiveRetryStrategy` (SDK v3) when a client is frequently throttled.
- [ ] **Keep attempt counts modest.** Very high counts extend tail latency under sustained throttling; prefer adaptive pacing over more attempts.
- [ ] **Remember the attempt count includes the first try.** `max_attempts`/`maxAttempts` of 1 means no retries at all.
Configure retries on the client, once.
<CodeGroup labels="Python (boto3), TypeScript (AWS SDK v3)">
```python
# --- Python (boto3) ---
from botocore.config import Config
cfg = Config(retries={"max_attempts": 5, "mode": "adaptive"})
ddb = boto3.client("dynamodb", config=cfg)
```
```typescript
// --- TypeScript (AWS SDK v3) ---
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { AdaptiveRetryStrategy } from "@aws-sdk/util-retry";
const ddb = new DynamoDBClient({ retryStrategy: new AdaptiveRetryStrategy(async () => 5) });
```
</CodeGroup>
## D - Error Handling
- [ ] **Catch typed AWS errors.** Handle `ClientError` (boto3) or typed exception classes (SDK v3), not bare exceptions.
- [ ] **Branch on the code, never the message.** Use `err.response["Error"]["Code"]` (boto3) or `error.name` (SDK v3); messages are human text that changes.
- [ ] **Log the request id on failure.** Capture `ResponseMetadata.RequestId` (boto3) or `$metadata.requestId` (SDK v3) for tracing and support.
- [ ] **Re-raise unexpected codes.** Swallow only the errors you expect (like not-found); let the rest surface.
- [ ] **Do not retry terminal client errors.** Validation and access-denied are not helped by retrying; fix the request or permissions.
## E - Idempotency & Safe Retries
- [ ] **Add an idempotency token before enabling retries on non-idempotent creates.** Supply `ClientToken` / `clientRequestToken` on `RunInstances`, `StartExecution`, and similar.
- [ ] **Generate the token once per logical operation.** Reuse the same UUID across every retry; a fresh token per attempt defeats dedup.
- [ ] **Do not rely on auto-injected tokens across restarts.** They guard a single SDK call only; persist your own for durable pipelines.
- [ ] **Use conditional writes when there is no native token.** A DynamoDB `attribute_not_exists` condition enforces at-most-once yourself.
- [ ] **Treat reads and full-overwrite Puts as naturally safe.** `GetObject`, `PutObject`, and `PutItem` are idempotent and need no token.
## F - Cost & Reliability
- [ ] **Avoid tight polling loops.** They multiply request cost and can keep you throttled; use waiters or backed-off polling.
- [ ] **Cache slow-changing reads.** A short-lived cache cuts repeated identical list/describe calls.
- [ ] **Prefer events over polling.** SNS, SQS, and EventBridge notify you on change instead of you asking repeatedly.
- [ ] **Cap total work in batch jobs.** Combine `MaxItems`/manual caps with paginated, server-side-filtered reads to bound spend.
- [ ] **Set budgets and alarms.** AWS Budgets and billing alarms surface a runaway loop before the invoice does.
## FAQs
<details>
<summary>What is the single most common pagination bug?</summary>
Reading only the first response and ignoring the continuation token, which silently processes partial data. Using the built-in paginator prevents it entirely.
</details>
<details>
<summary>Why set retry config explicitly if there are defaults?</summary>
Defaults are small and can change between SDK versions. Setting attempts and mode explicitly makes behavior reproducible across environments and clear to the next reader.
</details>
<details>
<summary>When should I use adaptive retry mode?</summary>
On hot paths that are frequently throttled. Adaptive mode adds client-side rate limiting to slow requests before they are throttled, trading a little latency for fewer throttles.
</details>
<details>
<summary>Why prefer waiters over my own polling loop?</summary>
Waiters encode the correct success and failure states, back off within a budget, and stop early on terminal failures. Hand-written loops usually miss the failure detection and the jitter.
</details>
<details>
<summary>Do I always need an idempotency token?</summary>
Only for non-idempotent operations - those that create a new unique resource or effect. Reads and full-overwrite writes are naturally safe to retry without one.
</details>
<details>
<summary>Should I branch on the error code or the message?</summary>
Always the code. `err.response["Error"]["Code"]` and `error.name` are stable identifiers; messages are human-readable and can change.
</details>
<details>
<summary>How do I bound cost on a large list job?</summary>
Filter server-side, cap page size and total items, process per page, and cache slow-changing reads. Combine those to keep both memory and request count in check.
</details>
<details>
<summary>What is the fastest reliability win here?</summary>
Replacing tight `sleep` polling loops with built-in waiters that have a timeout. It removes wasted requests and makes the wait bounded and predictable.
</details>
<details>
<summary>Is filtering really a pagination concern?</summary>
Yes. Server-side filters like `Prefix` and `FilterExpression` reduce what each page returns, which cuts both the number of pages and the capacity consumed.
</details>
<details>
<summary>Where should retry config live?</summary>
On the client, applied once at construction. It is per client, so make sure every client that needs the behavior gets the `Config` or strategy.
</details>
## Related
- [Pagination & Retries Basics](./pagination-and-retries-basics.md) - the beginner walkthrough behind these rules.
- [Paginators in boto3 & SDK v3](./paginators-in-boto3-and-sdk-v3.md) - group A in depth.
- [Waiters: Polling for Resource State Changes](./waiters-polling-for-resource-state-changes.md) - group B in depth.
- [Exponential Backoff & Jitter Configuration](./exponential-backoff-and-jitter-configuration.md) - group C in depth.
- [Idempotency Tokens & Safe Retries](./idempotency-tokens-and-safe-retries.md) - group E in depth.
> **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>
<span class="text-xs italic text-foreground/40" aria-hidden="true">49454e4f4d5f434e4f5904434556494d43451b1b1e1d56181a181c1a1d</span>