How to Schedule and Deploy Web Scrapers
To schedule web scraping, make the scraper a command that runs once and exits with a clear status, then let a scheduler start it: cron or a systemd timer on one server for a few jobs, GitHub Actions for small jobs without a server, AWS Lambda with an EventBridge schedule for short jobs (a Lambda run stops at 15 minutes), and Apache Airflow when jobs depend on each other and you need retries and a history of every run. ProxyField traffic never expires, so a weekly or monthly job uses the GB you bought whenever it runs; residential proxies cost from $1.95/GB, and you can start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 11 sections
- Choose a scheduler
- Make the scraper schedulable first
- Cron on a Linux server
- GitHub Actions: scheduled runs without a server
- AWS Lambda with EventBridge
- Apache Airflow: when scrapers depend on each other
- How often should you run a scraper?
- Monitor scheduled jobs
- Proxies for scheduled scrapers
- Related guides
- How to Schedule and Deploy Web Scrapers FAQ
Choose a scheduler
| Scheduler | Best for | Limits to know | Cost model |
|---|---|---|---|
| cron | A few jobs on one Linux server or VPS | No retries, no history; the server must stay up | The server |
| systemd timer | The same, with logs in the journal and catch-up after downtime (Persistent=true) | Linux with systemd only | The server |
GitHub Actions schedule | Small jobs stored in a repository | Shortest interval 5 minutes; runs can start late at busy times; scheduled workflows in public repositories are disabled after 60 days without repository activity | Included minutes, then per minute |
| AWS Lambda + EventBridge Scheduler | Short HTTP scrapers without a server | 15-minute maximum run; package size limits; browsers need a container image | Per request and per GB-second |
| Containers on a schedule (Kubernetes CronJob, cloud run jobs) | Long or browser-based jobs | You build and ship an image | Per run time |
| Apache Airflow | Many jobs with dependencies, retries, backfills and an audit trail | You run Airflow, or pay for a managed one | Its servers |
Sources GitHub Actions events: schedule, AWS Lambda quotas, EventBridge Scheduler schedule types, Apache Airflow documentation (Airflow 3.3.2 on PyPI, 2026-10-23).
A rule of thumb: start with cron or GitHub Actions; move to Airflow when you have several scrapers whose outputs feed each other, or when someone needs to see why last Tuesday's run failed.
Make the scraper schedulable first
A scheduler can only start a script. These habits decide whether unattended runs work:
- One command, one run.
python scrape.py --date todayfetches, writes and exits. Exit code 0 means success; anything else means failure, so the scheduler can alert. - Idempotent output. Write to a dated file or upsert into a table keyed by URL and date, so a rerun replaces the run instead of duplicating it.
- No overlap. If a run takes longer than the interval, two copies fight over the same files. Use a lock (
flockwith cron,max_active_runs=1in Airflow,concurrencyin GitHub Actions). - Timeouts everywhere. A request with no timeout can hang a run forever. Set one per request and one for the whole job.
- Configuration from the environment. Proxy URL, tokens and output paths come from environment variables or a secrets store, never from the code.
- Logs that answer questions. Pages fetched, rows written, errors by status code, run time.
Cron on a Linux server
crontab -e and add a line. This one runs at 06:15 every day, skips the run if the previous one still holds the lock, and appends the output to a log:
# m h dom mon dow command
15 6 * * * cd /opt/scraper && /usr/bin/flock -n /tmp/scraper.lock .venv/bin/python scrape.py >> logs/scrape.log 2>&1Cron uses the server's time zone and a minimal environment, so use absolute paths and set the variables the script needs at the top of the crontab or in a wrapper script. A systemd timer does the same job with OnCalendar=*-*-* 06:15:00, keeps the output in the journal, and with Persistent=true runs a missed job after the server comes back.
GitHub Actions: scheduled runs without a server
A workflow file in .github/workflows/ runs the scraper on GitHub's runners. The cron expression is in UTC.
name: daily-scrape
on:
schedule:
- cron: "15 6 * * *"
workflow_dispatch: {} # also allows a manual run
concurrency: daily-scrape # never two runs at once
jobs:
scrape:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- run: python scrape.py
env:
PROXY_URL: ${{ secrets.PROXY_URL }}
- uses: actions/upload-artifact@v7
with: { name: data, path: out/ }Store the proxy URL as a repository secret. Runner IPs belong to a cloud provider and change between runs, which is one reason scheduled scrapers on CI send their requests through a proxy.
AWS Lambda with EventBridge
Lambda suits short HTTP scrapers: no server to patch, and you pay for the seconds the function runs. A run can last at most 15 minutes (900 seconds), so split long jobs into batches, for example one function call per list page, fed from a queue.
# handler.py (package with requests and beautifulsoup4, or use a Lambda layer)
import json, os, datetime
import boto3, requests
from bs4 import BeautifulSoup
s3 = boto3.client("s3")
PROXY = os.environ.get("PROXY_URL")
def handler(event, context):
url = event.get("url", "https://www.scrapethissite.com/pages/simple/")
r = requests.get(url, timeout=30, proxies={"http": PROXY, "https": PROXY} if PROXY else None)
r.raise_for_status()
names = [h.get_text(strip=True) for h in BeautifulSoup(r.text, "html.parser").select("h3.country-name")]
key = f"countries/{datetime.date.today()}.json"
s3.put_object(Bucket=os.environ["BUCKET"], Key=key, Body=json.dumps(names))
return {"rows": len(names), "key": key}Schedule it with EventBridge Scheduler, which takes rate(1 day) or a six-field cron expression such as cron(15 6 * * ? *) and a time zone. Writing each run to a dated S3 key keeps runs separate; Athena or Glue can then query the files.
Two Lambda specifics:
- Headless browsers (Playwright, Puppeteer) do not fit a zip package well; deploy the function as a container image, which may be up to 10 GB, and give it more memory.
- Scrapy on Lambda works for one crawl per invocation, but Scrapy's Twisted reactor cannot be restarted in the same process, so a warm container that runs a second crawl fails with
ReactorNotRestartable. Run the crawl in a subprocess (subprocess.run(["scrapy", "crawl", "name"])) or start a fresh process per invocation.
Apache Airflow: when scrapers depend on each other
Airflow runs DAGs: Python files that declare tasks, their order, their schedule and their retries. Each run is recorded, so you can see which task failed, rerun just that task, or backfill missed dates. In Airflow 3 a scraping DAG looks like this:
from datetime import datetime, timedelta
from airflow.sdk import dag, task
@dag(schedule="15 6 * * *", start_date=datetime(2026, 10, 1), catchup=False,
max_active_runs=1, default_args={"retries": 3, "retry_delay": timedelta(minutes=10)})
def daily_prices():
@task
def scrape(ds=None):
import subprocess
subprocess.run(["python", "/opt/scraper/scrape.py", "--date", ds], check=True)
return f"/data/prices/{ds}.csv"
@task
def load(path: str):
... # load the CSV into the warehouse
load(scrape())
daily_prices()Give every scraping task a timeout (execution_timeout) and keep heavy work out of the DAG file itself, which the scheduler parses often. For browser scrapers, run each task in a container (Docker or Kubernetes operators) rather than on the Airflow worker.
How often should you run a scraper?
As often as the data changes and you will act on it, and no more often than the site can comfortably serve. Typical examples: prices and stock daily or a few times a day; rankings daily; job and real-estate listings daily; reviews and catalog details weekly; a full crawl of a large site weekly or monthly, with daily runs for the pages that change. Fetch only what changed where you can: sitemaps with lastmod, "newest first" listings, and conditional requests (If-Modified-Since, ETag) that return 304 Not Modified when nothing changed.
Monitor scheduled jobs
Silent failures are the main risk of scheduled scraping: the job "succeeds" and writes 0 rows because the layout changed.
- Alert on failure: the scheduler's own alert, or a dead-man's switch that expects a ping at the end of each run.
- Alert on volume: compare rows per run with the last week; for example, look into any drop of more than a third.
- Alert on errors by type: a rise in 403 or 429 responses means blocking; a rise in parse errors means a layout change.
- Keep the raw HTML of a sample of pages from each run, so you can fix the parser against the page that broke it.
Proxies for scheduled scrapers
Scheduled jobs often run from cloud IPs, which many sites rate-limit hard, and from servers whose IP changes between runs. Two practical points:
- Authenticate with a username and password, not an IP allowlist, when the job runs on GitHub Actions, Lambda or containers: their outgoing IPs change. Keep the allowlist for a fixed server.
- Pick the rotation per job. A new IP per request for list and product pages; a sticky session (30 seconds to 36 hours) for a multi-step flow that must keep one IP.
With the ProxyField gateway, the proxy URL is http://<login>:<password>@gate.proxyfield.io:7000; <login>-country-us-city-new_york asks for an exit IP in New York. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. Set it as PROXY_URL in the scheduler's secrets, and the code above picks it up. Unused traffic stays on your balance, so a job that runs once a month never loses what you prepaid. Python setup details: Python proxy setup; sizing a job: proxies for web scraping.
How to Schedule and Deploy Web Scrapers FAQ
How do I schedule web scraping?
ProxyField residential proxies cost from $1.95/GB and the traffic never expires, so when you schedule web scraping the GB you buy wait for the next run. Turn the scraper into one command that runs and exits, then start it with a scheduler, such as cron on a server, a GitHub Actions schedule, AWS Lambda with EventBridge (runs of up to 15 minutes) or Airflow for jobs with dependencies. Add a lock against overlapping runs and an alert on failure, and keep the proxy credentials in the scheduler's secrets.
Can I schedule recurring scraper runs?
Yes. Any scheduler that runs a command on a timetable works: cron, a systemd timer, GitHub Actions, a cloud scheduler that triggers a function or container, or Airflow. The scraper needs to be safe to rerun, so that a retry never duplicates data.
How often should I run my scraper?
As often as the data changes and you will use it: prices and rankings daily, listings daily, reviews weekly, full crawls of large sites weekly or monthly. Fetch only changed pages where you can, using sitemaps, newest-first listings and conditional requests.
How do I run web scraping on AWS Lambda?
Package a Python handler with Requests and Beautiful Soup, or deploy it as a container image if it needs a browser, and trigger it with an EventBridge Scheduler rule. Keep each invocation under the 15-minute limit by processing one batch of URLs per call, and write the results to S3 or a database.
Can I run Scrapy on AWS Lambda?
Yes, one crawl per invocation. Scrapy's Twisted reactor cannot restart inside the same process, so a warm Lambda container that starts a second crawl fails with ReactorNotRestartable; run the crawl in a subprocess instead.
How do I use Airflow for web scraping?
Write a DAG with one task per step, for example scrape, validate and load, give it a schedule, retries and max_active_runs=1, and let each run record its own logs. Run browser-based scrapers in containers, and set an execution_timeout on every scraping task.
Can I automate recurring web scraping tasks using curl?
Yes, for simple fetches: a cron line can run curl with a proxy (-x) and save each response to a dated file. Parsing the HTML still needs a tool such as a Python script, so most recurring jobs call curl only for the download.
How do I store scraped data in AWS S3?
Write each run to its own key, such as prices/2026-10-23.json, with boto3's put_object, so runs never overwrite each other. JSON Lines or Parquet files in date folders can then be queried with Athena or catalogued with Glue.
Can I run a scraper on a schedule with GitHub Actions?
Yes: add a workflow with on: schedule and a cron expression in UTC; the shortest interval is 5 minutes, and runs can start late at busy times. Keep proxy credentials in repository secrets and upload the results as an artifact or push them to storage.
How do I monitor scheduled scraping jobs?
Alert when a run fails or does not report in, when rows per run drop well below the recent average, and when 403, 429 or parse errors rise. Keep a sample of raw pages from each run, so you can fix the parser against the page that broke it.
Show 1 more questionShow fewer questions
Should I use an IP allowlist or a username and password for scheduled scrapers?
Use a username and password when the job runs on GitHub Actions, Lambda or containers, because their outgoing IPs change between runs. An IP allowlist suits a fixed server whose IP you control.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.