Quem quer acelerar um scraper geralmente pensa primeiro em "mais núcleos": dividir o trabalho entre processos, contratar um servidor maior. Mas, em um trabalho de scraping típico, o processador fica ocioso a maior parte do tempo. O programa espera segundos pela resposta do servidor de destino, pelo handshake TLS e pelo proxy abrir o túnel. O jeito de aproveitar esse tempo de espera se chama concorrência, o jeito de aproveitar a potência do processador se chama paralelismo, e os dois resolvem problemas diferentes.
Neste artigo, explicamos a diferença entre os dois conceitos, por que o scraping é principalmente um trabalho intensivo em entrada/saída (I/O) e quando asyncio, threads e processos do Python são úteis. Também vemos o event loop do Node.js, o que realmente limita a velocidade e como decidir o valor de concorrência. Rodamos os exemplos contra um servidor local com latência artificial e compartilhamos um esqueleto de medição para você medir o seu próprio trabalho.
O que é concorrência?
Concorrência é a capacidade de várias tarefas avançarem no mesmo período. As tarefas não precisam rodar no mesmo instante; enquanto uma espera, outra roda. Um cozinheiro que corta a salada enquanto espera a água do macarrão ferver está fazendo concorrência: uma pessoa conclui dois trabalhos no mesmo tempo.
Na programação, isso é feito quando uma tarefa cede o controle a outra no momento em que espera uma resposta. O asyncio do Python e o event loop do Node.js funcionam assim. Um único núcleo de processador e uma única thread conseguem manter centenas de requisições de rede "abertas" ao mesmo tempo, porque nenhuma delas usa o processador enquanto espera.
O que é paralelismo?
Paralelismo é rodar várias tarefas fisicamente ao mesmo tempo, em núcleos diferentes do processador. No exemplo da cozinha, são dois cozinheiros preparando dois pratos diferentes ao mesmo tempo. A velocidade do trabalho cresce diretamente com o número de trabalhadores, mas cada um precisa da própria bancada e dos próprios ingredientes.
Na programação, o paralelismo geralmente é obtido com vários processos ou com threads que realmente podem rodar ao mesmo tempo. O benefício aparece quando o trabalho usa muito o processador: analisar um documento HTML grande, processar imagens, comprimir dados.
| Critério | Concorrência | Paralelismo |
|---|---|---|
| Ideia central | Sobrepor os tempos de espera | Dividir o trabalho entre núcleos |
| Núcleos necessários | Um núcleo basta | Vários núcleos |
| Trabalho adequado | Intensivo em I/O: rede, disco, banco de dados | Intensivo em CPU: análise, cálculo |
| Ferramentas no Python | asyncio, threads | multiprocessing, ProcessPoolExecutor |
| Ferramentas no Node.js | Event loop, Promise | worker_threads, vários processos |
| Custo de memória | Baixo, pequeno por tarefa | Alto, memória própria por processo |
| Papel no scraping | Enviar requisições e receber respostas | Analisar páginas, rodar navegadores headless |
| Erro típico | Sobrecarregar o destino com concorrência ilimitada | Dividir trabalho de I/O entre processos e desperdiçar recursos |
Os dois conceitos não são alternativas. Um sistema grande de scraping geralmente usa os dois: cada processo gerencia centenas de requisições de forma concorrente, e os processos rodam em paralelo em núcleos diferentes.
Por que o scraping é intensivo em I/O?
Olhar as etapas de uma única requisição de página mostra como o processador trabalha pouco:
- Resolução DNS. Transformar o nome de domínio em endereço IP. Uma consulta e uma resposta pela rede.
- Conexão com o proxy e o túnel. Uma conexão TCP com o proxy, uma requisição
CONNECTe a espera até o proxy se conectar ao destino. - Handshake TLS. Uma conexão criptografada com o servidor de destino. Leva algumas idas e voltas.
- Enviar a requisição e esperar a resposta. O servidor de destino prepara a página; em páginas com consultas a banco de dados, isso demora mais.
- Baixar a resposta. Depende do tamanho da página e da banda.
- Análise. Extrair dados do HTML. É a única etapa que usa o processador.
Nas cinco primeiras etapas, o programa espera bytes da rede. Em um script que baixa páginas uma após a outra, a análise é uma parte pequena do total; o script passa a maior parte do tempo esperando, com o processador ocioso.
Mas essa proporção não é fixa. Em um servidor local que atrasa cada resposta em 300 milissegundos, baixamos e analisamos 40 páginas com 400 cards de produto cada. Aumentar a concorrência de 1 para 5 e 20 reduziu o tempo de download em mais de dez vezes. Com concorrência 20, analisar essas mesmas páginas em um único núcleo levou mais tempo do que baixá-las; passar para um pool de processos cortou essa etapa mais ou menos pela metade. Ou seja, conforme a concorrência sobe, o gargalo pode passar da rede para o processador. Com os seus destinos, os números serão outros; recomendamos medir o seu próprio trabalho com o esqueleto de medição abaixo.
Qual é a diferença entre asyncio, threads e processos no Python?
O Python tem três ferramentas, e a chave para escolher entre elas é o GIL (Global Interpreter Lock). No interpretador padrão CPython, o GIL permite que só uma thread rode código Python por vez. Mas uma thread libera o GIL enquanto espera dados da rede. Como resultado:
asyncio: um event loop que roda em uma única thread. As tarefas cedem o controle umas às outras nos pontos deawait. É a opção mais leve para centenas ou até milhares de requisições de rede concorrentes. A documentação do asyncio descreve as peças desse modelo. A biblioteca também precisa ser assíncrona: oAsyncClientdo HTTPX ou o AIOHTTP.- Threads (
ThreadPoolExecutor): como o GIL é liberado durante a espera de rede, as threads dão ganho real de velocidade em trabalho de I/O. São o jeito mais fácil de adicionar concorrência com bibliotecas síncronas como o Requests. Uma thread custa mais memória que uma tarefa doasyncio, então a eficiência cai depois de algumas centenas de requisições concorrentes. - Processos (
ProcessPoolExecutor,multiprocessing): cada processo roda com o próprio interpretador e o próprio GIL, então dão paralelismo real em trabalho intensivo em CPU. Iniciar processos e mover dados entre eles é caro. A documentação do concurrent.futures define a interface comum dos pools de threads e de processos.
O Python 3.13 também trouxe uma versão experimental que pode rodar sem GIL; mas, como a compatibilidade das bibliotecas ainda é limitada, a versão padrão e as três ferramentas acima continuam comuns em trabalhos de scraping em produção.
| Ferramenta | Dá paralelismo? | Trabalho adequado | Uso no scraping |
|---|---|---|---|
asyncio + HTTPX/AIOHTTP | Não, uma única thread | Muitas requisições de rede | Baixar páginas |
ThreadPoolExecutor + Requests | Na prática sim, durante I/O | Número moderado de requisições, código síncrono | Acelerar código Requests existente |
ProcessPoolExecutor | Sim | Trabalho intensivo em CPU | Analisar páginas grandes |
Comparamos o suporte a concorrência das bibliotecas Python em HTTPX, Requests ou AIOHTTP.
Exemplo 1: concorrência limitada com asyncio.Semaphore
O código abaixo baixa páginas de forma concorrente, mas limita com o valor limit o número de requisições abertas ao mesmo tempo. Sem o Semaphore, o asyncio.gather inicia todas as requisições de uma vez, o que sobrecarrega tanto o seu próprio pool de conexões quanto o site de destino.
import asyncio
import httpx
async def fetch(client, sem, url):
async with sem:
response = await client.get(url)
response.raise_for_status()
return response.text
async def fetch_all(urls, limit=10, proxy=None):
sem = asyncio.Semaphore(limit)
async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
return await asyncio.gather(*(fetch(client, sem, u) for u in urls), return_exceptions=True)
urls = [f"https://example.com/product/{i}" for i in range(100)]
pages = asyncio.run(fetch_all(urls, limit=10, proxy="http://user:pass@pr.proxynet.io:8000"))Um único AsyncClient é compartilhado entre todas as requisições. Assim as conexões são reutilizadas e evita-se um novo handshake TLS a cada requisição.
Exemplo 2: download concorrente, análise em paralelo
Se as páginas são grandes e a análise leva um tempo perceptível, faz sentido combinar os dois modelos: baixar com asyncio e analisar em um pool de processos.
import asyncio
from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup
def parse(html):
soup = BeautifulSoup(html, "html.parser")
return [(p.h2.get_text(strip=True), p.select_one(".price").get_text(strip=True)) for p in soup.select(".product")]
async def scrape(urls, limit=10, proxy=None):
pages = await fetch_all(urls, limit=limit, proxy=proxy)
html_pages = [p for p in pages if isinstance(p, str)]
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
return await asyncio.gather(*(loop.run_in_executor(pool, parse, html) for html in html_pages))
if __name__ == "__main__":
results = asyncio.run(scrape(urls, limit=10))A linha if __name__ == "__main__": é obrigatória no Windows e no macOS: o pool de processos inicia processos novos carregando o módulo desde o início, e, sem essa proteção, o código roda a si mesmo repetidamente.
É importante saber que essa configuração nem sempre acelera. Iniciar processos e mover HTML entre eles tem custo; se as páginas são pequenas e poucas, esse custo pode superar a própria análise. No teste acima, o pool de processos encurtou claramente a análise de páginas com 400 cards, mas o custo de inicialização dos processos se somou ao tempo total de download e análise. Meça com as suas próprias páginas antes de decidir.
Como funciona o event loop do Node.js?
O Node.js roda o código JavaScript em uma única thread e gerencia as operações de rede com um event loop. Quando você chama fetch, o Node.js entrega a requisição ao sistema operacional e o código JavaScript segue; quando a resposta chega, o callback correspondente entra na fila. O guia do event loop do Node.js explica as fases desse loop em detalhe.
Para o scraping, isso tem três consequências:
- Requisições de rede são naturalmente concorrentes. Cada requisição baseada em
Promisenão bloqueia as outras enquanto espera. É parecido com o modeloasynciodo Python. - Código intensivo em CPU trava o loop inteiro. Enquanto uma função síncrona analisa um documento HTML grande, nenhuma outra resposta pode ser processada. Para esse tipo de trabalho, use threads separadas com o módulo
worker_threadsou processos separados. - A resolução de nomes de domínio pode ser um gargalo escondido. A função padrão
dns.lookupdo Node.js roda o resolvedor do sistema operacional no pequeno pool de threads do libuv. Em um script que se conecta a muitos domínios diferentes ao mesmo tempo, esse pool pode lotar. Quando você usa proxy, os domínios de destino são resolvidos do lado do proxy, o que reduz o efeito.
Você não precisa de uma biblioteca extra para limitar a concorrência; um limitador simples basta:
function limiter(max) {
let active = 0;
const queue = [];
const next = () => {
if (active >= max || queue.length === 0) return;
active++;
const { task, resolve, reject } = queue.shift();
task().then(resolve, reject).finally(() => {
active--;
next();
});
};
return (task) => new Promise((resolve, reject) => {
queue.push({ task, resolve, reject });
next();
});
}
const limit = limiter(10);
const results = await Promise.allSettled(
urls.map((url) => limit(() => fetch(url).then((r) => r.text()))),
);Como passar um proxy no Node.js, com um exemplo de rotação e novas tentativas, está em Como usar proxy no Node.js.
O que realmente limita a velocidade?
Aumentar a concorrência acelera até certo ponto; depois disso, não faz efeito ou deixa o trabalho mais lento. No scraping, o teto geralmente não é o processador, e sim estes fatores:
- O limite de taxa do site de destino. Se o site permite um certo número de requisições por minuto da mesma fonte, aumentar a concorrência só gera mais respostas
429. Como lidar com esses códigos está em Códigos de status HTTP no web scraping. - A capacidade do servidor de destino. Quando o servidor de um site pequeno fica lento, a sua concorrência aumenta os tempos de resposta dele; tanto o seu trabalho quanto os visitantes reais do site ficam mais lentos.
- A capacidade do proxy. O limite de conexões simultâneas do plano, o número de IPs do pool e os tempos de resposta dos pontos de saída. Com um Proxies rotativos, que oferece muitos IPs de saída por um único endereço, as requisições são distribuídas entre endereços diferentes, mas o limite de conexões do plano continua valendo.
- Banda. Ao baixar páginas grandes e respostas com imagens, entra em jogo o limite da sua conexão ou do tráfego do proxy.
- Custo de abrir conexões. Se cada requisição abre uma conexão nova com um handshake TLS novo, o tempo aumenta. Compartilhar o cliente e reutilizar conexões reduz esse custo.
- Distância. A latência entre o ponto de saída do proxy e o servidor de destino se soma a cada ida e volta. Com endereços Proxies residenciais, que saem por conexões domésticas reais, esse tempo varia com a localização e a linha.
- O processador. Só passa a ser decisivo ao rodar navegadores headless, analisar documentos muito grandes ou processar muitos dados na mesma máquina.
Tratamos de outras formas de ajustar a velocidade sem ser bloqueado nem prejudicar o site em Como fazer web scraping sem ser bloqueado.
Qual deve ser o valor de concorrência?
Não existe um número único para todo trabalho; o valor certo se encontra medindo. O método que recomendamos:
- Comece com um valor baixo. Algumas requisições simultâneas para um único site de destino.
- Meça três coisas. Páginas concluídas por minuto, tempo médio de resposta e taxa de erros (
429,503, timeouts). - Aumente o valor aos poucos. Repita a mesma medição a cada passo.
- Encontre o ponto de virada. Quando as páginas concluídas param de aumentar e o tempo de resposta ou a taxa de erros começam a subir, volte ao valor anterior.
- Defina um limite por site. Se você rastreia vários sites ao mesmo tempo, a concorrência total pode ser alta, mas a fatia de cada site deve continuar pequena.
- Meça de novo periodicamente. A infraestrutura e os limites de taxa dos sites de destino mudam com o tempo.
Um esqueleto simples para medir:
import asyncio
import time
def measure(label, fn):
start = time.perf_counter()
result = fn()
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.2f} s")
return result
for limit in (1, 5, 10, 20):
pages = measure(f"limit={limit}", lambda: asyncio.run(fetch_all(urls, limit=limit)))
errors = sum(isinstance(p, Exception) for p in pages)
print(f" erros: {errors} / {len(pages)}")time.perf_counter() é um contador de alta resolução que não é afetado por mudanças no relógio do sistema e é mais adequado que time.time() para medir durações. Faça a medição com uma lista pequena de URLs que não sobrecarregue o site de destino e, se possível, em horários de pouco movimento no site.
Casos de uso
- Poucas páginas de um único site: Requests síncrono com pausas entre as requisições. Não precisa de concorrência.
- Coleta de preços e catálogos de muitos sites: concorrência com limite pequeno por site usando
asyncio, e um proxy rotativo para distribuir. A configuração geral está na nossa página de solução de extração de dados. - Rastreamento em grande escala: vários processos, cada um rodando
asyncio; uma fila por domínio. O lado da escala está na nossa página de solução de web crawler. - Acelerar um script Requests existente: com
ThreadPoolExecutor, sem reescrever o código. Para uma configuração rotativa com Requests, veja Como rotacionar proxies em Python. - Páginas carregadas com JavaScript: como um navegador headless usa muita CPU e memória, o número de navegadores simultâneos é limitado por núcleos e memória.
Erros comuns
- Dividir trabalho intensivo em I/O entre processos. Você paga o custo de inicialização e a memória dos processos, mas a velocidade não melhora, porque o gargalo é a rede.
- Usar uma biblioteca síncrona dentro do
asyncio. Uma chamadarequests.get()dentro de umaasync deftrava o event loop, e todas as tarefas rodam uma após a outra. gatherouPromise.allsem limite. Iniciar milhares de requisições de uma vez leva a erros de conexão e limites de taxa no site de destino.- Criar um cliente novo para cada requisição. As conexões não são reutilizadas e cada requisição faz um handshake TLS novo.
- Olhar o tempo total, mas não a taxa de erros. Um trabalho que fica mais rápido enquanto a taxa de
429sobe coleta menos dados. - Analisar na thread principal do Node.js. Com documentos grandes, o event loop trava e as respostas em espera podem dar timeout.
Guia de decisão
| Sua situação | Recomendação |
|---|---|
| Muitas páginas, HTML pequeno | asyncio + HTTPX/AIOHTTP, limitado com Semaphore |
| Código Requests síncrono existente | ThreadPoolExecutor |
| Páginas grandes, análise pesada | Baixar com asyncio, analisar com ProcessPoolExecutor |
| Baixar páginas com Node.js | Promise + limitador de concorrência |
| Análise pesada no Node.js | worker_threads |
| Navegador headless | Poucos navegadores simultâneos, conforme núcleos e memória |
A taxa de 429 está subindo | Reduzir a concorrência, definir limite por site |
| Sem ganho de velocidade e sem erros | Medir o gargalo: proxy, banda, servidor de destino |
Perguntas frequentes
Concorrência e paralelismo são a mesma coisa?
Não. Concorrência é as tarefas avançarem no mesmo período e pode acontecer em um único núcleo. Paralelismo é as tarefas rodarem de fato ao mesmo tempo em vários núcleos. Todo sistema paralelo é concorrente, mas nem todo sistema concorrente é paralelo.
asyncio ou threads para scraping?
Se você está escrevendo do zero e vai enviar muitas requisições, o asyncio gerencia mais requisições concorrentes com menos recursos. Se o seu código existente usa Requests e as requisições concorrentes não vão passar de algumas centenas, o ThreadPoolExecutor é o jeito mais fácil de acelerar sem reescrever o código.
O GIL deixa o scraping mais lento?
Como o GIL é liberado durante as requisições de rede, na prática ele não deixa o download de páginas mais lento. Em etapas intensivas em CPU, como a análise, as threads não conseguem rodar código Python ao mesmo tempo; para essas etapas usa-se um pool de processos.
Mais IPs de proxy aumentam a velocidade?
Se o site de destino conta o limite de taxa por IP, distribuir a carga pode permitir chegar a mais páginas no mesmo tempo. Mas, se o site aplica o limite por outros critérios, ou se o gargalo é a banda ou a capacidade do servidor de destino, o número de IPs não muda o resultado. Não prejudicar o site é uma responsabilidade que vale independentemente de quantos IPs você tem.
O que acontece se eu colocar a concorrência alta demais?
Do seu lado, surgem limites de pool de conexões e de descritores de arquivo; do lado do destino, respostas 429, timeouts e um site mais lento. A partir de certo ponto, as páginas concluídas param de aumentar e os erros aumentam.
Como ajustar a concorrência com navegadores headless?
Cada instância de navegador usa bastante memória e CPU, então o número de páginas simultâneas é limitado pelos recursos da máquina, não pela rede. Comece com poucas instâncias e aumente observando o uso de memória e CPU. Quando um navegador headless é realmente necessário está explicado em Páginas estáticas e dinâmicas.
Em resumo
A concorrência aproveita o tempo de espera e o paralelismo, os núcleos do processador. Como no scraping a maior parte do tempo vai para esperar respostas de rede, a velocidade aumenta principalmente com concorrência limitada, construída com asyncio, threads ou o event loop do Node.js; um pool de processos só é necessário em etapas intensivas em CPU, como análise pesada e navegadores headless. O teto costuma ser o limite de taxa do site de destino, a capacidade do proxy e a banda. Defina a concorrência medindo, não chutando, e estabeleça um limite por site. Para coletar dados em escala, conheça os nossos serviços de proxy.




