---
title: "Concorrência e paralelismo no web scraping"
description: "A concorrência aproveita o tempo de espera e o paralelismo, a potência da CPU. Explicamos qual acelera o scraping, com exemplos em Python e Node.js."
url: https://proxynet.io/pt-br/blog/concurrency-vs-parallelism
date: 2026-09-13
author: "Acar Diveroli"
category: "Comparativos, Web scraping"
lang: pt-BR
---

# Concorrência e paralelismo no web scraping

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.

> **Nota: Resposta rápida**
>
> Concorrência é várias tarefas avançarem no mesmo período; funciona até em um único núcleo, sobrepondo esperas. Paralelismo é as tarefas rodarem de fato ao mesmo tempo em vários núcleos. Como no scraping a maior parte do tempo vai para esperar respostas de rede, a velocidade aumenta principalmente com concorrência; o paralelismo só faz diferença em etapas intensivas em CPU, como analisar páginas grandes ou rodar um navegador headless. O teto real costuma ser o limite de taxa do site de destino e a capacidade do proxy.

## 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:

1. **Resolução DNS.** Transformar o nome de domínio em endereço IP. Uma consulta e uma resposta pela rede.
2. **Conexão com o proxy e o túnel.** Uma conexão TCP com o proxy, uma requisição `CONNECT` e a espera até o proxy se conectar ao destino.
3. **Handshake TLS.** Uma conexão criptografada com o servidor de destino. Leva algumas idas e voltas.
4. **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.
5. **Baixar a resposta.** Depende do tamanho da página e da banda.
6. **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 de `await`. É a opção mais leve para centenas ou até milhares de requisições de rede concorrentes. A [documentação do asyncio](https://docs.python.org/3/library/asyncio.html) descreve as peças desse modelo. A biblioteca também precisa ser assíncrona: o `AsyncClient` do 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 do `asyncio`, 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](https://docs.python.org/3/library/concurrent.futures.html) 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](/pt-br/blog/httpx-vs-requests-vs-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.

```python
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.

```python
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](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick) 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 `Promise` não bloqueia as outras enquanto espera. É parecido com o modelo `asyncio` do 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_threads` ou processos separados.
- **A resolução de nomes de domínio pode ser um gargalo escondido.** A função padrão `dns.lookup` do 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:

```javascript
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](/pt-br/blog/nodejs-proxy).

## 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](/pt-br/blog/http-status-codes-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](https://proxynet.io/pt-br/rotating-proxy), 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](https://proxynet.io/pt-br/residential-proxy), 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](/pt-br/blog/web-scraping-without-getting-blocked).

## 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:

1. **Comece com um valor baixo.** Algumas requisições simultâneas para um único site de destino.
2. **Meça três coisas.** Páginas concluídas por minuto, tempo médio de resposta e taxa de erros (`429`, `503`, timeouts).
3. **Aumente o valor aos poucos.** Repita a mesma medição a cada passo.
4. **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.
5. **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.
6. **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:

```python
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](/pt-br/data-scraping).
- **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](/pt-br/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](/pt-br/blog/how-to-rotate-proxies-in-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 chamada `requests.get()` dentro de uma `async def` trava o event loop, e todas as tarefas rodam uma após a outra.
- **`gather` ou `Promise.all` sem 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 `429` sobe 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](/pt-br/blog/static-vs-dynamic-pages).

## 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](/pt-br/proxy).
