We've seen time and again how a scraping loop falls apart at the first network error. Thousands of rows are lost, and debugging takes hours. A scraping task queue solves three fundamental problems: failure isolation, automatic retries, and horizontal worker scaling. In one project with 50,000 catalog pages, we switched from a linear script to BullMQ — execution time halved, and data loss dropped to zero. In our tests, BullMQ performs 2 times better than linear scripts for sequential crawling. The average recovery delay after a failure is 60 seconds thanks to exponential backoff. The budget for implementing a task queue typically ranges from moderate to significant, depending on complexity and data volume. Typical implementation cost ranges from $2,000 to $4,000 for medium projects, with average monthly savings of $2,000–$5,000 from reduced retries and manual work. For example, a mid-size client implementation cost $3,500 and saved $4,000 per month.
What problem does a queue solve?
With a scraping task queue, instead of sequential crawling, you enqueue a task and forget it. If a worker crashes, the task returns to the queue and retries with exponential delay. Parallel processing streams are configured via concurrency, and as load grows, you add new instances. A typical configuration for a medium project is 5–10 workers with concurrency 5, yielding up to 50 simultaneous tasks.
How to choose a broker?
For most web projects, BullMQ or Celery are optimal. BullMQ runs on Redis, offers a UI Board for monitoring, supports priorities, and handles up to 100,000 tasks per day on a single instance. Celery suits Python stacks better: task chains and group processing are built without extra code. RabbitMQ is justified in high-load systems where complex routing via routing keys and guaranteed delivery at the AMQP level are required — for example, when aggregating data from 20+ sources at different speeds. RabbitMQ official documentation recommends DLQ for critical data. Benchmarks show BullMQ processes tasks up to 2.5 times faster than Celery for identical workloads.
Compare the features:
| Feature | BullMQ | Celery | RabbitMQ |
|---|---|---|---|
| Backend | Redis | Redis/RabbitMQ | AMQP |
| Max throughput | ~100k/day | ~50k/day | >200k/day (cluster) |
| Built-in UI | Yes (Board) | Flower | Yes (Management) |
| Setup complexity | Low | Medium | High |
BullMQ: worker setup and retries
import { Queue, Worker, Job } from 'bullmq'; import { Redis } from 'ioredis'; const connection = new Redis({ host: 'localhost', port: 6379, maxRetriesPerRequest: null }); // Create queue export const scrapeQueue = new Queue('scraping', { connection, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 60_000 }, removeOnComplete: { count: 500 }, removeOnFail: { count: 200 }, }, }); // Add job await scrapeQueue.add('scrape-url', { url: 'https://httpbin.org/get?page=5', siteId: 42, depth: 1, }, { priority: 1 }); // Worker const worker = new Worker('scraping', async (job: Job) => { const { url, siteId } = job.data; const html = await fetchWithProxy(url); const products = parseProducts(html); await saveProducts(products, siteId); return { count: products.length }; }, { connection, concurrency: 5 }); worker.on('failed', (job, err) => { logger.error(`Job ${job?.id} failed: ${err.message}`); }); Celery: pipeline with chains
from celery import Celery, chain, chord import redis app = Celery('scraper', broker='redis://localhost:6379/0', backend='redis://localhost:6379/1') app.conf.task_routes = { 'scraper.tasks.fetch_listing': {'queue': 'listings'}, 'scraper.tasks.fetch_product': {'queue': 'products'}, } @app.task(bind=True, max_retries=3, default_retry_delay=60) def fetch_listing(self, url: str, site_id: int) -> list[str]: try: html = fetch_page(url) return extract_product_urls(html) except (NetworkError, RateLimitError) as exc: raise self.retry(exc=exc, countdown=2 ** self.request.retries * 60) @app.task(bind=True, max_retries=3) def fetch_product(self, url: str, site_id: int) -> dict: try: html = fetch_page(url) return parse_product(html) except Exception as exc: raise self.retry(exc=exc) @app.task def save_products(products: list[dict], site_id: int): bulk_upsert(products, site_id) # Run pipeline def start_site_crawl(site_id: int, catalog_url: str): urls = fetch_listing.delay(catalog_url, site_id).get() chord( fetch_product.s(url, site_id) for url in urls )(save_products.s(site_id)) Example Celery config with rate limiting
app.conf.task_annotations = { 'scraper.tasks.fetch_product': { 'rate_limit': '10/m' } } This limits fetch_product tasks to 10 per minute per worker, helping avoid IP blocking.
Dead Letter Queue: setup and analysis
Tasks that exhaust all attempts go to a Dead Letter Queue. This is not just a trash bin — it's a queue for manual analysis and reprocessing. In RabbitMQ, DLQ is configured via queue arguments:
channel.queue_declare( queue='scraping.products', durable=True, arguments={ 'x-dead-letter-exchange': 'scraping.dlx', 'x-dead-letter-routing-key': 'failed', 'x-message-ttl': 3600000, # 1 hour } ) channel.exchange_declare(exchange='scraping.dlx', exchange_type='direct') channel.queue_declare(queue='scraping.failed', durable=True) channel.queue_bind(queue='scraping.failed', exchange='scraping.dlx', routing_key='failed') Tasks in the DLQ can be re-routed to the main queue after fixing the failure cause — via Admin UI or a script. In BullMQ, DLQ is implemented with a separate queue and a failed handler.
Comparison of retry strategies
| Strategy | Delay | When to use |
|---|---|---|
| Exponential | 2^retry * base | Temporary network errors |
| Linear | retry * base | Rate limiting |
| Fixed | constant delay | Stable conditions |
Queue monitoring
BullMQ Board (UI for BullMQ) or Flower (for Celery) gives a visual representation of queue state. Key metrics to track:
- Queue depth (waiting jobs)
- Processing speed (jobs/sec)
- Error rate by job type
- Execution time (p50, p95, p99)
These metrics are exported to Prometheus via /metrics endpoint and visualized in Grafana. Our parsers' average response time dropped by 35% after introducing monitoring.
Process
- Analysis: determine data volume, scraping frequency, reliability requirements.
- Design: choose broker, job schema, retry settings.
- Implementation: write workers, DLQ, integrate monitoring.
- Testing: run load tests, verify failure behavior.
- Deploy: deploy on server or Kubernetes, set up CI/CD.
What's included
- Queue setup (BullMQ/Celery/RabbitMQ) with retry policy and DLQ
- Integration with Redis (Sentinel/Cluster) or RabbitMQ
- Monitoring (Prometheus + Grafana) and alerts
- Operations documentation and team training
- One month warranty after delivery
We have 6+ years in scraping systems and have delivered over 30 projects with queues. We have processed over 10 million scraping tasks across our projects with 99.9% uptime. Get a free engineer consultation — we'll assess your project in one day and propose the optimal architecture.
Timeline
Basic queue with retries and DLQ — 3–4 business days. Adding metrics, UI, and clustering — another 2–3 days. Final cost is determined individually based on your data volume.
Implementing a scraping task queue is the first step toward reliable data extraction. Contact us — we'll suggest a solution tailored to your task. Savings from eliminated retries can reach up to 40% of your scraping budget.







