Lorsque le parseur ou le bot envoie des centaines de requêtes par minute depuis une seule IP, le serveur active rapidement la limite de taux, et ensuite — captcha ou bannissement. La solution est une file d'attente de tâches qui répartit les requêtes entre les workers et les proxies. Dans cet article, nous allons examiner comment une telle architecture est organisée dans Celery, RQ et BullMQ, et comment gérer efficacement la limite de requêtes pour chaque adresse IP.
Pourquoi une file d'attente de tâches lors de l'utilisation de proxies
Si la tâche consiste à extraire 50 000 fiches produits de Wildberries ou à vérifier le statut de 10 000 comptes Instagram, l'exécution séquentielle des requêtes prendra des heures, tandis qu'une exécution parallèle sans contrôle entraînera rapidement un bannissement par IP. La file d'attente de tâches (task queue) résout les deux problèmes : elle divise un grand volume de travail en petites tâches, les répartit entre plusieurs workers et permet d'attacher un proxy distinct à chaque worker.
L'architecture ressemble généralement à ceci : un broker de messages (Redis ou RabbitMQ) stocke la file d'attente des tâches, les workers prennent les tâches de la file et les exécutent, chaque worker utilise son propre proxy ou fait tourner les proxies d'un pool. Cela permet de maintenir la charge sur chaque IP dans des limites sûres, même si le volume total de requêtes est énorme.
Cette approche est utilisée non seulement par les développeurs de parseurs, mais aussi par des équipes qui automatisent la vérification des comptes publicitaires Facebook Ads, surveillent les prix sur Ozon ou collectent des données sur des dizaines de sites simultanément. Sans file d'attente et répartition adéquate par IP, tout système de ce type est voué à des blocages dès le premier jour de fonctionnement.
Problème de limite de requêtes par IP
La plupart des sites et API ont une protection contre la surcharge. Cela peut se manifester de différentes manières : le serveur renvoie HTTP 429 (Trop de requêtes), affiche un captcha, bannit temporairement l'IP pendant 10 à 60 minutes ou l'ajoute définitivement à une liste noire. Les limites varient considérablement d'une plateforme à l'autre — par exemple, l'API de Wildberries autorise un nombre limité de requêtes par seconde depuis une seule adresse, tandis qu'Instagram réagit à une activité suspecte après seulement 15 à 20 actions consécutives.
La principale erreur lors de la construction d'une file d'attente est de lancer tous les workers via la même IP. Même si les tâches sont réparties entre 20 workers, si tous utilisent un seul proxy ou ne l'utilisent pas du tout, la limite pour cette IP sera atteinte aussi rapidement que lors d'une exécution séquentielle. Par conséquent, le nombre de workers doit être proportionnel au nombre d'adresses IP disponibles dans le pool de proxies.
Pour les tâches avec une fréquence de requêtes élevée sur un seul domaine, il est préférable d'utiliser des proxies de data center — ils offrent une faible latence et conviennent pour le parsing parallèle à grande échelle, où la vitesse est plus importante que la dissimulation en tant qu'utilisateur ordinaire. Si l'objectif est de contourner une protection plus stricte, comme les systèmes anti-bots des marketplaces ou des réseaux sociaux, des IP ressemblant à des utilisateurs ordinaires, c'est-à-dire résidentielles ou mobiles, sont nécessaires.
Celery : architecture et rotation des proxies dans le code
Celery est la file d'attente de tâches la plus mature et populaire dans l'écosystème Python. Elle fonctionne au-dessus d'un broker (le plus souvent Redis ou RabbitMQ) et prend en charge une configuration flexible de la concurrence, des tentatives de réessai et des priorités des tâches. Pour la rotation des proxies, il est pratique de stocker le pool d'IP dans Redis et d'attribuer un proxy à chaque tâche lors de son ajout à la file.
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 via Redis pour ne pas frapper une seule 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("Limite de taux atteinte"))
return response.status_code
except requests.exceptions.RequestException as exc:
raise self.retry(exc=exc)
Le point clé est la fonction get_proxy(), qui utilise un compteur Redis pour fournir le prochain proxy du pool en rotation. Cela garantit que la charge est répartie uniformément entre toutes les IP, et non concentrée sur une seule. Le paramètre max_retries et le traitement du code 429 permettent à la tâche de se renvoyer automatiquement avec un autre proxy si l'IP actuelle a atteint la limite.
Pour contrôler la concurrence au niveau des workers, Celery permet de limiter le nombre de tâches simultanées via le paramètre --concurrency lors du démarrage du worker, ainsi que par le biais de files d'attente avec des priorités différentes (par exemple, une file distincte pour les tâches lentes avec une limite de taux stricte).
RQ (Redis Queue) : une alternative simple
RQ est une bibliothèque plus légère que Celery, sans broker RabbitMQ et avec une configuration minimale. Elle convient si le projet est petit et ne nécessite pas de routage complexe des tâches, mais simplement une file d'attente avec des workers et la possibilité d'attacher un proxy à une tâche.
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:
# Réajout à la file avec un délai
q.enqueue_in(60, fetch_with_proxy, url)
return None
return response.status_code
# Ajout de tâches à la file
urls = ["https://example.com/page1", "https://example.com/page2"]
for url in urls:
q.enqueue(fetch_with_proxy, url)
Dans RQ, la rotation via itertools.cycle fonctionne à l'intérieur du processus du worker — c'est plus simple qu'un compteur Redis, mais moins évolutif avec de nombreux workers parallèles, car chaque worker commence le cycle depuis le début. Pour de petits projets avec 1 à 5 workers, cela suffit, mais pour des projets plus importants, il est préférable de revenir à un compteur centralisé dans Redis, comme dans l'exemple avec Celery.
RQ convient bien aux développeurs qui utilisent déjà Redis dans leur projet et ne souhaitent pas mettre en place un broker de messages séparé. Le inconvénient est qu'il y a moins de mécanismes intégrés de priorisation et de surveillance par rapport à Celery, donc pour des systèmes de production avec des centaines de milliers de tâches par jour, Celery est souvent préféré.
BullMQ : file d'attente sur Node.js avec limitation par IP
BullMQ est la norme pour les files d'attente de tâches dans l'écosystème Node.js, construit sur Redis et dispose d'un limiteur de taux intégré, ce qui est particulièrement pratique pour contrôler la limite de requêtes par IP. Au lieu de compter manuellement les requêtes, vous pouvez spécifier le limiteur directement au niveau de la file.
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 });
// Limiteur : pas plus de 5 tâches par seconde pour la file
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('Limite de taux atteinte, réessayer avec un autre 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 },
});
}
Le paramètre limiter dans BullMQ est un mécanisme intégré qui limite le nombre total de tâches traitées sur une période donnée, ce qui est pratique pour respecter la limite de requêtes au niveau de l'ensemble du système. Le paramètre concurrency, égal à la taille du pool de proxies, garantit que le nombre de workers actifs est exactement égal au nombre d'IP disponibles.
Cette approche est souvent utilisée par des équipes qui écrivent leurs propres bots pour automatiser des actions sur TikTok ou Telegram — BullMQ s'intègre bien avec le reste de la pile Node.js et ne nécessite pas de passer à Python pour une seule tâche.
Comparaison de Celery, RQ et BullMQ
| Paramètre | Celery | RQ | BullMQ |
|---|---|---|---|
| Langue | Python | Python | Node.js |
| Broker | Redis / RabbitMQ | Uniquement Redis | Uniquement Redis |
| Limiteur de taux intégré | Non (à écrire manuellement) | Non | Oui, par défaut |
| Seuil d'entrée | Moyen | Bas | Moyen |
| Convient pour | Grands systèmes de production | Petits projets, prototypes | Pile Node.js, bots, automatisation |
Stratégies de répartition de la charge par IP
Le round-robin, utilisé dans les exemples ci-dessus, est la stratégie la plus simple, mais pas toujours optimale. Voici quelques options à considérer en fonction de la tâche :
- Round-robin — chaque requête suivante passe par le prochain proxy en rotation. Convient lorsque toutes les IP sont équivalentes et que les limites sont identiques.
- Session collante — un proxy est attaché à un compte ou une session spécifique pendant toute la durée de l'interaction. Critique pour les réseaux sociaux et les marketplaces, où le changement d'IP au milieu d'une session semble suspect.
- Distribution pondérée — une partie des requêtes est dirigée vers des IP plus coûteuses et fiables (par exemple, mobiles), tandis qu'une autre partie va vers des IP de data center moins chères, en fonction du domaine traité.
- Throttling adaptatif — la vitesse d'envoi des tâches est automatiquement réduite si le pourcentage d'erreurs 429 ou de captchas augmente, et se rétablit lorsque les erreurs disparaissent.
Pour les tâches où il est important de se camoufler en tant qu'utilisateur vivant — par exemple, le réchauffement et la vérification des comptes sur Instagram ou TikTok via des navigateurs anti-détection comme Dolphin Anty ou AdsPower — il est préférable d'appliquer une session collante avec des proxies mobiles : ils sont moins susceptibles d'être suspectés par les systèmes anti-bots et se comportent comme des utilisateurs ordinaires de l'internet mobile.
Pour la collecte massive de données sur des sites où la vitesse et le volume sont plus importants que la confiance de l'algorithme, le round-robin avec des proxies résidentiels offre un équilibre entre stabilité et prix : les IP ressemblent à de véritables utilisateurs, mais peuvent être changées fréquemment sans risque pour une session spécifique.
Surveillance des erreurs 429 et des bannissements
Une file d'attente de tâches sans surveillance est une boîte noire : il est difficile de comprendre pourquoi certaines tâches échouent et quelles IP sont déjà sous surveillance. Voici le minimum de métriques à suivre :
- Pourcentage de réponses avec le code 429 et 403 pour chaque proxy séparément
- Temps de réponse moyen — une augmentation de la latence précède souvent un bannissement
- Nombre de tentatives de réessai (retries) par tâche
- Longueur de la file d'attente des tâches en temps réel — si elle augmente, les workers ne parviennent pas à suivre
Dans Celery, il est pratique d'utiliser Flower — une interface web pour surveiller les tâches et les workers en temps réel. BullMQ dispose de Bull Board avec une fonctionnalité similaire. Pour RQ, il existe RQ Dashboard. Tous ces trois outils montrent l'état des tâches, le nombre d'échecs et permettent de comprendre rapidement quel proxy du pool doit être temporairement exclu de la rotation.
Une bonne pratique consiste à marquer automatiquement les proxies comme "en quarantaine" pendant 15 à 30 minutes après plusieurs erreurs 429 consécutives provenant de cette IP, et à les réintroduire dans le pool uniquement après une requête de test réussie. Cela empêche la situation où une IP bannie continue à recevoir des tâches et à faire perdre du temps aux workers.
Conclusion
La file d'attente de tâches résout le problème de mise à l'échelle des requêtes, mais à elle seule, elle ne protège pas contre la limite par IP — pour cela, une rotation réfléchie des proxies au sein de Celery, RQ ou BullMQ est nécessaire. Le choix de l'outil dépend de la pile : les projets Python choisissent souvent Celery pour la production et RQ pour des tâches simples, tandis que les développeurs Node.js optent pour BullMQ avec son limiteur de taux intégré.
Quelle que soit la file choisie, le résultat dépend fortement de la qualité du pool de proxies lui-même. Si la tâche consiste en un parsing massif rapide, envisagez des proxies de data center pour la vitesse, et pour travailler avec des réseaux sociaux et des marketplaces, où la loyauté des systèmes anti-bots est importante, des IP résidentielles ou mobiles conviendront — elles réduisent le pourcentage d'erreurs 429 et de bannissements, ce qui diminue directement le nombre de tentatives de réessai et accélère l'exécution de l'ensemble de la file d'attente des tâches.