Éthique & Perf

    Concurrency Control

    Mis à jour le 1 février 2026

    Définition

    Le Concurrency Control régule le nombre de requêtes simultanées envoyées à un serveur lors du scraping. Une concurrence trop élevée peut surcharger le serveur cible (DoS involontaire), déclencher des blocages IP, ou violer les conditions d'utilisation. Une gestion intelligente équilibre vitesse et respect du serveur.

    Deep Dive

    Concurrency Control : l'art de scraper sans nuire

    Pourquoi limiter la concurrence ?

    **Risques d'une concurrence excessive :**

  1. Surcharge du serveur cible (temps de réponse dégradé)
  2. Blocage IP temporaire ou permanent
  3. Détection comme attaque DDoS
  4. Violation des CGU et risques légaux
  5. **Bonnes pratiques par type de site :**

    | Type de site | Concurrence max | Délai entre requêtes |

    |--------------|-----------------|---------------------|

    | Petit site perso | 1-2 | 2-5 sec |

    | Site corporate | 2-5 | 1-2 sec |

    | E-commerce | 5-10 | 0.5-1 sec |

    | CDN/API publique | 20-50 | 100-500 ms |

    Stratégies avancées

    1. **Rate limiting adaptatif** : Ralentir si le serveur répond lentement

    2. **Backoff exponentiel** : Doubler le délai après chaque erreur

    3. **Respect du Crawl-Delay** : Lire robots.txt

    4. **Fingerprint par domaine** : Limites différentes par site

    Code Playground

    Contrôleur de concurrence adaptatif avec rate limiting par domaine et backoff automatique

    python
    # Système de concurrence adaptatif avec rate limiting intelligent
    import asyncio
    import time
    from dataclasses import dataclass, field
    from typing import Dict, Optional
    import httpx
    
    @dataclass
    class DomainState:
        """État de rate limiting pour un domaine"""
        concurrent_requests: int = 0
        max_concurrent: int = 5
        last_request_time: float = 0
        min_delay: float = 0.5
        current_delay: float = 0.5
        consecutive_errors: int = 0
        avg_response_time: float = 0.5
        request_count: int = 0
    
    class AdaptiveConcurrencyController:
        """Contrôleur de concurrence adaptatif par domaine"""
        
        def __init__(self):
            self.domains: Dict[str, DomainState] = {}
            self.locks: Dict[str, asyncio.Lock] = {}
        
        def _get_domain(self, url: str) -> str:
            from urllib.parse import urlparse
            return urlparse(url).netloc
        
        def _get_state(self, domain: str) -> DomainState:
            if domain not in self.domains:
                self.domains[domain] = DomainState()
                self.locks[domain] = asyncio.Lock()
            return self.domains[domain]
        
        async def acquire(self, url: str):
            """Attend et acquiert un slot de concurrence"""
            domain = self._get_domain(url)
            state = self._get_state(domain)
            
            async with self.locks[domain]:
                # Attendre si trop de requêtes en cours
                while state.concurrent_requests >= state.max_concurrent:
                    await asyncio.sleep(0.1)
                
                # Respecter le délai minimum
                elapsed = time.time() - state.last_request_time
                if elapsed < state.current_delay:
                    await asyncio.sleep(state.current_delay - elapsed)
                
                state.concurrent_requests += 1
                state.last_request_time = time.time()
        
        def release(self, url: str, response_time: float, success: bool):
            """Libère un slot et ajuste les paramètres"""
            domain = self._get_domain(url)
            state = self._get_state(domain)
            
            state.concurrent_requests -= 1
            state.request_count += 1
            
            # Mise à jour du temps de réponse moyen (moyenne mobile)
            alpha = 0.2
            state.avg_response_time = alpha * response_time + (1 - alpha) * state.avg_response_time
            
            if success:
                state.consecutive_errors = 0
                # Accélérer si le serveur répond vite
                if state.avg_response_time < 0.3 and state.current_delay > state.min_delay:
                    state.current_delay = max(state.min_delay, state.current_delay * 0.9)
                # Augmenter la concurrence si tout va bien
                if state.request_count % 100 == 0 and state.avg_response_time < 0.5:
                    state.max_concurrent = min(20, state.max_concurrent + 1)
            else:
                state.consecutive_errors += 1
                # Backoff exponentiel
                state.current_delay = min(30, state.current_delay * 2)
                # Réduire la concurrence en cas d'erreurs
                if state.consecutive_errors >= 3:
                    state.max_concurrent = max(1, state.max_concurrent // 2)
        
        def get_stats(self) -> Dict:
            return {
                domain: {
                    'concurrent': state.concurrent_requests,
                    'max_concurrent': state.max_concurrent,
                    'current_delay': round(state.current_delay, 2),
                    'avg_response': round(state.avg_response_time, 3),
                    'total_requests': state.request_count
                }
                for domain, state in self.domains.items()
            }
    
    # Client HTTP avec concurrence contrôlée
    class PoliteClient:
        def __init__(self):
            self.controller = AdaptiveConcurrencyController()
            self.client = httpx.AsyncClient(timeout=30)
        
        async def get(self, url: str) -> httpx.Response:
            await self.controller.acquire(url)
            
            start = time.time()
            success = False
            try:
                response = await self.client.get(url)
                success = response.status_code < 400
                return response
            finally:
                response_time = time.time() - start
                self.controller.release(url, response_time, success)
        
        async def close(self):
            await self.client.aclose()
    
    # Usage
    async def polite_scrape(urls: list):
        client = PoliteClient()
        
        async def fetch(url):
            try:
                response = await client.get(url)
                return {'url': url, 'status': response.status_code}
            except Exception as e:
                return {'url': url, 'error': str(e)}
        
        results = await asyncio.gather(*[fetch(url) for url in urls])
        
        print("Stats:", client.controller.get_stats())
        await client.close()
        return results

    Avis d'Expert 2026

    Le Concurrency Control différencie le scraping professionnel du "hit and run". En 2026, les entreprises responsables implémentent des systèmes adaptatifs qui respectent les serveurs cibles. La règle d'or : si vous ne voudriez pas que quelqu'un scrappe votre site à cette vitesse, ne le faites pas aux autres.

    Besoin d'aide sur Concurrency Control ?

    Nos experts peuvent vous accompagner sur vos projets de crawling et d'optimisation GEO.

    Contactez l'expert Crawlers.fr
    4.6/5 (73 avis d'experts)