← Torna al blog

Celery, RQ o BullMQ: come costruire una coda di lavoro con proxy e superare il limite di richieste per IP

Analizziamo l'architettura delle code di lavoro con rotazione dei proxy in Celery, RQ e BullMQ: come distribuire il carico tra gli indirizzi IP ed evitare il rate limit.

📅20 ottobre 2026

Quando un parser o un bot invia centinaia di richieste al minuto da un singolo IP, il server attiva rapidamente il rate limit, e poi ci sono captcha o ban. La soluzione è una coda di attività che distribuisce le richieste tra i worker e i proxy. In questo articolo esamineremo come è strutturata tale architettura in Celery, RQ e BullMQ, e come gestire correttamente il limite di richieste per ogni indirizzo IP.

Perché una coda di attività quando si lavora con i proxy

Se l'obiettivo è estrarre 50.000 schede prodotto da Wildberries o controllare lo stato di 10.000 account Instagram, l'esecuzione sequenziale delle richieste richiederà ore, mentre l'esecuzione parallela senza controllo porterà rapidamente a un ban per IP. La coda di attività (task queue) risolve entrambi i problemi: suddivide un grande volume di lavoro in compiti più piccoli, li distribuisce tra più worker e consente di associare a ciascun worker un proxy separato.

L'architettura di solito appare così: un broker di messaggi (Redis o RabbitMQ) memorizza la coda di attività, i worker prelevano i compiti dalla coda e li eseguono, ciascun worker utilizza il proprio proxy o ruota i proxy da un pool. Questo consente di mantenere il carico su ogni IP entro limiti sicuri, anche se il volume totale delle richieste è enorme.

Questo approccio è utilizzato non solo dagli sviluppatori di parser, ma anche da team che automatizzano il controllo delle campagne pubblicitarie su Facebook Ads, monitorano i prezzi su Ozon o raccolgono dati da decine di siti contemporaneamente. Senza una coda e una distribuzione intelligente per IP, qualsiasi sistema di questo tipo è destinato a essere bloccato già nel primo giorno di lavoro.

Problema del limite di richieste per un singolo IP

La maggior parte dei siti e delle API ha una protezione contro il sovraccarico. Questa può manifestarsi in vari modi: il server restituisce HTTP 429 (Too Many Requests), mostra un captcha, banna temporaneamente l'IP per 10-60 minuti o lo aggiunge permanentemente alla blacklist. I limiti variano notevolmente tra le diverse piattaforme: ad esempio, l'API di Wildberries consente un numero limitato di richieste al secondo da un singolo indirizzo, mentre Instagram reagisce a un'attività sospetta già dopo 15-20 azioni consecutive.

L'errore principale nella costruzione di una coda è avviare tutti i worker tramite lo stesso IP. Anche se i compiti sono distribuiti tra 20 worker, se tutti utilizzano lo stesso proxy o non ne utilizzano affatto, il limite per questo IP verrà raggiunto altrettanto rapidamente quanto nell'esecuzione sequenziale. Pertanto, il numero di worker deve essere proporzionato al numero di indirizzi IP disponibili nel pool di proxy.

Per compiti con un'alta frequenza di richieste su un singolo dominio, è meglio utilizzare proxy di data center — offrono bassa latenza e sono adatti per il parsing parallelo su larga scala, dove la velocità è importante, non la mascheratura come utente normale. Se invece l'obiettivo è aggirare protezioni più severe come i sistemi anti-bot dei marketplace o dei social network, sono necessari IP simili a quelli degli utenti normali, ovvero residenziali o mobili.

Celery: architettura e rotazione dei proxy nel codice

Celery è la coda di attività più matura e popolare nell'ecosistema Python. Funziona sopra un broker (più comunemente Redis o RabbitMQ) e supporta una configurazione flessibile della concorrenza, dei tentativi di ripetizione e delle priorità dei compiti. Per la rotazione dei proxy, è comodo memorizzare il pool di IP in Redis e assegnare un proxy a ciascun compito al momento dell'inserimento nella coda.

from celery import Celery
import requests
import redis
import random

app = Celery('tasks', broker='redis://localhost:6379/0')
redis_client = redis.Redis(host='localhost', port=6379, db=1)

PROXY_POOL = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]

def get_proxy():
    # Round-robin tramite Redis, per non colpire un solo IP
    idx = redis_client.incr("proxy_cursor") % len(PROXY_POOL)
    return PROXY_POOL[idx]

@app.task(bind=True, max_retries=3, default_retry_delay=30)
def fetch_url(self, url):
    proxy = get_proxy()
    try:
        response = requests.get(
            url,
            proxies={"http": proxy, "https": proxy},
            timeout=15
        )
        if response.status_code == 429:
            raise self.retry(exc=Exception("Rate limited"))
        return response.status_code
    except requests.exceptions.RequestException as exc:
        raise self.retry(exc=exc)

Il punto chiave è la funzione get_proxy(), che tramite un contatore Redis fornisce il prossimo proxy dal pool in modo circolare. Questo garantisce che il carico sia distribuito uniformemente tra tutti gli IP, e non si concentri su uno solo. Il parametro max_retries e la gestione del codice 429 consentono al compito di essere automaticamente ripetuto con un altro proxy, se l'IP corrente ha ricevuto un limite.

Per controllare la concorrenza a livello di worker, Celery consente di limitare il numero di compiti simultanei tramite il parametro --concurrency al momento dell'avvio del worker, così come tramite code con priorità diverse (ad esempio, una coda separata per compiti lenti con un rigido rate limit).

RQ (Redis Queue): un'alternativa semplice

RQ è una libreria più leggera rispetto a Celery, senza broker RabbitMQ e con una configurazione minima. È adatta se il progetto è piccolo e non richiede una complessa instradamento dei compiti, ma ha bisogno semplicemente di una coda con worker e la possibilità di associare un proxy a un compito.

from redis import Redis
from rq import Queue
import requests
import itertools

redis_conn = Redis()
q = Queue(connection=redis_conn)

PROXY_POOL = [
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
]
proxy_cycle = itertools.cycle(PROXY_POOL)

def fetch_with_proxy(url):
    proxy = next(proxy_cycle)
    response = requests.get(
        url,
        proxies={"http": proxy, "https": proxy},
        timeout=15
    )
    if response.status_code == 429:
        # Ripetizione nella coda con ritardo
        q.enqueue_in(60, fetch_with_proxy, url)
        return None
    return response.status_code

# Inserimento dei compiti nella coda
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
    q.enqueue(fetch_with_proxy, url)

In RQ, la rotazione tramite itertools.cycle funziona all'interno del processo del worker — è più semplice rispetto a un contatore Redis, ma scala peggio con molti worker paralleli, poiché ogni worker inizia il ciclo dall'inizio. Per piccoli progetti con 1-5 worker, questo è sufficiente; per progetti più grandi è meglio tornare a un contatore centralizzato in Redis, come nell'esempio con Celery.

RQ è ben adatta agli sviluppatori che già utilizzano Redis nel progetto e non vogliono avviare un broker di messaggi separato. Lo svantaggio è che ha meno meccanismi integrati di priorità e monitoraggio rispetto a Celery, quindi per sistemi di produzione con centinaia di migliaia di compiti al giorno si sceglie più spesso Celery.

BullMQ: coda su Node.js con limitazione per IP

BullMQ è lo standard per le code di attività nell'ecosistema Node.js, costruito su Redis e dotato di un rate limiter integrato, il che è particolarmente utile per controllare il limite di richieste per IP. Invece di contare manualmente le richieste, è possibile specificare il limiter direttamente a livello di coda.

const { Queue, Worker } = require('bullmq');
const axios = require('axios');

const connection = { host: 'localhost', port: 6379 };

const proxyPool = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

const queue = new Queue('fetchQueue', { connection });

// Limiter: non più di 5 compiti al secondo per coda
const worker = new Worker('fetchQueue', async job => {
  const proxy = proxyPool[job.id % proxyPool.length];
  try {
    const response = await axios.get(job.data.url, {
      proxy: false,
      httpsAgent: new (require('https-proxy-agent'))(proxy),
      timeout: 15000,
    });
    return response.status;
  } catch (err) {
    if (err.response && err.response.status === 429) {
      throw new Error('Rate limited, retrying with another proxy');
    }
    throw err;
  }
}, {
  connection,
  limiter: { max: 5, duration: 1000 },
  concurrency: proxyPool.length,
});

for (let i = 0; i < 100; i++) {
  queue.add('fetch', { url: `https://example.com/item/${i}` }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 5000 },
  });
}

Il parametro limiter in BullMQ è un meccanismo integrato che limita il numero totale di compiti elaborati in un intervallo di tempo, il che è utile per rispettare il limite di richieste a livello di sistema. E il parametro concurrency, pari alla dimensione del pool di proxy, garantisce che ci siano esattamente tanti worker attivi quanti sono gli IP disponibili.

Questo approccio è spesso utilizzato da team che scrivono i propri bot per automatizzare le azioni su TikTok o Telegram — BullMQ si integra bene con il resto dello stack Node.js e non richiede di passare a Python per un singolo compito.

Confronto tra Celery, RQ e BullMQ

Parametro Celery RQ BullMQ
Linguaggio Python Python Node.js
Broker Redis / RabbitMQ Solo Redis Solo Redis
Rate limiter integrato No (da scrivere manualmente) No Sì, out of the box
Soglia di ingresso Media Bassa Media
Adatto per Grandi sistemi di produzione Progetti piccoli, prototipi Stack Node.js, bot, automazione

Strategie di distribuzione del carico per IP

Il round-robin, utilizzato negli esempi sopra, è la strategia più semplice, ma non sempre ottimale. Ecco alcune opzioni da considerare a seconda del compito:

  • Round-robin — ogni richiesta successiva passa attraverso il prossimo proxy in modo circolare. Adatto quando tutti gli IP sono equivalenti e i limiti su di essi sono gli stessi.
  • Sticky session — un proxy è assegnato a un account o a una sessione specifica per tutta la durata del lavoro con esso. Critico per i social network e i marketplace, dove il cambio di IP durante una sessione appare sospetto.
  • Weighted distribution — parte delle richieste è indirizzata a IP più costosi e affidabili (ad esempio, mobili), parte a quelli economici di data center, a seconda del dominio trattato.
  • Adaptive throttling — la velocità di invio dei compiti viene automaticamente ridotta se aumenta la percentuale di errori 429 o captcha, e ripristinata quando gli errori scompaiono.

Per compiti in cui è importante mascherarsi da utente reale — ad esempio, il riscaldamento e il controllo degli account su Instagram o TikTok tramite browser anti-detect come Dolphin Anty o AdsPower — è meglio applicare sticky session con proxy mobili: sono meno suscettibili a sospetti da parte dei sistemi anti-bot e si comportano come normali utenti di internet mobile.

Per la raccolta massiva di dati da siti, dove la velocità e il volume sono più importanti della fiducia nell'algoritmo, il round-robin con proxy residenziali offre un equilibrio tra stabilità e prezzo: gli IP sembrano utenti reali, ma possono essere cambiati frequentemente senza rischiare una singola sessione.

Monitoraggio degli errori 429 e dei ban

Una coda di attività senza monitoraggio è una scatola nera: è difficile capire perché parte dei compiti fallisce e quali IP sono già sotto sospetto. Il set minimo di metriche da monitorare è:

  • Percentuale di risposte con codice 429 e 403 per ciascun proxy separatamente
  • Tempo medio di risposta — un aumento della latenza spesso precede un ban
  • Numero di tentativi (retries) per compito
  • Lunghezza della coda di attività in tempo reale — se cresce, i worker non riescono a gestirla

In Celery è comodo utilizzare Flower — un'interfaccia web per il monitoraggio dei compiti e dei worker in tempo reale. In BullMQ c'è Bull Board con funzionalità simili. Per RQ esiste RQ Dashboard. Tutti e tre gli strumenti mostrano lo stato dei compiti, il numero di tentativi falliti e consentono di capire rapidamente quale proxy dal pool escludere temporaneamente dalla rotazione.

Una buona pratica è contrassegnare automaticamente i proxy come "in quarantena" per 15-30 minuti dopo diversi errori consecutivi 429 da quell'IP, e reinserirli nel pool solo dopo una richiesta di test riuscita. Questo previene situazioni in cui un IP bannato continua a ricevere compiti e spreca il tempo dei worker.

Conclusione

La coda di attività risolve il problema della scalabilità delle richieste, ma di per sé non protegge dal limite sugli IP — per questo è necessaria una rotazione dei proxy ben pensata all'interno di Celery, RQ o BullMQ. La scelta dello strumento dipende dallo stack: i progetti Python scelgono più spesso Celery per la produzione e RQ per compiti semplici, mentre gli sviluppatori Node.js optano per BullMQ con il suo rate limiter integrato.

Indipendentemente dalla coda scelta, il risultato dipende fortemente dalla qualità stessa del pool di proxy. Se l'obiettivo è un rapido parsing massivo, considera i proxy di data center per la velocità, mentre per lavorare con social media e marketplace, dove la lealtà dei sistemi anti-bot è importante, sono adatti gli IP residenziali o mobili — riducono la percentuale di errori 429 e di ban, il che riduce direttamente il numero di tentativi e accelera l'esecuzione dell'intera coda di attività.