How to build a Python database scraper that survives auth, pagination, and rate limits
Last reviewed: 2026-05-23 · 20 min read read · WebScrapingTool.net
How to build a Python database scraper that survives auth, pagination, and rate limits
A production-ready guide to scraping directory sites with Requests or Scrapy, storing results in SQLite or PostgreSQL, and automating the pipeline with cron or GitHub Actions.
Maxime Yao, research editor · Published 2026-05-23
Research Note & TL;DR
Last updated: June 2025
This guide synthesizes documented evidence across the web scraping ecosystem. It does not pretend to be first-person testing. The recommendations are drawn from published case studies, framework documentation, and industry analyses.
The short version:
-
10–15% of crawlers require weekly fixes due to DOM drift and throttling. Most scrapers fail because they are scripts, not infrastructure.
-
This guide builds a Production Scraper Stack: a scraper that survives auth, pagination, rate limits, and automation.
-
Two paths exist: Requests+BeautifulSoup for small jobs, Scrapy for scale. Choose based on your buyer archetype.
-
Rate limiting (exponential backoff, proxy rotation) is non-negotiable. HTTP 429 is your signal to pause.
-
Store in PostgreSQL for concurrent pipelines; SQLite for single-user portability.
-
Schedule with cron or GitHub Actions. A laptop that sleeps is not infrastructure.
Memory line: Scraping is infrastructure. Treat it that way.
Who this is for: Backend engineers and data scientists building repeatable pipelines. Not for: one-off CSV exports or quick prototypes.
1. The Failure Rate That Should Scare You
You think writing a scraper is a one-time task. The data says otherwise.
10–15% of crawlers in some industries require weekly fixes. The culprit: DOM drift. Changes in HTML structure that break the selectors your script depends on. A single CSS class rename can corrupt terabytes of data downstream. 1
The global web scraping market is $1.03B and growing at 14.2% CAGR, projected to hit $2.00B by 2030. Yet most scrapers fail within weeks because they are treated as throwaway scripts, not production systems. 1
One company proved the fix exists. A fintech aggregator adopted version-aware selectors. Selectors that carry a version stamp tied to the page’s HTML attributes. Result: 73% less downtime over two months, even with 14 frontend hotfixes deployed in that period. 1
The pattern is clear:
| Challenge | Stat from the field |
|---|---|
| DOM drift frequency | 10–15% of crawlers need weekly fixes 1 |
| Downtime reduction with version-aware selectors | 73% over 2 months despite 14 hotfixes 1 |
| Market growth | $1.03B → $2.00B by 2030, 14.2% CAGR 1 |
| Required mindset shift | Web scraping at scale is infrastructure, not a side project 2 |
Every row points to the same conclusion: treating a scraper as a script guarantees rework. The infrastructure approach. Selectors that survive DOM changes, retry logic that handles throttling, scheduling that doesn’t depend on a laptop. Is what separates a one-time export from a pipeline that runs for months.
A scraper that breaks in a week is not a scraper; it is a recurring problem.
For our worked example. Commercial real estate lease data from a public MLS directory. The same failure modes apply. A script that fetches listings once will work. Add pagination, authentication, and a weekly schedule, and you are in the 10–15% zone unless you architect deliberately.
Action this week: Audit your last three scraper projects. Count how many died from a DOM change, a rate limit, or a missing auth token. That number is your cost of treating scraping as a script instead of infrastructure.
2. Requests+BS vs Scrapy: Which Base to Pick
Requests+BeautifulSoup is easy. So is writing a check that bounces. For a paginated commercial real estate lease directory, the simplicity of a raw script evaporates the moment you need throttling, retry logic, or structured output.
The brick: Requests+BS is a prototype. Scrapy is a pipeline.
Here is a minimal Scrapy spider that fetches paginated lease listings from the MLS directory:
import scrapy
Class LeaseSpider(scrapy.Spider):
name = "leases"
start_urls = ["https://mls.example.com/leases?page=1"]
def parse(self, response):
for lease in response.css("div.lease-listing"):
yield {
"address": lease.css("span.address::text").get,
"sqft": lease.css("td.sqft::text").get,
"rate": lease.css("td.rate::text").get,
}
next_page = response.css("a.next::attr(href)").get
if next_page:
yield scrapy.Request(response.urljoin(next_page))
Scrapy adds auto-throttling, retry middleware, and feed exporters (CSV, JSON, SQLite, PostgreSQL. ) out of the box. Requests+BS requires you to hand-roll all of that.
For the worked example. Daily extraction of lease data from a public MLS directory. The choice depends on your archetype:
| Archetype | Approach | Why |
|---|---|---|
| Data analyst (one-off CSV) | Requests+BS | Simpler to write, no framework overhead |
| Backend engineer (daily pipeline) | Scrapy | Built-in scheduling, auto-throttle, retry queue |
| Startup founder (prototype) | Requests+BS → migrate to Scrapy | Validate demand fast, then harden |
The same MLS directory served with Requests+BS would need manual asyncio for concurrency, custom retry logic, and a home-grown exporter. With Scrapy, those are framework primitives.
Memory line: Requests+BS is a prototype. Scrapy is a pipeline.
Action this week: Run scrapy startproject mls_scraper and port your existing script into a Scrapy spider before the next DOM change hits.
3. Rate Limiting: The 429 Trap and How to Escape It
Nearly half of all internet traffic comes from bots. Sites defend against them with rate limits. The result: HTTP 429 Too Many Requests. Soft limit? The site injects artificial delays. Hard limit? 429 or 403. Permanent ban? Blacklisted IP, account, or subnet.
Most scrapers treat 429 as a failure. That is wrong. 429 is not a failure; it is a signal to back off and rotate.
The standard production pattern is a three-stage retry queue:
- Backoff. Wait 2^i seconds (exponential backoff). If the response includes a Retry-After header, use that value instead.
- Rotate proxy. Change the IP, User-Agent, and optionally the TLS fingerprint.
- Escalate. Log the failure and alert a human. The request is dead.
Here is a minimal implementation for the requests library:
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
Session = requests.Session
retries = Retry(
total=3,
backoff_factor=2, # 1, 2, 4 seconds
status_forcelist=[429],
raise_on_status=True
)
adapter = HTTPAdapter(max_retries=retries)
session.mount('http://', adapter)
session.mount('https://', adapter)
For Scrapy, enable the built-in AutoThrottle extension (CONCURRENT_REQUESTS_PER_DOMAIN, AUTOTHROTTLE_ENABLED) and configure RETRY_HTTP_CODES to include 429.
Now apply this to the worked example: commercial real estate lease data from a public MLS directory. MLS sites often throttle aggressively. Expect soft limits (delayed responses) before hard 429s. A backend engineer building a pipeline should implement the three-stage queue before the first production run. A data scientist running weekly price checks needs at minimum exponential backoff. An enterprise team scraping millions of pages per day needs smart identity layers that rotate fingerprints, not just IPs.
The moat here is integrated anti-bot bypass. Rich observability. Logging each retry stage. Lets you detect when the site has changed its throttling pattern. Without that, you are flying blind.
Action this week: Add exponential backoff and proxy rotation to your scraper before the first production run. Start with the three lines of code above. Test against a real 429 endpoint.
4. Authentication: Session Cookies, OAuth, and Login Flows
Login once. Persist session. No credential soup.
Most directory sites gate data behind a login. Hardcode credentials in the script, and the first password rotation breaks everything. Store them in plaintext, and one commit leak exposes your infrastructure.
The fix: treat authentication as a persistent connection, not a one-time handshake. Use requests.Session for manual scrapers or Scrapy’s FormRequest.from_response to simulate login flows. Both maintain cookies across requests. Hook a token refresh routine into the retry queue. If a 403 arrives, replay the login step before escalating.
A code block for a Scrapy spider that logs into the MLS directory and fetches lease listings:
class LeaseSpider(scrapy.Spider):
name = 'lease_spider'
start_urls = ['https://mls-example.com/login']
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={'username': self.settings['USERNAME'],
'password': self.settings['PASSWORD']},
callback=self.after_login
)
def after_login(self, response):
if "login failed" in response.text:
self.logger.error("Auth failed")
return
yield scrapy.Request(
url='https://mls-example.com/leases?page=1',
callback=self.parse_leases
)
Keep credentials in environment variables or a vault, not in source control. Scrapy settings can read os.environ.
| Auth method | Best for | Persistence mechanism |
|---|---|---|
| Session cookie | Simple login forms | requests.Session / Scrapy cookie jar |
| OAuth2 token | API-backed directories | Token stored in file or env, refreshed on expiry |
| Basic auth | HTTP-level gate | Headers passed every request |
For real production, bolt on a credential rotation schedule. A session is not a one-time login; it is a persistent connection. Start with requests.Session or FormRequest.from_response, and wrap the login in a function that the retry queue can call again.
Action this week: 1. Extract your login credentials into environment variables. 2. Replace bare requests.get calls with requests.Session in your prototype. 3. Add a 403 handler that triggers a re-login before the proxy rotation step.
5. Storage: SQLite vs PostgreSQL-Which One for Your Data
Storage is the last thing you think about and the first thing that corrupts your project. SQLite is a single file. PostgreSQL is a server. One dies under concurrent writes. The other does not.
Brick: SQLite-file, zero config, single writer. PostgreSQL-concurrent writes, complex config, validation built in.
Here is how they compare for a scraping pipeline:
| Aspect | SQLite | PostgreSQL |
|---|---|---|
| Setup | No install. File on disk. | Install server, create database, manage users. |
| Concurrency | Serializes writes. Fails under parallel pipelines. | Handles many concurrent readers/writers. |
| Validation | Must enforce in application code. | Constraints, triggers, data types built in. |
| Scaling | Works for single-machine, low-volume data. | Scales to terabytes and multiple pipelines. |
| Best for | Prototyping, data analyst one-offs, local dev. | Production, team data, backend engineer pipelines. |
Scrapy stores output in both via Feed Exporters 3. For the worked example (commercial real estate lease data), start with SQLite to validate schema. Use a schema like:
CREATE TABLE leases (
property_id TEXT PRIMARY KEY,
address TEXT NOT NULL,
lease_price DECIMAL(10,2),
scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
But do not stop there. A single selector change can corrupt terabytes of data if the database accepts malformed rows 2. Add validation rules on field presence, type, and range before every insert. A simple Python check:
if row.get("lease_price") and row["lease_price"] < 0:
log_error(f"Lease price negative: {row}")
continue
Storage is not an afterthought; it is the foundation of data quality.
Action this week: 1. Add validation rules (field presence, type, range) to your Scrapy Item Pipeline before the database write. 2. If you are prototyping, use SQLite. If more than one pipeline will write simultaneously, switch to PostgreSQL.
6. Automation: Cron vs GitHub Actions for Daily Runs
Local cron jobs fail when your laptop sleeps, loses internet, or runs out of battery. A scraper that depends on a personal machine is not automated; it is an experiment. Over 60% of data professionals use web scraping, and 85% of those automate it 4. The question is how.
GitHub Actions is the free, reliable path. For a public repository, minutes are unlimited. No server, no uptime worry. A single YAML file schedules your scraper to run daily at 3 AM UTC. The optimal time for commercial real estate lease sites, which typically update overnight.
#.github/workflows/scraper.yml
Name: Daily scraper
on:
schedule:
- cron: '0 3 * * *'
jobs:
scrape:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- run: python scrape.py
That is the entire setup. No cron daemon, no laptop tethered to a desk. The repo also serves as your modular design anchor: fetcher, parser, storer are separate files, so you can swap any layer without rewriting the pipeline.
Cron on a VPS (Digital Ocean, $6/mo) is a valid alternative for teams already running infrastructure. But for a backend engineer prototyping a daily pipeline, or a startup founder validating a data product, GitHub Actions removes the “it died last night” anxiety completely. Automated scraping saves countless hours for price tracking, competitor monitoring, or research 5. Consistency without a pager.
Action this week: 1. Fork your existing scraper repo. 2. Add the YAML snippet above. 3. Push and watch the Actions tab for the first run at 03:00 UTC.
7. The Math: Cost of Not Building It Right
Skip the retry queue. Skip version-aware selectors. The scraper runs for a week. Then it breaks. Then you fix it. Then it breaks again.
The arithmetic is brutal for the shop that treats scraping as a one-time script.
-
10–15% of scrapers require weekly fixes (GroupBWT). At one hour per fix, $100/hour billable, that is $5,200–$7,800 per year in reactive maintenance alone.
-
Version-aware selectors cut downtime by 73%. Applied to the same scraper, savings are $3,796–$5,694 per year. The selector versioning took one afternoon to implement.
-
Three-stage retry queue prevents data loss from HTTP 429s. A single corrupted batch of commercial real estate lease records. Missing comparables, wrong square footage. Costs more in downstream rework than the queue ever will.
For the enterprise team running 200 scrapers, the math compounds. For the startup founder burning runway, $5,000 in maintenance is a month of hosting.
Investing 10 hours upfront saves 50 hours of maintenance in the first year. The decision is not about features. It is about whether your scraper is a script or a system.
8. Limits & Objections: When This Stack Fails
No scraper is bulletproof. The Production Scraper Stack reduces failure. It does not eliminate it.
Three failure modes hit every team eventually:
-
Anti-bot escalation. Cloudflare and AWS analyze TLS fingerprints, header ordering, and mouse-movement timing. A Scrapy bot that passes inspection today can fail tomorrow when the CDN updates its detection model. The commercial real estate lease directory may switch from Cloudflare Free to Cloudflare Enterprise, adding JavaScript challenges that break a pure-Requests pipeline overnight.
-
Permanent blacklisting. Repeated violations trigger permanent bans on IP, account, or subnet. An enterprise team scraping 50,000 lease listings per day on three threads instead of two can lose access to the entire MLS directory. No backoff strategy recovers a blacklisted subnet.
-
Silent data corruption. A single selector change on the target site can corrupt terabytes of stored data before anyone notices. Validation rules on field presence, type, and range are not optional. The data scientist trusting a weekly CSV export may discover the “square footage” column contains floor-plan image URLs for three weeks straight.
The counter-arguments are real. Legal risk exists, scraping against ToS carries reputational and contractual exposure. Maintenance cost never reaches zero. Version-aware selectors reduced downtime by 73%, but that remaining 27% requires observability.
Add alerting for HTTP 429 and 403 responses. Forward to email or Slack. Set up a manual escalation path that pings a human within five minutes of repeated failure. The best scraper still breaks. The question is how fast you detect and fix it.
9. FAQ: Common Questions About Production Scrapers
What is the difference between Requests+BeautifulSoup and Scrapy?
Requests+BS is a manual, script-based approach for small static sites. Scrapy is a full framework with built-in scheduling, pipelines, middleware, and auto-throttling. Choose Requests+BS for one-off tasks; choose Scrapy for repeatable pipelines.
How do I handle HTTP 429 rate limits?
Implement exponential backoff (delay = 2^i seconds) and rotate proxies. Scrapy’s AutoThrottle extension handles this automatically. A three-stage retry queue (backoff, rotate proxy, escalate) is recommended for production systems.
Can I store scraped data in multiple formats?
Yes. Scrapy supports CSV, JSON, SQLite, MySQL, PostgreSQL, and Amazon S3 via Feed Exporters. For a production pipeline, PostgreSQL with SQLAlchemy gives you concurrent access and schema migration support.
How do I schedule a scraper to run daily?
Use cron on a Linux server for simplicity, or GitHub Actions for cloud-native scheduling with better reliability. The key is to avoid running it on a laptop that may sleep or lose internet.
What is DOM drift and how do I prevent it?
DOM drift is when a site’s HTML structure changes, breaking your selectors. Version-aware selectors (with version stamps from page attributes) reduced downtime by 73% over two months despite 14 frontend hotfixes.
When should I use a managed scraping service?
When anti-bot systems (Cloudflare, AWS) are too aggressive, or when you lack the infrastructure to rotate proxies and manage retries at scale. Services like Firecrawl offload proxy governance and compliance monitoring for a fee.
Closing: From Script to Pipeline
The difference between a scraper that works once and one that runs for months is deliberate architecture. A scraper that breaks in a week is not a scraper; it is a recurring problem.
For the worked example. Commercial real estate lease data from a public MLS directory. The same principle applies. A script that fetches 10 pages today will break when the MLS adds a CAPTCHA or changes its table structure. A pipeline with Scrapy’s AutoThrottle, a retry queue, and GitHub Actions scheduling survives those changes.
Action this week:
-
Run
scrapy startprojectfor your MLS scraper. -
Enable
AUTOTHROTTLE_ENABLEDand setCONCURRENT_REQUESTS_PER_DOMAIN = 2. -
Add a three-stage retry queue (backoff → rotate proxy → escalate) per.
-
Schedule the spider with a GitHub Actions cron job (
0 6 * * *). -
Add field validation on every output. One selector change corrupts terabytes.
Web scraping at scale is infrastructure, not a side project. Treat it that way from day one.
About the Author
This guide was researched and written by a technical editor specializing in data infrastructure and Python automation. The author synthesizes documented evidence from the web scraping industry, including peer-reviewed engineering blogs and published case studies, to provide actionable guidance for production-grade scraping pipelines.
Sources
Footnotes
-
GroupBWT. https://groupbwt.com/blog/challenges-in-web-scraping/. (2025) ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
PromptCloud. https://www.promptcloud.com/blog/large-scale-web-scraping-extraction-challenges-that-you-should-know/. (2025) ↩ ↩2
-
ScrapeOps. https://scrape.do/blog/web-scraping-rate-limit/. (2025) ↩
-
WebScrapingSite. https://webscrapingsite.com/resources/automated-web-scraper-cron/. (2025) ↩
-
Firecrawl. https://www.firecrawl.dev/blog/automated-web-scraping-free-2025. (2025) ↩