Rotating Proxy Python: Sessions, Scheduling, and Exit Checks
Sep 16, 2026 · Guides · 20 min read
TL;DR
Rotate routes between independent tasks, not in response to access restrictions. Use a documented provider mode for stateless checks and retain one session for stateful work. Verify the observed IP separately from the gateway address, limit aggregate traffic to each target, and stop on authorization or rate-limit signals. Requests and Selenium require separate route checks; success in one client does not validate the other.
What Does Proxy Rotation in Python Actually Change?
Three layers are often conflated. A proxy gateway is the host and port your Python client connects to. An exit IP is the public address the destination observes. A session is the provider-defined mapping intended to keep an exit stable for a period or task. The gateway can stay constant while the exit changes behind it; changing a username parameter can change the exit policy without changing the gateway host.
| Strategy | Rotation unit | Appropriate workload | Main tradeoff |
|---|---|---|---|
| Provider per-request mode | Each independent request | Stateless, permitted read-only checks | Consecutive results can repeat; no cookie continuity promise |
| Provider fixed session | One task or session window | A permitted multi-step QA transaction | Exit may change when the session expires or resources change |
| Application-managed route pool | Independent jobs | Several approved regions or networks | You must manage health, assignment, secrets, and observability |
| Selenium browser-per-route | New browser instance | Independent browser tasks | Browser startup cost; credentials need a supported arrangement |
A rotating proxy can supply an ISP-associated exit for an authorized regional test. If the task is cost-sensitive and does not require a residential route, compare the documented rotating datacenter proxies instead. Neither product type guarantees that a particular destination will accept a request, and proxy rotation does not authorize access beyond the target’s rules or applicable law.
Prerequisites, Scope, and Tested Environment
The executable Requests path uses the standard-library modules argparse, os, and urllib.parse, plus Requests. SOCKS5 routing needs Requests’ optional SOCKS dependencies. The proxy details—host, port, generated username, and password—must come from your Rola dashboard for the selected network. Do not copy example account names as if they were working credentials.
| Component | This draft’s verification | Reader action |
|---|---|---|
| Python | 3.12.14, Windows local fixture | Use Python 3.10+ and record your exact version |
| Requests | 2.34.2 supplied in the local runtime fixture | Install requests[socks] and pin the resolved versions in your project |
| Selenium | Documentation and Python snippet syntax reviewed, no browser run | Install Selenium and a supported Chrome/driver environment before using that branch |
| Local proxy fixtures | Authenticated HTTP echo with two synthetic exits and a task scheduler | Run the included fixtures to test control flow without spending proxy traffic |
| Live Rola exit | Not tested; no account credentials supplied | Verify your own exit with a neutral service before a target test |
In a project virtual environment, install the dependencies. These are the official installation forms, not a claim that this draft completed a fresh PyPI installation in the local fixture:
python -m venv .venv
# Activate .venv using the command appropriate to your shell.
python -m pip install "requests[socks]" selenium
python -m pip freeze > requirements-lock.txt
Requests’ proxy and SOCKS documentation describes the proxies mapping, optional requests[socks] install, and the socks5h scheme that resolves destination names at the proxy rather than on the client. This DNS behavior does not make SOCKS5 authentication or the client-to-gateway path encrypted. Keep destination TLS verification enabled. If an organization uses an approved custom CA bundle, provide it explicitly through APPROVED_CA_BUNDLE; session.trust_env = False also suppresses environment-derived proxy and certificate settings. Never use verify=False to mask a trust failure. Supply credentials through a secret manager in production rather than committing them or placing them in screenshots.
Only send requests to a neutral exit-check service or a page you own or are expressly authorized to test. Do not use this tutorial to continue after a login wall, access denial, CAPTCHA, 403, or 429. Those observations need the relevant authorization or rate-limit process, not a fresh address.
Step 1: Choose Rola IP Rotation Parameters for the Task
Rola IP’s current proxy parameters control country and sessions through the username. The English page was captured on September 16, 2026; it documents -f-1, the _1 session suffix, -sessiontime-{minutes}, the _dc network marker, and a 1–120 minute supported session-time range. State/city targeting applies only to rotating residential, while all three rotating networks support country targeting. The dashboard’s host and port are the gateway—not the exit IP. These are documentation claims, not a measured duration, IP-stability guarantee, or proof that our account was configured.
| Goal | Documented illustrative username | Why it fits |
|---|---|---|
| Stateless US exit checks | test-country-us-f-1 |
Provider selects a rotating exit for each request |
| One US task with continuity | test_1-country-us-sessiontime-10 |
Session ID 1 is intended to hold an exit for ten minutes |
| Separate independent task | test_2-country-us-sessiontime-10 |
A different session ID separates task assignments |
| Datacenter country session | test_dc_1-country-us-sessiontime-10 |
_dc identifies the selected network before the session suffix |
test is illustrative. Use the account name and exact parameters generated or allowed for your chosen Rola network. Do not combine -f-1 with a task that needs stable cookies or a multi-step form. Also, the documented “new exit per request” policy does not mean every observed IP in a tiny sample must be unique: pool selection and resource conditions can produce repeats.

Step 2: Build a Bounded Requests Exit Checker
Save the following complete listing as rotating_proxy.py. It uses one proxy URL for both destination schemes, disables unrelated environment proxy overrides, refuses redirects on the neutral check, caps each response at 4 KiB and each run at three checks, and validates the returned IPv4/IPv6 address. It never catches an access restriction and switches routes.
"""Bounded, authorized exit-IP checks through a configured proxy gateway.
The username must be copied/generated according to the current provider docs.
This script never changes routes in response to a target restriction.
"""
import argparse
import json
import os
from ipaddress import ip_address
from urllib.parse import quote, urlsplit
import requests
DEFAULT_CHECK_URL = "https://api64.ipify.org?format=json"
STOP_STATUSES = {401, 403, 429}
MAX_RESPONSE_BYTES = 4096
def proxy_url_from_env(username_override=None):
scheme = os.getenv("ROLA_PROXY_SCHEME", "socks5h")
if scheme not in {"socks5h", "http"}:
raise ValueError("ROLA_PROXY_SCHEME must be socks5h or http")
host = os.environ["ROLA_PROXY_HOST"].strip()
port = int(os.environ["ROLA_PROXY_PORT"])
username = username_override or os.environ["ROLA_PROXY_USERNAME"]
password = os.environ["ROLA_PROXY_PASSWORD"]
if not host or "://" in host or "/" in host or "@" in host:
raise ValueError("ROLA_PROXY_HOST must be a hostname, not a URL")
if not 1 <= port <= 65535:
raise ValueError("ROLA_PROXY_PORT is out of range")
if not username or not password:
raise ValueError("Proxy credentials must not be empty")
return (
f"{scheme}://{quote(username, safe='')}:"
f"{quote(password, safe='')}@{host}:{port}"
)
def validate_check_url(url):
parts = urlsplit(url)
if (
parts.scheme not in {"http", "https"}
or not parts.hostname
or parts.username
or parts.password
or parts.fragment
or (parts.query and url != DEFAULT_CHECK_URL)
or (
parts.scheme == "http"
and (
parts.hostname != "fixture.invalid"
or os.getenv("ROLA_PROXY_SCHEME") != "http"
or os.getenv("ROLA_PROXY_HOST") not in {"127.0.0.1", "localhost"}
)
)
):
raise ValueError("Use HTTPS without credentials; HTTP is limited to the local fixture")
return url
def check_exit(session, check_url, proxy_url, timeout=(5, 15)):
with session.get(
check_url,
proxies={"http": proxy_url, "https": proxy_url},
timeout=timeout,
allow_redirects=False,
stream=True,
) as response:
if response.status_code in STOP_STATUSES:
raise RuntimeError(f"HTTP {response.status_code}: stop; do not change routes")
if response.status_code == 407:
raise RuntimeError("HTTP 407: proxy authentication failed")
if response.status_code != 200:
raise RuntimeError(f"HTTP {response.status_code}: inspect this response separately")
if int(response.headers.get("Content-Length", "0")) > MAX_RESPONSE_BYTES:
raise ValueError("Exit-check response is too large")
body = bytearray()
for chunk in response.iter_content(chunk_size=1024):
body.extend(chunk)
if len(body) > MAX_RESPONSE_BYTES:
raise ValueError("Exit-check response is too large")
try:
data = json.loads(body)
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise ValueError("Exit-check response is not valid JSON") from exc
if not isinstance(data, dict):
raise ValueError("Exit-check response must be a JSON object")
raw_ip = data.get("ip")
if not isinstance(raw_ip, str):
raise ValueError("Exit-check 'ip' must be a string")
try:
observed_ip = str(ip_address(raw_ip.strip()))
except ValueError as exc:
raise ValueError("Exit-check response contains an invalid IP") from exc
return {"ip": observed_ip, "country": str(data.get("country", "unknown"))}
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--url", default=DEFAULT_CHECK_URL)
parser.add_argument("--checks", type=int, default=2)
args = parser.parse_args(argv)
if not 1 <= args.checks <= 3:
parser.error("--checks must be between 1 and 3")
check_url = validate_check_url(args.url)
proxy_url = proxy_url_from_env()
observed = []
with requests.Session() as session:
session.trust_env = False
session.verify = os.getenv("APPROVED_CA_BUNDLE", True)
for index in range(args.checks):
try:
result = check_exit(session, check_url, proxy_url)
except requests.exceptions.ProxyError as exc:
raise SystemExit("Proxy gateway or authentication failed") from exc
except requests.exceptions.SSLError as exc:
raise SystemExit("TLS verification failed; do not disable it") from exc
except requests.exceptions.Timeout as exc:
raise SystemExit("Connect or read timeout; inspect the network path") from exc
except requests.exceptions.RequestException as exc:
raise SystemExit(f"Transport error: {type(exc).__name__}") from exc
except (RuntimeError, ValueError) as exc:
raise SystemExit(str(exc)) from exc
observed.append(result["ip"])
print(f"check={index + 1} exit_ip={result['ip']} country={result['country']}")
if len(observed) > 1:
print(f"distinct_exits={len(set(observed))}/{len(observed)}")
print("A repeated exit does not, by itself, prove that rotation is broken.")
return observed
if __name__ == "__main__":
main()
The socks5h prefix is deliberate: proxy-side DNS prevents the target hostname from being resolved through the client’s local DNS path. The separate 5-second connect and 15-second read timeouts are not a guaranteed 20-second total deadline. quote() protects special characters in generated usernames and passwords. A gateway address need not match the ip field returned by the exit service.
Inject the four Rola environment values through your shell’s ephemeral session or secret manager, then run:
python rotating_proxy.py --checks 2
The default is ipify’s documented HTTPS JSON endpoint, https://api64.ipify.org?format=json, which can return IPv4 or IPv6. A direct no-proxy GET returned HTTP 200 with a string ip field on September 15, 2026; this does not validate a Rola route. Only the isolated local fixture may use plaintext HTTP through --url; other replacement exit-check URLs must be HTTPS. If you use an authorized replacement, ensure it returns a JSON object with a valid ip string. The actual exit depends on your account and cannot be promised.
How to read the result
With a documented -f-1 username, two checks may show different exits; that supports the observed route change for those checks. A repeated exit alone does not prove misconfiguration. With a fixed-session username, a stable exit within the configured window supports continuity for that sample, not an indefinite guarantee. Record timestamp, product network, non-secret route label, username parameter pattern, HTTP status, and observed exit; never log the raw proxy URL or password.
Step 3: Verify the Rotation Logic Without a Live Proxy
Save the following as verify_local_fixture.py beside rotating_proxy.py. It starts an authenticated HTTP proxy echo on 127.0.0.1 and returns synthetic documentation-range addresses. It alternates two exits when the fixture username ends in -f-1; it holds one exit for a fixed-session username. This is a control-flow test only. It cannot establish that a live Rola gateway will return any particular exit, that an address belongs to Rola IP, or that a target accepts it.
"""Local-only fixture: an authenticated HTTP proxy echo with two synthetic exits."""
import base64
import json
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import rotating_proxy
class EchoProxy(BaseHTTPRequestHandler):
request_number = 0
total_requests = 0
target_429_requests = 0
slow_second_seen = 0
def do_GET(self):
EchoProxy.total_requests += 1
if "/sleep" in self.path:
time.sleep(0.3)
return
if "/status/429" in self.path:
EchoProxy.target_429_requests += 1
self.send_response(429)
self.end_headers()
return
if "/slow-second-check" in self.path:
EchoProxy.slow_second_seen += 1
if EchoProxy.slow_second_seen == 2:
time.sleep(0.7)
header = self.headers.get("Proxy-Authorization", "")
if not header.startswith("Basic "):
self.send_response(407)
self.send_header("Proxy-Authenticate", 'Basic realm="fixture"')
self.end_headers()
return
credentials = base64.b64decode(header[6:]).decode("utf-8")
username, _, password = credentials.partition(":")
if password != "fixture-password":
self.send_response(407)
self.send_header("Proxy-Authenticate", 'Basic realm="fixture"')
self.end_headers()
return
if "/bad-json" in self.path:
body = b"{broken"
elif "/bad-ip" in self.path:
body = json.dumps({"ip": "not-an-ip"}).encode("utf-8")
elif "/oversize" in self.path:
body = json.dumps({"ip": "192.0.2.10", "pad": "x" * 5000}).encode("utf-8")
else:
if username.endswith("-f-1"):
EchoProxy.request_number += 1
ip = ["192.0.2.10", "192.0.2.11"][EchoProxy.request_number % 2]
else:
ip = "198.51.100.20"
body = json.dumps({"ip": ip, "country": "fixture"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
def run_fixture():
server = ThreadingHTTPServer(("127.0.0.1", 0), EchoProxy)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
os.environ.update(
ROLA_PROXY_SCHEME="http",
ROLA_PROXY_HOST="127.0.0.1",
ROLA_PROXY_PORT=str(server.server_port),
ROLA_PROXY_PASSWORD="fixture-password",
)
url = "http://fixture.invalid/ip.json"
os.environ["ROLA_PROXY_USERNAME"] = "fixture-country-us-f-1"
print("rotating fixture:")
before = EchoProxy.total_requests
rotating = rotating_proxy.main(["--url", url, "--checks", "2"])
assert rotating == ["192.0.2.11", "192.0.2.10"]
assert EchoProxy.total_requests - before == 2
print("rotation values and call count: PASS")
os.environ["ROLA_PROXY_USERNAME"] = "fixture_1-country-us-sessiontime-10"
print("fixed-session fixture:")
before = EchoProxy.total_requests
fixed = rotating_proxy.main(["--url", url, "--checks", "2"])
assert fixed == ["198.51.100.20", "198.51.100.20"]
assert EchoProxy.total_requests - before == 2
print("fixed values and call count: PASS")
print("target rate-limit fixture:")
before = EchoProxy.total_requests
try:
rotating_proxy.main(
["--url", "http://fixture.invalid/status/429", "--checks", "2"]
)
except SystemExit as exc:
assert "stop; do not change routes" in str(exc)
print("HTTP 429 stop rule: PASS")
else:
raise AssertionError("The client did not stop on HTTP 429")
assert EchoProxy.total_requests - before == 1
for path, expected in [
("bad-json", "not valid JSON"),
("bad-ip", "invalid IP"),
("oversize", "too large"),
]:
try:
rotating_proxy.main(
["--url", f"http://fixture.invalid/{path}", "--checks", "1"]
)
except SystemExit as exc:
assert expected in str(exc)
print(f"{path} rejection: PASS")
else:
raise AssertionError(f"{path} was accepted")
os.environ["ROLA_PROXY_PASSWORD"] = "wrong-password"
try:
rotating_proxy.main(["--url", url, "--checks", "1"])
except SystemExit as exc:
assert "authentication" in str(exc).lower()
print("proxy authentication rejection: PASS")
else:
raise AssertionError("Bad proxy credentials were accepted")
os.environ["ROLA_PROXY_PASSWORD"] = "fixture-password"
with rotating_proxy.requests.Session() as session:
session.trust_env = False
try:
rotating_proxy.check_exit(
session,
"http://fixture.invalid/sleep",
rotating_proxy.proxy_url_from_env(),
timeout=(0.1, 0.1),
)
except rotating_proxy.requests.exceptions.Timeout:
print("transport timeout classification: PASS")
else:
raise AssertionError("Timeout was not raised")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
if __name__ == "__main__":
run_fixture()
Run the fixture with Requests installed:
python verify_local_fixture.py
Recorded locally on Python 3.12.14 and the bundled Requests 2.34.2, then rerun on September 16, 2026:
rotating fixture:
check=1 exit_ip=192.0.2.11 country=fixture
check=2 exit_ip=192.0.2.10 country=fixture
distinct_exits=2/2
A repeated exit does not, by itself, prove that rotation is broken.
rotation values and call count: PASS
fixed-session fixture:
check=1 exit_ip=198.51.100.20 country=fixture
check=2 exit_ip=198.51.100.20 country=fixture
distinct_exits=1/2
A repeated exit does not, by itself, prove that rotation is broken.
fixed values and call count: PASS
target rate-limit fixture:
HTTP 429 stop rule: PASS
bad-json rejection: PASS
bad-ip rejection: PASS
oversize rejection: PASS
proxy authentication rejection: PASS
transport timeout classification: PASS
The addresses are synthetic examples from documentation ranges. The fixture makes no request to a target website or Rola’s production gateway. Its 429, malformed JSON, invalid IP, oversize response, authentication failure, and timeout are generated locally. The test asserts observed exit values and request counts, including that a 429 prevents a second check. It does not test SOCKS5, HTTPS tunneling, or actual Rola sessions.

Step 4: Schedule One Bound Route per Independent Task
The task-level scheduler below binds a generated fixed-session username before each job, caps workers at three, shares one pacing clock across requests to the same target, verifies a neutral exit before and after the target check, and closes each Requests session. It never changes the assigned route after a failure. Use only an owned or explicitly authorized, read-only target. Declare the estimated task duration, the age since each session assignment began, an allowance for diagnostic checks, and a safety margin. These values are compared with the configured session window; the code does not query the provider’s remaining session lifetime or guarantee that an exit remains available. Use fresh, documented session assignments where appropriate, and do not use an unknown-age reused username as if it were new. The neutral checks do not count toward the target pacing gap, but their elapsed time does reduce the local budget estimate.
Save this complete listing as schedule_tasks.py beside rotating_proxy.py:
"""Minimal task-level proxy-session scheduler for authorized read-only checks.
Each task receives one route before it starts. A route never changes after a
target rejection. Deadline checks are soft admission limits, not forced network
request cancellation.
"""
import argparse
import math
import os
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from threading import Event, Lock
import requests
from rotating_proxy import (
DEFAULT_CHECK_URL,
STOP_STATUSES,
check_exit,
proxy_url_from_env,
validate_check_url,
)
@dataclass(frozen=True)
class Task:
label: str
username: str
target_url: str
session_minutes: int
expected_seconds: float
session_age_seconds: float = 0.0
diagnostic_allowance_seconds: float = 20.0
safety_margin_seconds: float = 10.0
class TargetPacer:
def __init__(self, gap_seconds):
self.gap_seconds = gap_seconds
self.next_allowed = 0.0
self.lock = Lock()
def wait_turn(self, stop, deadline):
with self.lock:
turn = max(time.monotonic(), self.next_allowed)
self.next_allowed = turn + self.gap_seconds
delay = turn - time.monotonic()
if turn >= deadline or stop.wait(max(0, delay)):
return False
return time.monotonic() < deadline and not stop.is_set()
def run_task(task, check_url, pacer, stop, deadline):
if stop.is_set() or time.monotonic() >= deadline:
return task.label, "not_started", None
if not 1 <= task.session_minutes <= 120:
return task.label, "session_config_error", None
estimates = (
task.expected_seconds,
task.session_age_seconds,
task.diagnostic_allowance_seconds,
task.safety_margin_seconds,
)
if (not all(math.isfinite(value) for value in estimates)
or task.expected_seconds <= 0
or any(value < 0 for value in estimates[1:])):
return task.label, "session_config_error", None
if sum(estimates) >= task.session_minutes * 60:
return task.label, "session_budget_too_short", None
task_started = time.monotonic()
proxy_url = proxy_url_from_env(task.username)
with requests.Session() as session:
session.trust_env = False
session.verify = os.getenv("APPROVED_CA_BUNDLE", True)
try:
if time.monotonic() >= deadline:
return task.label, "deadline", None
before = check_exit(session, check_url, proxy_url)["ip"]
# Diagnostic time consumed so far reduces the declared budget.
elapsed = time.monotonic() - task_started
if (task.session_age_seconds + elapsed + task.expected_seconds
+ task.diagnostic_allowance_seconds
+ task.safety_margin_seconds >= task.session_minutes * 60):
return task.label, "session_budget_too_short", before
if not pacer.wait_turn(stop, deadline):
return task.label, "not_started", before
if stop.is_set() or time.monotonic() >= deadline:
return task.label, "not_started", before
with session.get(
task.target_url,
proxies={"http": proxy_url, "https": proxy_url},
timeout=(5, 15),
allow_redirects=False,
stream=True,
) as response:
status = response.status_code
if status in STOP_STATUSES:
stop.set()
return task.label, f"target_stop_{status}", before
if status == 407:
return task.label, "proxy_auth_failure", before
if status != 200:
return task.label, f"target_status_{status}", before
if time.monotonic() >= deadline:
stop.set()
return task.label, "deadline_after_target", before
after = check_exit(session, check_url, proxy_url)["ip"]
if time.monotonic() >= deadline:
stop.set()
return task.label, "deadline_after_verification", after
if after != before:
stop.set()
return task.label, "session_exit_changed", after
return task.label, "status_and_exit_checks_passed", after
except requests.exceptions.ProxyError:
return task.label, "proxy_transport_failure", None
except requests.exceptions.Timeout:
return task.label, "transport_timeout", None
except requests.exceptions.SSLError:
stop.set()
return task.label, "tls_verification_failure", None
except (requests.exceptions.RequestException, RuntimeError, ValueError) as exc:
# No automatic route change or direct-connection fallback.
if "stop; do not change routes" in str(exc):
stop.set()
return task.label, "diagnostic_stop_signal", None
if "proxy authentication" in str(exc).lower():
return task.label, "proxy_auth_failure", None
return task.label, "diagnostic_or_transport_failure", None
def run(tasks, check_url=DEFAULT_CHECK_URL, max_workers=2, gap_seconds=1.0,
deadline_seconds=30.0):
if (not 1 <= max_workers <= 3
or not math.isfinite(gap_seconds)
or not 0 <= gap_seconds <= 60
or not math.isfinite(deadline_seconds)
or deadline_seconds <= 0):
raise ValueError("Worker count, target gap, or deadline is invalid")
validate_check_url(check_url)
if not tasks:
raise ValueError("At least one task is required")
for task in tasks:
validate_check_url(task.target_url)
stop = Event()
pacer = TargetPacer(gap_seconds)
deadline = time.monotonic() + deadline_seconds
with ThreadPoolExecutor(max_workers=max_workers) as pool:
futures = [pool.submit(run_task, task, check_url, pacer, stop, deadline)
for task in tasks]
results = [future.result() for future in futures]
return results
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", required=True, help="Owned/authorized read-only URL")
parser.add_argument("--check-url", default=DEFAULT_CHECK_URL)
parser.add_argument("--gap-seconds", type=float, default=1.0)
parser.add_argument("--deadline-seconds", type=float, default=30.0)
parser.add_argument("--estimated-task-seconds", type=float, required=True,
help="Declared duration of each authorized target task")
parser.add_argument("--session-age-seconds", type=float, required=True,
help="Declared age since this session assignment first began")
parser.add_argument("--diagnostic-allowance-seconds", type=float, default=20.0)
parser.add_argument("--safety-margin-seconds", type=float, default=10.0)
args = parser.parse_args()
usernames = [item.strip() for item in os.environ["ROLA_TASK_USERNAMES"].split(",")]
if (not 1 <= len(usernames) <= 3 or any(not item for item in usernames)
or len(set(usernames)) != len(usernames)):
parser.error("ROLA_TASK_USERNAMES needs one to three distinct generated usernames")
minutes = int(os.getenv("ROLA_SESSION_MINUTES", "10"))
estimates = (args.estimated_task_seconds, args.session_age_seconds,
args.diagnostic_allowance_seconds, args.safety_margin_seconds)
if (not all(math.isfinite(value) for value in estimates)
or args.estimated_task_seconds <= 0
or any(value < 0 for value in estimates[1:])):
parser.error("Task/session estimates must be finite and non-negative")
tasks = [Task(f"route-{index+1}", username, args.target, minutes,
args.estimated_task_seconds, args.session_age_seconds,
args.diagnostic_allowance_seconds, args.safety_margin_seconds)
for index, username in enumerate(usernames)]
for label, state, exit_ip in run(
tasks, args.check_url, min(2, len(tasks)), args.gap_seconds,
args.deadline_seconds,
):
print(f"task={label} state={state} exit_ip={exit_ip or 'none'}")
if __name__ == "__main__":
main()
Inject ROLA_TASK_USERNAMES as one to three distinct, comma-separated generated fixed-session usernames, plus the four gateway values from Step 2. Keep ROLA_SESSION_MINUTES aligned with the documented -sessiontime- parameter in those usernames. Supply a defensible estimate and the age since each assignment first began; the CLI uses one declared age for this small batch, so split batches if their ages differ. Then run, replacing the URL with a page you own or are authorized to check:
python schedule_tasks.py --target https://your-owned-site.example/health --estimated-task-seconds 20 --session-age-seconds 0 --diagnostic-allowance-seconds 20 --gap-seconds 2 --deadline-seconds 60
The gap is shared across this batch’s target calls, even when two worker threads run. A 401, 403, or 429 sets a global stop flag; queued or paced jobs do not proceed. An already in-flight HTTP request cannot be forcibly cancelled by that flag. Connect/read timeouts and deadline checks are soft bounds, not a hard wall-clock cancellation guarantee. A target login wall, CAPTCHA, or policy notice may return HTTP 200, so an operator must inspect such page content before any further approved work. The status_and_exit_checks_passed state means only that the sampled HTTP status and neutral exit checks passed; it does not prove that the target content was correct or that the provider exit will persist. Do not interpret an exit change after a job as permission to retry the same target from a new route.
A local scheduler fixture verifies two bound tasks under a two-worker cap, six proxy echo calls, aggregate pacing, a shared 429 stop, declared-age and diagnostic budget rejection, non-finite input rejection, and a deadline reached after final exit verification. Run verify_scheduler_fixture.py beside the other files. It is an HTTP-only synthetic control-flow test, not a production route, HTTPS tunnel, or provider TTL measurement.
"""Local-only assertions for task binding, pacing, stop, and session budget."""
import os
from http.server import ThreadingHTTPServer
from threading import Thread
from schedule_tasks import Task, run
from verify_local_fixture import EchoProxy
def verify():
server = ThreadingHTTPServer(("127.0.0.1", 0), EchoProxy)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
os.environ.update(
ROLA_PROXY_SCHEME="http",
ROLA_PROXY_HOST="127.0.0.1",
ROLA_PROXY_PORT=str(server.server_port),
ROLA_PROXY_PASSWORD="fixture-password",
)
check_url = "http://fixture.invalid/ip.json"
target_url = "http://fixture.invalid/page"
tasks = [
Task("route-1", "fixture_1-country-us-sessiontime-10", target_url, 10, 10),
Task("route-2", "fixture_2-country-us-sessiontime-10", target_url, 10, 10),
]
before = EchoProxy.total_requests
results = run(tasks, check_url, max_workers=2, gap_seconds=0.1,
deadline_seconds=5)
assert [state for _, state, _ in results] == [
"status_and_exit_checks_passed", "status_and_exit_checks_passed"
]
assert EchoProxy.total_requests - before == 6
print("two bound tasks, two-worker cap, six local calls: PASS")
restricted = [Task(task.label, task.username,
"http://fixture.invalid/status/429", 10, 10)
for task in tasks]
before_429 = EchoProxy.target_429_requests
results = run(restricted, check_url, max_workers=2, gap_seconds=0.5,
deadline_seconds=5)
assert EchoProxy.target_429_requests - before_429 == 1
assert "target_stop_429" in [state for _, state, _ in results]
assert "not_started" in [state for _, state, _ in results]
print("shared target 429 stops a second target call: PASS")
too_short = [Task("short", tasks[0].username, target_url, 1, 55)]
results = run(too_short, check_url, deadline_seconds=5)
assert results[0][1] == "session_budget_too_short"
print("short session budget rejection: PASS")
aged = [Task("aged", tasks[0].username, target_url, 1, 20,
session_age_seconds=50)]
results = run(aged, check_url, deadline_seconds=5)
assert results[0][1] == "session_budget_too_short"
print("declared session age and diagnostic budget rejection: PASS")
invalid = [Task("nan", tasks[0].username, target_url, 10, float("nan"))]
results = run(invalid, check_url, deadline_seconds=5)
assert results[0][1] == "session_config_error"
for kwargs in ({"gap_seconds": float("nan")},
{"deadline_seconds": float("nan")}):
try:
run(tasks, check_url, **kwargs)
except ValueError:
pass
else:
raise AssertionError("Non-finite scheduler limit was accepted")
print("non-finite budget and scheduler limits rejected: PASS")
EchoProxy.slow_second_seen = 0
delayed = [Task("delayed", tasks[0].username, target_url, 10, 10)]
results = run(delayed, "http://fixture.invalid/slow-second-check",
max_workers=1, gap_seconds=0, deadline_seconds=0.5)
assert results[0][1] == "deadline_after_verification"
print("deadline after final exit check: PASS")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
if __name__ == "__main__":
verify()
Run python verify_scheduler_fixture.py with Requests available in the active Python environment.
two bound tasks, two-worker cap, six local calls: PASS
shared target 429 stops a second target call: PASS
short session budget rejection: PASS
declared session age and diagnostic budget rejection: PASS
non-finite budget and scheduler limits rejected: PASS
deadline after final exit check: PASS
| Scheduler state | What it means | Next action |
|---|---|---|
not_started |
Global stop or deadline prevented admission | Do not schedule more work until the reason is reviewed |
status_and_exit_checks_passed |
Sampled HTTP status and neutral exit checks passed | Inspect target content and close the route; no persistence claim |
proxy_auth_failure / proxy_transport_failure |
Gateway configuration or authentication needs diagnosis | Check dashboard settings and connection path; do not fall back to direct access |
target_stop_401 / target_stop_403 / target_stop_429 |
Destination reported an access or rate-limit signal | Stop the batch and follow the target’s authorization guidance |
session_budget_too_short / session_exit_changed |
Declared remaining estimate was inadequate, or observed route drifted | End the task; inspect policy and budget before scheduling a new authorized job |
deadline_after_verification |
Final neutral check ended beyond the soft deadline | Do not record success; review elapsed time and scheduling limits |

How to Rotate Proxy in Selenium Python
Selenium’s browser proxy documentation exposes a manual proxy capability through Chrome options. The browser route is configured before webdriver.Chrome() starts. For two independent regional jobs, make two browser instances with separately approved proxy endpoints; don’t assume that altering a Python list changes a running browser’s networking or that swapping a proxy between page resources preserves a consistent session.
The following is a syntax-reviewed integration pattern for passwordless or allowlisted HTTP proxy endpoints, not a live authenticated Rola browser test. Rola’s allowlist documentation describes an access method separate from username/password authentication; its English URL returned HTTP 200 on September 15, 2026. Confirm your dashboard’s endpoint type and access arrangement before using it. Do not assume the username’s -f-1 or session-ID policy applies to an extracted passwordless IP:port endpoint. Selenium’s basic host:port capability does not solve an authentication challenge, and user:password@host should not be embedded in a Chrome proxy argument.
import json
from ipaddress import ip_address
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.proxy import Proxy, ProxyType
approved_jobs = [
("region-a", "proxy-a.example:8080"),
("region-b", "proxy-b.example:8080"),
]
for label, endpoint in approved_jobs:
options = webdriver.ChromeOptions()
options.proxy = Proxy({
"proxyType": ProxyType.MANUAL,
"httpProxy": endpoint,
"sslProxy": endpoint,
})
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(15)
driver.get("https://api64.ipify.org?format=json")
body = driver.find_element(By.TAG_NAME, "body").text
data = json.loads(body) # A login/error HTML page is not an IP result.
if not isinstance(data, dict) or not isinstance(data.get("ip"), str):
raise ValueError("Neutral page did not return an IP string")
observed_ip = str(ip_address(data["ip"].strip()))
print(label, observed_ip)
finally:
driver.quit()
Replace the illustrative endpoints; they cannot connect as written. Browser startup, driver management, HTTPS behavior, authentication, and target page rendering must be checked in your environment. JSON parsing rejects an error page, and ip_address() rejects malformed exit values; a successful print still says nothing about target authorization. For a stateful browser task, keep one browser, one proxy assignment, and one cookie jar through its steps; start a new browser only for the next independent task.
Troubleshooting: Which Layer Failed?
| Symptom | Likely layer | Verification | Safe fix |
|---|---|---|---|
MissingSchema or invalid proxy URL |
Client configuration | Check the explicit http:// or socks5h:// scheme and escaped credentials |
Correct the scheme or URL construction |
InvalidSchema for socks5h |
Python dependency | Confirm requests[socks] and its optional SOCKS package are installed in the active environment |
Install the missing optional dependency |
| Proxy connection refused or timeout | Client → gateway | Check copied dashboard host/port and local firewall, not a target retry | Fix endpoint or network path |
HTTP 407 or SOCKS auth reply |
Proxy authentication | Check generated username, password, allowlist, and selected product | Correct credentials or approved allowlist |
| Exit country or IP unexpected | Gateway → exit | Check username syntax, network marker, country, and neutral service response | Correct configuration; allow for service/geolocation variance |
Repeated exit with -f-1 |
Pool observation | Compare bounded samples and the current parameter docs | Do not assume every sample must be unique |
| Exit changes inside a multi-step job | Session policy | Check session ID, time window, and whether -f-1 was used |
Use a documented fixed session for the next authorized job |
Target 401, 403, 429, or CAPTCHA |
Target control | Read exact response, notice, endpoint, and account scope | Stop; resolve authorization or wait according to target guidance |
| Selenium prompt or proxy failure | Browser authentication | Test the endpoint and browser auth arrangement on a neutral fixture | Use a documented allowlisted route or tested auth integration |
Requests’ documentation warns that environment proxy settings can override Session.proxies; passing proxies on each call and setting session.trust_env = False makes this isolated demo easier to interpret. It also warns against disabling TLS checks or storing proxy credentials in version-controlled files. A proxy does not change whether collection is allowed; legal and policy applicability depends on the jurisdiction, target, data, and purpose.
Conclusion
The practical choice is not “rotate as often as possible.” It is to align a documented provider mode with the task: per-request exits for independent stateless checks, a fixed session for continuity, and a new Selenium browser only between independent jobs. Verify the public exit separately from the gateway, keep credentials out of logs, and pause on target enforcement or rate limits.
The local fixture confirms the Python control flow without claiming a production Rola result. Before deployment, pin your dependencies, check current Rola parameters for your chosen network, run a neutral exit check with your own account, and test the Selenium branch in the actual browser and authentication environment.