2026-11-01 · VeilSocks

Why Your Scraper Fails on GitHub Actions and Cloud Runners (and How to Route Public-Data Jobs Cleanly)

It is a classic frustration in data engineering and QA automation: you write a Python script to archive public filings, verify regional pricing, or run synthetic uptime checks. On your home connection, every request returns 200 OK. You commit the code, schedule a daily cron job in GitHub Actions or an AWS runner, and the first automated run fails with 403 Forbidden.

Nothing in your parsing logic changed. What changed is the network identity, transport stack, and environment metadata of the machine sending the request.

Here is why scheduled jobs fail on cloud runners—and how to route legitimate public-data and QA workflows cleanly using a real 4G/5G mobile proxy.

Why cloud runners get blocked before your code runs

When your script runs on a local workstation, traffic exits through a consumer ISP or mobile carrier. When that same script executes inside a GitHub-hosted runner, AWS Fargate task, or Google Cloud Run job, edge firewalls flag three differences immediately:

1. Cloud hosting ASNs are blocked by default on many public portals

GitHub-hosted Linux and Windows runners run inside Microsoft Azure datacenters (AS8075). AWS jobs exit from Amazon (AS16509 / AS14618), and Google Cloud jobs exit from AS15169.

As explained in our guide to what an ASN is and why it matters, edge CDNs and government web application firewalls maintain strict Autonomous System Number rules. Because virtually no human citizen browses public agency portals or retail catalogs from an Azure server subnet, many administrators set blanket rate limits or outright blocks on cloud hosting ASNs to prevent automated flood traffic. You can verify any runner IP instantly with our ASN & IP lookup tool.

2. Shared cloud egress IPs inherit noisy-neighbor reputation

Ephemeral CI runners share outbound NAT pools with thousands of other build jobs. Even if a target site does not block AS8075 outright, an outbound Azure IP that was used ten minutes ago by an aggressive crawler often sits on temporary reputation blocklists. Compare how edge firewalls score different network tiers in our breakdown of datacenter vs residential vs mobile IPs.

3. Container defaults and outdated HTTP client stacks

Cloud runners default to UTC timezones, minimal locale variables (C.UTF-8), and bare Linux libraries. Furthermore, a script using default python-requests negotiates HTTP/1.1 with an OpenSSL cipher order that looks nothing like a modern browser. When a non-browser TLS handshake arrives from a datacenter ASN, even public document portals frequently reject the connection.

Three rules for clean scheduled CI jobs

Rule 1: Route outbound requests through a sticky 4G/5G carrier exit

Instead of sending requests directly from the runner's Azure IP, route your HTTP or SOCKS5 client through a SIM-backed US mobile carrier exit (T-Mobile, Verizon, or AT&T). Because mobile networks place thousands of real subscribers behind carrier-grade NAT (CGNAT), firewalls treat compliant traffic from mobile ASNs as standard consumer connectivity.

For multi-step workflows—such as accepting a public portal's terms-of-use form via POST before downloading a daily report—always bind your job to a sticky session (sticky_10) so your session cookie and exit IP stay paired across every step.

Rule 2: Use modern HTTP/2 clients or aligned browser contexts

If your job uses a headless browser in CI, follow our Playwright + Cloudflare diagnostic checklist to ensure locale (en-US) and timezone_id match your US proxy region rather than the runner's UTC default. For lightweight HTTP jobs, use a modern HTTP/2-capable client (httpx with http2=True or curl_cffi) with standard headers and explicit timeouts.

Rule 3: Store proxy credentials in GitHub Encrypted Secrets and fail fast

Never hardcode proxy credentials in a repository. Store your proxy URL in GitHub Actions Settings → Secrets and variables → Actions as VEILSOCKS_PROXY_URL using the standard proxy authentication syntax for curl, Python, and Playwright. Before hitting your target portal, run a single preflight check against a lightweight IP endpoint to confirm the tunnel is active and exiting through a mobile ASN.

Working GitHub Actions workflow + Python preflight pattern

Below is a minimal, production-ready pattern for a daily public-data sync or QA check.

.github/workflows/daily-public-sync.yml:

name: Daily Public Data Sync
on:
  schedule:
    - cron: '17 6 * * *'  # Avoid top-of-the-hour traffic spikes
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install httpx[http2]
      - name: Run public data collector
        env:
          VEILSOCKS_PROXY_URL: ${{ secrets.VEILSOCKS_PROXY_URL }}
          TZ: America/New_York
        run: python scripts/collect_public_filings.py

scripts/collect_public_filings.py:

import os, sys, time, random, httpx

PROXY_URL = os.environ.get("VEILSOCKS_PROXY_URL")
if not PROXY_URL:
    sys.exit("Missing VEILSOCKS_PROXY_URL secret")

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
}

with httpx.Client(proxy=PROXY_URL, headers=HEADERS, http2=True, timeout=20.0) as client:
    # Step 1: Preflight check — never hit the target from a leaked runner IP
    probe = client.get("https://blog.veilsocks.com/whatismyip")
    probe.raise_for_status()
    print("Verified mobile exit OK")

    # Step 2: Fetch public pages with polite jitter
    time.sleep(random.uniform(1.0, 2.5))
    resp = client.get("https://example.gov/public-bulletins/latest.json")
    resp.raise_for_status()
    print(f"Fetched {len(resp.content)} bytes")

Notice the cron expression '17 6 *'. Scheduling CI jobs at :00 (top of the hour) coincides with thousands of other automated runners firing simultaneously. Offsetting your cron minute reduces upstream server load and avoids burst anomalies.

Why non-expiring pay-per-GB fits scheduled CI jobs

Scheduled CI jobs transfer very little data. As detailed in our guide on how much bandwidth web scraping uses, fetching 50 JSON or HTML pages once a day consumes 2 to 5 MB per run—roughly 100 to 150 MB per month.

Unlike monthly subscriptions where unused gigabytes expire every 30 days, VeilSocks' $3/GB pay-per-GB mobile proxy plan never expires. A single 1 GB top-up ($3) can power a lightweight daily GitHub Actions monitor for six months with zero wasted monthly reset. Model your exact CI usage with our data & cost calculator.

Respecting public portals and rate limits

Routing through a clean mobile IP solves false-positive ASN blocks on cloud runners, but always cache unchanged files (If-Modified-Since / ETag), schedule runs off-peak, honor robots.txt, and stay within polite limits (see how many requests per minute a mobile proxy IP can handle).

FAQ

Why does my Python scraper work locally but return 403 on GitHub Actions? GitHub-hosted runners execute inside Microsoft Azure datacenters (AS8075). Many public websites and government portals automatically block or challenge requests originating from cloud hosting ASNs while allowing consumer ISP and mobile carrier ASNs.

How do I pass mobile proxy credentials safely in GitHub Actions? Store your full proxy string (http://USER-sticky_10:PASS@us-mob.veilsocks.io:9200) in GitHub Repository Secrets and inject it into your workflow step as an environment variable (os.environ["VEILSOCKS_PROXY_URL"]). GitHub automatically masks secret values in build logs.

Should scheduled CI jobs use rotating or sticky mobile proxy sessions? Use sticky sessions (sticky_10 or sticky_30) whenever your job logs into a test account, submits a search or agreement form, or paginates using session cookies. Use per-request rotation only for independent, stateless single-page fetches.

How much mobile proxy bandwidth does a daily GitHub Actions job use? A daily job fetching 20 to 50 HTML or JSON endpoints typically uses 1 to 5 MB per run (30 to 150 MB per month). On a non-expiring $3/GB mobile proxy plan, that costs less than $0.50 per month of actual usage.

Real 4G/5G mobile proxies, your GB never expire

Genuine US carrier SIM IPs — SOCKS5 & HTTP, pay per GB, instant activation.

Create your account →