Home Python Scrapy: Production Web Scraping Framework in Python
Advanced 5 min · July 14, 2026

Scrapy: Production Web Scraping Framework in Python

Master Scrapy for production web scraping.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic knowledge of Python (variables, functions, classes)
  • Familiarity with HTML and CSS selectors
  • Understanding of HTTP requests and responses
  • Python installed (3.6 or higher)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Scrapy is a high-level Python framework for extracting data from websites efficiently.
  • It handles requests asynchronously, supports middleware, and exports data in multiple formats.
  • Ideal for large-scale scraping projects with built-in features like throttling and retries.
  • Production use requires handling JavaScript, proxies, and anti-bot measures.
  • Debugging common issues like 403 errors, infinite redirects, and memory leaks is critical.
✦ Definition~90s read
What is Scrapy?

Scrapy is a high-level Python framework for extracting data from websites efficiently, handling requests asynchronously, and providing built-in support for data pipelines, middleware, and exporters.

Think of Scrapy as a smart robot librarian that can fetch thousands of books (web pages) simultaneously, extract the information you need, and organize it neatly into a spreadsheet.
Plain-English First

Think of Scrapy as a smart robot librarian that can fetch thousands of books (web pages) simultaneously, extract the information you need, and organize it neatly into a spreadsheet. It follows rules to avoid disturbing the library (respecting robots.txt) and can even wait politely between requests (download delays).

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Web scraping is a cornerstone of data-driven applications, from price monitoring to lead generation. While you can scrape with libraries like Requests and BeautifulSoup, production-grade scraping demands a framework that handles concurrency, retries, middleware, and data pipelines. Enter Scrapy: an asynchronous framework built for speed and reliability.

Scrapy is not just a library; it's a complete ecosystem. It provides a spider architecture, item pipelines, exporters, and a built-in shell for testing. With Scrapy, you can scrape thousands of pages per minute while respecting website policies and avoiding bans.

In this tutorial, you'll learn how to build a production-ready Scrapy spider from scratch. We'll cover real-world challenges: handling JavaScript-rendered content, rotating user agents and proxies, managing cookies, and debugging common failures. By the end, you'll be able to deploy a spider that runs reliably in a production environment.

We'll use a fictional e-commerce site as our target. The code examples are tested and ready to adapt. Let's dive into the world of production web scraping with Scrapy.

1. Setting Up Scrapy Project

Before writing spiders, you need to create a Scrapy project. This organizes your code into a standard structure with settings, spiders, items, and pipelines.

First, install Scrapy using pip. It's recommended to use a virtual environment to avoid conflicts.

``bash pip install scrapy ``

``bash scrapy startproject myproject cd myproject ``

This creates the following structure
  • myproject/
  • main module
  • myproject/spiders/
  • where you put your spiders
  • myproject/items.py
  • define data models
  • myproject/pipelines.py
  • process scraped items
  • myproject/settings.py
  • configuration
  • scrapy.cfg
  • project configuration file

``bash scrapy genspider example example.com ``

This creates a basic spider in spiders/example.py. You can edit it to define your scraping logic.

Production Tip: Always use a dedicated virtual environment and pin your dependencies in a requirements.txt file. This ensures reproducibility across environments.

setup.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
# Create a virtual environment
python -m venv scrapy_env
source scrapy_env/bin/activate  # On Windows: scrapy_env\Scripts\activate

# Install Scrapy
pip install scrapy

# Start a project
scrapy startproject myproject
cd myproject

# Generate a spider
scrapy genspider example example.com
Output
New Scrapy project 'myproject' created.
Created spider 'example' using template 'basic' in module:
myproject.spiders.example
💡Use a Virtual Environment
📊 Production Insight
In production, you might want to use Docker to containerize your Scrapy project. This ensures consistent behavior across different servers.
🎯 Key Takeaway
Scrapy projects are organized with a standard structure. Use scrapy startproject to create one and scrapy genspider to create a spider template.

2. Writing Your First Spider

A spider is a class that defines how to scrape a website. It must subclass scrapy.Spider and define the name, start_urls, and parse method.

```python import scrapy

class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ 'http://quotes.toscrape.com/page/1/', ]

def parse(self, response): for quote in response.css('div.quote'): yield { 'text': quote.css('span.text::text').get(), 'author': quote.css('small.author::text').get(), 'tags': quote.css('div.tags a.tag::text').getall(), } # Follow pagination link next_page = response.css('li.next a::attr(href)').get() if next_page is not None: yield response.follow(next_page, callback=self.parse) ```

Key points
  • start_urls is a list of initial URLs.
  • parse is the default callback for responses.
  • Use CSS selectors or XPath to extract data.
  • yield returns scraped items or new requests.
  • response.follow creates a new request from a relative URL.

``bash scrapy crawl quotes -o quotes.json ``

This outputs scraped data to quotes.json.

Production Tip: Use scrapy crawl quotes -o quotes.jl for JSON Lines format, which is append-friendly and easier to process in pipelines.

spiders/quotes_spider.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }
        
        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)
Output
[{"text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”", "author": "Albert Einstein", "tags": ["change", "deep-thoughts", "thinking", "world"]}, ...]
🔥CSS vs XPath
📊 Production Insight
In production, avoid hardcoding start_urls. Instead, read them from a database or a file to allow dynamic updates without redeploying the spider.
🎯 Key Takeaway
A spider defines how to extract data and follow links. Use yield to return items or requests. Always handle pagination to scrape multiple pages.

3. Using Item Pipelines for Data Processing

Item pipelines are used to process scraped items after they are yielded by spiders. Common tasks include cleaning, validating, deduplicating, and storing data.

```python import scrapy

class QuoteItem(scrapy.Item): text = scrapy.Field() author = scrapy.Field() tags = scrapy.Field() ```

```python class CleanDataPipeline: def process_item(self, item, spider): # Strip whitespace from text fields item['text'] = item['text'].strip() item['author'] = item['author'].strip() # Convert tags to lowercase item['tags'] = [tag.strip().lower() for tag in item['tags']] return item

class DuplicatesPipeline: def __init__(self): self.seen = set()

def process_item(self, item, spider): if item['text'] in self.seen: raise DropItem(f"Duplicate item found: {item}") else: self.seen.add(item['text']) return item ```

``python ITEM_PIPELINES = { 'myproject.pipelines.CleanDataPipeline': 300, 'myproject.pipelines.DuplicatesPipeline': 400, } ``

The numbers define the order (lower numbers run first).

Production Tip: Use a database pipeline to store items directly into PostgreSQL or MongoDB. For high throughput, consider using a message queue like RabbitMQ between Scrapy and your storage.

pipelines.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from scrapy.exceptions import DropItem

class CleanDataPipeline:
    def process_item(self, item, spider):
        item['text'] = item['text'].strip()
        item['author'] = item['author'].strip()
        item['tags'] = [tag.strip().lower() for tag in item['tags']]
        return item

class DuplicatesPipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        if item['text'] in self.seen:
            raise DropItem(f"Duplicate item found: {item}")
        else:
            self.seen.add(item['text'])
            return item
⚠ Pipeline Order Matters
📊 Production Insight
In production, avoid storing large datasets in memory (like the duplicates set). Use a Redis set for distributed deduplication across multiple spider instances.
🎯 Key Takeaway
Item pipelines allow you to clean, validate, and store scraped data. Use multiple pipelines for separation of concerns.

4. Handling JavaScript-Rendered Content with Splash

Many modern websites rely on JavaScript to render content. Scrapy's default HTTP client cannot execute JavaScript. To handle this, you can use Scrapy-Splash, which integrates the Splash JavaScript rendering service.

``bash pip install scrapy-splash ``

```python SPLASH_URL = 'http://localhost:8050' # Splash instance

DOWNLOADER_MIDDLEWARES = { 'scrapy_splash.SplashCookiesMiddleware': 723, 'scrapy_splash.SplashMiddleware': 725, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810, }

SPIDER_MIDDLEWARES = { 'scrapy_splash.SplashDeduplicateArgsMiddleware': 100, }

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter' ```

```python import scrapy from scrapy_splash import SplashRequest

class JavaScriptSpider(scrapy.Spider): name = "js_spider" start_urls = ['https://example.com/js-page']

def start_requests(self): for url in self.start_urls: yield SplashRequest(url, self.parse, args={'wait': 2})

def parse(self, response): # Now response contains the rendered HTML yield { 'title': response.css('h1::text').get(), } ```

The wait argument tells Splash to wait for 2 seconds after page load to allow JavaScript to execute.

Production Tip: Run Splash in a Docker container. Use a pool of Splash instances behind a load balancer for high traffic.

spiders/js_spider.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import scrapy
from scrapy_splash import SplashRequest

class JavaScriptSpider(scrapy.Spider):
    name = "js_spider"
    start_urls = ['https://example.com/js-page']

    def start_requests(self):
        for url in self.start_urls:
            yield SplashRequest(url, self.parse, args={'wait': 2})

    def parse(self, response):
        yield {
            'title': response.css('h1::text').get(),
        }
Output
[{"title": "Dynamic Content Loaded"}]
🔥Alternatives to Splash
📊 Production Insight
Splash can be a bottleneck. Monitor its resource usage and scale horizontally. Consider caching rendered pages to reduce load.
🎯 Key Takeaway
For JavaScript-heavy sites, use Splash or similar tools to render pages before scraping. Configure middleware and use SplashRequest.

5. Avoiding Bans: Rotating User Agents and Proxies

Websites often block scrapers that send too many requests from the same IP with the same User-Agent. To avoid bans, you need to rotate user agents and proxies.

User-Agent Rotation:

```python # middlewares.py import random from scrapy import signals

class RandomUserAgentMiddleware: def __init__(self, user_agents): self.user_agents = user_agents

@classmethod def from_crawler(cls, crawler): return cls(user_agents=crawler.settings.get('USER_AGENTS_LIST'))

def process_request(self, request, spider): request.headers['User-Agent'] = random.choice(self.user_agents) ```

```python USER_AGENTS_LIST = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', # Add more real user agents ]

DOWNLOADER_MIDDLEWARES = { 'myproject.middlewares.RandomUserAgentMiddleware': 400, } ```

Proxy Rotation:

Use scrapy-proxy-pool or a custom middleware. For example, with a list of proxies:

```python class RandomProxyMiddleware: def __init__(self, proxies): self.proxies = proxies

@classmethod def from_crawler(cls, crawler): return cls(proxies=crawler.settings.get('PROXY_LIST'))

def process_request(self, request, spider): request.meta['proxy'] = random.choice(self.proxies) ```

Production Tip: Use a proxy service like Luminati or Smartproxy that provides rotating residential IPs. Avoid free proxies as they are unreliable and often blocked.

middlewares.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import random

class RandomUserAgentMiddleware:
    def __init__(self, user_agents):
        self.user_agents = user_agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(user_agents=crawler.settings.get('USER_AGENTS_LIST'))

    def process_request(self, request, spider):
        request.headers['User-Agent'] = random.choice(self.user_agents)

class RandomProxyMiddleware:
    def __init__(self, proxies):
        self.proxies = proxies

    @classmethod
    def from_crawler(cls, crawler):
        return cls(proxies=crawler.settings.get('PROXY_LIST'))

    def process_request(self, request, spider):
        request.meta['proxy'] = random.choice(self.proxies)
⚠ Respect robots.txt
📊 Production Insight
In production, monitor your proxy pool's health. Remove dead proxies and add new ones dynamically. Use a service that provides backconnect proxies for better reliability.
🎯 Key Takeaway
Rotate user agents and proxies to avoid detection. Use middleware to inject them into requests. Always respect robots.txt.

6. Scaling Scrapy with Settings and Extensions

Scrapy provides numerous settings to control concurrency, delays, and resource usage. Proper tuning is essential for production.

  • CONCURRENT_REQUESTS: Number of simultaneous requests (default 16). Increase for faster scraping, but beware of bans.
  • DOWNLOAD_DELAY: Delay between consecutive requests (seconds). Use to throttle requests.
  • AUTOTHROTTLE_ENABLED: Automatically adjusts delay based on server response times.
  • COOKIES_ENABLED: Enable if the site uses cookies for sessions.
  • RETRY_ENABLED: Retry failed requests.
  • DOWNLOAD_TIMEOUT: Timeout for requests.

``python # settings.py CONCURRENT_REQUESTS = 32 DOWNLOAD_DELAY = 1 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 5 AUTOTHROTTLE_MAX_DELAY = 60 COOKIES_ENABLED = False RETRY_ENABLED = True RETRY_TIMES = 3 DOWNLOAD_TIMEOUT = 30 ``

Extensions: Scrapy has built-in extensions for logging, stats collection, and memory usage. You can also write custom extensions.

```python # extensions.py from scrapy import signals import psutil

class MemoryUsageExtension: def __init__(self, crawler): self.crawler = crawler

@classmethod def from_crawler(cls, crawler): ext = cls(crawler) crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed) return ext

def spider_closed(self, spider): mem = psutil.Process().memory_info().rss / (1024 ** 2) spider.logger.info(f"Memory usage: {mem:.2f} MB") ```

``python EXTENSIONS = { 'myproject.extensions.MemoryUsageExtension': 500, } ``

Production Tip: Use scrapy crawl spider -s LOG_LEVEL=INFO to control logging verbosity. In production, set LOG_FILE to write logs to a file for debugging.

settings.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# settings.py
BOT_NAME = 'myproject'

SPIDER_MODULES = ['myproject.spiders']
NEWSPIDER_MODULE = 'myproject.spiders'

ROBOTSTXT_OBEY = True

CONCURRENT_REQUESTS = 32
DOWNLOAD_DELAY = 1
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
COOKIES_ENABLED = False
RETRY_ENABLED = True
RETRY_TIMES = 3
DOWNLOAD_TIMEOUT = 30

ITEM_PIPELINES = {
    'myproject.pipelines.CleanDataPipeline': 300,
    'myproject.pipelines.DuplicatesPipeline': 400,
}

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.RandomUserAgentMiddleware': 400,
    'myproject.middlewares.RandomProxyMiddleware': 500,
}
💡Autothrottle is Your Friend
📊 Production Insight
In production, use a configuration management system to adjust settings without redeploying. For example, use environment variables or a config file.
🎯 Key Takeaway
Tune Scrapy settings for your target site. Use autothrottle, set appropriate delays, and monitor memory usage with extensions.

7. Deploying Scrapy Spiders to Production

Deploying Scrapy spiders involves running them on a server or cloud platform. Common approaches include:

  1. Scrapyd: A service to run Scrapy spiders remotely. Install with pip install scrapyd and start the server. Then deploy your project using scrapyd-deploy.
  2. Docker: Containerize your spider and run it on any platform (AWS, GCP, Azure).
  3. Serverless: Use AWS Lambda with scrapy-lambda or similar.

Docker example:

```dockerfile FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["scrapy", "crawl", "quotes"] ```

``bash docker build -t my-scrapy-spider . docker run my-scrapy-spider ``

Scheduling: Use cron jobs or a scheduler like Apache Airflow to run spiders periodically.

Monitoring: Integrate with logging services (e.g., ELK stack) and set up alerts for failures.

Production Tip: Use a secrets manager (e.g., AWS Secrets Manager) to store API keys, proxy credentials, and database passwords. Never hardcode them.

DockerfilePYTHON
1
2
3
4
5
6
7
8
9
10
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["scrapy", "crawl", "quotes"]
🔥Scrapyd vs Docker
📊 Production Insight
In production, implement health checks and automatic restarts. Use a process manager like Supervisor to keep your spider running.
🎯 Key Takeaway
Deploy Scrapy spiders using Docker, Scrapyd, or serverless. Use scheduling and monitoring for production reliability.
● Production incidentPOST-MORTEMseverity: high

The 403 Forbidden Nightmare: When Scrapy Gets Blocked

Symptom
All requests return HTTP 403 Forbidden after running successfully for weeks.
Assumption
The developer assumed the website's anti-bot system was triggered by request frequency.
Root cause
The website updated its TLS fingerprinting and started blocking the default Scrapy TLS handshake.
Fix
Updated Scrapy to use a custom TLS fingerprint via the DOWNLOADER_CLIENT_TLS_METHOD setting and added a middleware to mimic a real browser's TLS fingerprint.
Key lesson
  • Always monitor response status codes and set up alerts for sudden changes.
  • Use realistic TLS fingerprints and headers to avoid detection.
  • Implement proxy rotation and request throttling from day one.
  • Keep Scrapy and its dependencies updated to benefit from security patches.
  • Test your spider against anti-bot services like Cloudflare before deployment.
Production debug guideSymptom to Action4 entries
Symptom · 01
Spider returns 403 Forbidden on all requests
Fix
Check if the site uses Cloudflare or similar. Update TLS settings and add a middleware to rotate user agents and headers.
Symptom · 02
Spider gets stuck on a single page (infinite redirects)
Fix
Enable REDIRECT_ENABLED=False temporarily to see the final URL. Check for redirect loops and set a maximum redirect depth.
Symptom · 03
Memory usage grows indefinitely
Fix
Use scrapy crawl spider -s MEMUSAGE_LIMIT_MB=512 to limit memory. Check for memory leaks in pipelines or middleware.
Symptom · 04
Items are duplicated or missing
Fix
Enable DUPEFILTER_CLASS and check your item pipeline for errors. Use scrapy crawl spider -o output.json -t json to inspect output.
★ Quick Debug Cheat SheetCommon Scrapy issues and immediate actions
403 Forbidden
Immediate action
Check if site blocks Scrapy's default User-Agent. Rotate user agents and enable cookies.
Commands
scrapy shell 'https://example.com' -s USER_AGENT='Mozilla/5.0'
fetch('https://example.com')
Fix now
Add a middleware to rotate user agents and use a real browser's TLS fingerprint.
Infinite redirects+
Immediate action
Disable redirects temporarily to see the final URL.
Commands
scrapy crawl spider -s REDIRECT_ENABLED=False
Check response.meta['redirect_urls'] in spider
Fix now
Set REDIRECT_MAX_TIMES=5 and handle redirects explicitly.
Memory leak+
Immediate action
Limit memory usage and enable memory debugging.
Commands
scrapy crawl spider -s MEMUSAGE_LIMIT_MB=512 -s MEMUSAGE_NOTIFY_MAIL=[]
Use `scrapy crawl spider -s MEMUSAGE_REPORT=True`
Fix now
Check for unclosed connections or large caches in pipelines.
FeatureScrapyBeautifulSoupSelenium
ConcurrencyBuilt-in asyncNoneLimited (thread-based)
JavaScript RenderingVia Splash/PlaywrightNoYes
Data PipelinesBuilt-inNoNo
MiddlewareYesNoNo
Ease of UseModerateEasyModerate
Best ForLarge-scale scrapingSmall projectsDynamic sites
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
setup.pypython -m venv scrapy_env1. Setting Up Scrapy Project
spidersquotes_spider.pyclass QuotesSpider(scrapy.Spider):2. Writing Your First Spider
pipelines.pyfrom scrapy.exceptions import DropItem3. Using Item Pipelines for Data Processing
spidersjs_spider.pyfrom scrapy_splash import SplashRequest4. Handling JavaScript-Rendered Content with Splash
middlewares.pyclass RandomUserAgentMiddleware:5. Avoiding Bans
settings.pyBOT_NAME = 'myproject'6. Scaling Scrapy with Settings and Extensions
DockerfileFROM python:3.9-slim7. Deploying Scrapy Spiders to Production

Key takeaways

1
Scrapy is a powerful framework for production web scraping, offering concurrency, middleware, and pipelines.
2
Always respect robots.txt and use polite scraping practices like delays and autothrottle.
3
Handle JavaScript-rendered content with Splash or similar tools.
4
Rotate user agents and proxies to avoid bans.
5
Deploy spiders using Docker or Scrapyd, and monitor them with logging and alerts.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how Scrapy handles concurrency. What is the reactor pattern?
Q02SENIOR
How would you implement a spider that scrapes a paginated website with i...
Q03JUNIOR
What is the purpose of item pipelines? Give an example of a pipeline tha...
Q04SENIOR
How do you handle cookies and sessions in Scrapy?
Q05SENIOR
Describe a scenario where you would use a custom middleware in Scrapy.
Q01 of 05SENIOR

Explain how Scrapy handles concurrency. What is the reactor pattern?

ANSWER
Scrapy uses Twisted, an asynchronous networking library, to handle concurrency. It uses a reactor pattern where a single thread manages multiple connections via an event loop. This allows Scrapy to send many requests concurrently without blocking, improving throughput.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Scrapy and BeautifulSoup?
02
How do I handle CAPTCHAs in Scrapy?
03
Can Scrapy scrape websites that require login?
04
How do I export scraped data to a database?
05
What is the best way to avoid getting banned while scraping?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Python Libraries. Mark it forged?

5 min read · try the examples if you haven't

Previous
CLI Applications in Python with Click and Typer
68 / 69 · Python Libraries
Next
Pydantic v2: Rust-Powered Data Validation