Polling is the default pattern developers reach for when they need to know if an async job has finished. Create a task, then loop: check status, wait, check again. It works, but the costs accumulate in ways that stay hidden until you scale.
This guide quantifies the real cost of polling and shows when to use cheaper alternatives.
Step 1: Calculate the True Cost of Polling
Consider a polling loop that checks every 5 seconds:
const checkStatus = async (taskId) => { while (true) { const task = await aq.tasks.get(taskId); if (['completed', 'failed', 'timeout'].includes(task.status)) { return task; } await sleep(5000); }};For a single task that takes 60 seconds to complete:
- 12 API calls (one every 5 seconds)
- 11 of those calls returned “still processing” and produced no value
Scale that to your workload:
| Daily Tasks | Avg Completion Time | Poll Interval | Wasted API Calls/Day |
|---|---|---|---|
| 100 | 30 seconds | 5s | 500 |
| 1,000 | 60 seconds | 5s | 11,000 |
| 10,000 | 2 minutes | 5s | 230,000 |
| 10,000 | 10 minutes | 5s | 1,190,000 |
Over a million wasted API calls per day, all to learn that jobs have finished. Every one of those calls costs compute, network bandwidth, and rate limit headroom.
Step 2: Understand the Latency Penalty
Polling carries a built-in delay. If your task completes 1 second after your last poll, you will not know for another 4 seconds (with a 5-second interval). On average, you detect completion halfway through the poll interval.
| Poll Interval | Average Detection Latency | Worst Case Latency |
|---|---|---|
| 1 second | 500ms | 1 second |
| 5 seconds | 2.5 seconds | 5 seconds |
| 10 seconds | 5 seconds | 10 seconds |
| 30 seconds | 15 seconds | 30 seconds |
You can shrink the interval, but that multiplies API call volume. The trade-off is direct: lower latency means more wasted calls.
With a webhook or signal, detection latency drops to zero. The system notifies you the moment the task completes.
Step 3: Recognize Rate Limit Pressure
Every polling call counts against your rate limit. If your plan allows 600 requests per minute and you have 50 tasks polling every 5 seconds, those status checks consume 600 requests per minute, leaving zero headroom for creating new tasks or reading results.
Rate limit budget: 600 req/min
Polling overhead: 50 active tasks x 12 polls/min = 600 req/min (ALL of your budget)
Productive API calls: Creating tasks: 0 remaining Reading results: 0 remainingThis creates a counterintuitive failure mode: the busier your system gets, the more polling devours your rate limit, which slows down the productive work.
Webhooks and signals use 1 request per task completion. 50 task completions = 50 requests, not 600.
Step 4: Switch to Push-Based Alternatives
Alternative 1: onComplete webhook
The simplest replacement. AsyncQueue calls your endpoint when the task finishes:
// No polling neededawait aq.tasks.create({ targetUrl: 'https://your-app.com/api/process', payload: { orderId: 'order_123' }, onCompleteUrl: 'https://your-app.com/api/task-done',});
// Your webhook handler receives the resultapp.post('/api/task-done', async (req, res) => { const { task } = req.body; await handleCompletion(task.id, task.result); res.json({ received: true });});Cost: 0 polling calls. 1 webhook delivery.
Alternative 2: wait-for-signal
For tasks that depend on external events:
const { task, signalToken } = await aq.tasks.create({ targetUrl: 'https://your-app.com/api/start', waitForSignal: true, maxWaitTime: 3600,});
// External system sends signal when ready// No polling on your side at allCost: 0 polling calls. 1 signal request.
Alternative 3: client-side polling with smart intervals
If you must poll (e.g., showing progress in a UI), use adaptive intervals:
async function smartPoll(taskId) { let interval = 1000; // Start at 1 second const maxInterval = 15000; // Cap at 15 seconds
while (true) { const task = await aq.tasks.get(taskId); if (['completed', 'failed', 'timeout'].includes(task.status)) { return task; }
await sleep(interval); interval = Math.min(interval * 1.5, maxInterval); // Slow down over time }}This cuts total calls by 40-60% compared to fixed-interval polling.
Step 5: Use Polling Only When It Makes Sense
Polling is not always wrong. Use it when:
- You are building a CLI tool that runs once and exits. A webhook endpoint makes no sense for a throwaway script.
- Tasks complete in under 5 seconds. Two or three polls cause no harm.
- You lack a publicly accessible endpoint to receive webhooks (local development, internal tools).
- You are showing real-time progress in a UI and the user watches actively.
For everything else, prefer webhooks or signals:
| Scenario | Best Approach | Why |
|---|---|---|
| Background job, result needed later | onComplete webhook | Zero polling overhead |
| Waiting for external event | Wait-for-signal | Zero polling, instant resolution |
| CLI script, quick task | Polling (short) | No infrastructure needed |
| UI progress indicator | Client-side polling (adaptive) | User expects real-time feedback |
| High-volume batch processing | onComplete webhook | Polling at scale destroys rate limits |