Web Scraping APIs

Synchronous vs Asynchronous Web Scraping: When to Use Each

Synchronous vs Asynchronous Web Scraping: When to Use Each — conceptual illustration
On this page

Synchronous web scraping sends one request at a time and waits ("blocks") until each one finishes before starting the next; asynchronous scraping fires off many requests at once and handles each response as it arrives, using an event loop or a pool of workers. Which one is better depends on what is slowing you down. If your requests are slow (page rendering, anti-bot challenges), async wins because it does useful work during the wait. But if you are capped by per-host rate limits (how often a site lets one source hit it) or by proxy throughput, async stops helping - the bottleneck is no longer your own machine's CPU.

Quick facts

Sync patternrequests.get(url) for url in urls — simple, slow
Async patternasyncio.gather() with aiohttp/httpx, or a thread pool
When sync is fineSmall jobs, low-latency targets, simple debugging
When async helpsMany slow requests (rendering, CAPTCHAs), wide URL fan-out
When neither mattersWhen rate-limited or proxy-capped — scale proxies first, not concurrency

The shape of the bottleneck

Web scraping is almost always I/O-bound, meaning the time is spent waiting on the network, not crunching numbers. A single request takes anywhere from 200ms to 30s of real elapsed time, but the actual CPU work on your machine is just milliseconds. Synchronous code wastes all the rest of that time sitting idle; asynchronous code starts another request during the wait. The math is stark: for 1,000 URLs at 1 second each, sync takes 1,000 seconds, while async with 50 concurrent workers (50 requests in flight at once) finishes in about 20 seconds.

Where async stops helping

Concurrency always runs into a ceiling somewhere - your proxy pool, the target site's per-IP rate limit, or your scraping API's per-account throughput cap. Once you hit any of these, adding more concurrency just makes requests pile up in a queue without finishing any faster. The number to watch is throughput (URLs actually completed per minute), not how many requests you launch at once. Measure it directly. If 50 workers produce the same throughput as 200, the bottleneck has moved off your machine and onto one of those external limits.

Practical recommendations

For under 100 URLs, just write synchronous code - it is easier to debug and easier to retry the odd failure by hand. For 100 to 10,000 URLs, use async with a modest concurrency cap (10-50). Above 10,000 URLs, switch to a managed scraping API that handles concurrency, retries, and dead-letter queues (a holding spot for requests that keep failing) for you. Building that layer yourself is more work than it looks.

Code example

python
import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url, timeout=30) as r:
        return await r.text()

async def main(urls):
    sem = asyncio.Semaphore(50)
    async with aiohttp.ClientSession() as session:
        async def bounded(u):
            async with sem:
                return await fetch(session, u)
        return await asyncio.gather(*[bounded(u) for u in urls])

Related terms

Concept map

How Synchronous vs Asynchronous Web Scraping connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Web Scraping APIs
Building map…

Frequently asked questions

What concurrency should I start with?

Start at 10 and keep doubling until throughput stops improving. The common sweet spot is 20-50 for general scraping; go lower (5-10) for tough anti-bot targets, where high concurrency tends to trigger blocks.

Is async faster than threading?

For pure I/O work like HTTP scraping, async carries slightly less overhead than threads at very high concurrency (1,000+ requests at once). Below that, threading is simpler and the difference is negligible.

Does async help with CPU-bound parsing?

No. Parsing is CPU work, and async does not run CPU work in parallel - it only overlaps waiting. If parsing becomes a bottleneck, use a multiprocessing pool instead. This is rare, since parsing is usually fast compared to fetching the page.

Last updated: 2026-05-31