Proxies die. Orbit routes around them. Rotation, async health checks, latency scoring and exponential-backoff cooldown — so a dead endpoint costs you one retry, not your whole run.
rotation strategies · async curl_multi checks · latency scoring · cooldown + ban · zero dependencies
⋆ ˚ 。 ⋆ ୨ ⋆ ˚ 。 ⋆
Why Orbit · Install · Quick start · Rotation · Health checks · Failure handling · With PulsarX · API
| Feature | What it does | |
|---|---|---|
| ◐ | Rotation | Round-robin, random, or least-latency — swap strategy in one call. |
| ⟡ | Async checks | Probe the whole list in parallel over curl_multi, with a rolling concurrency window. |
| ✸ | Latency scoring | Every success updates an EWMA latency score; least-latency routing uses it. |
| ↻ | Self-healing | Failures trigger exponential-backoff cooldown; repeat offenders get banned. |
| ⌗ | Formats | HTTP/HTTPS and SOCKS4/4a/5/5h, with or without credentials, parsed from plain strings. |
| ⧉ | Testable | Health checking is behind a ProberInterface — inject a fake and drive the pool deterministically. |
| ⬡ | Zero deps | One class per file, a tiny autoloader, optional Composer. |
PHP's proxy landscape is all checkers — libraries that validate a list once and hand it back. None of them manage a live pool: rotating across it, scoring it by latency, and quarantining endpoints that start failing mid-run. Orbit is the stateful layer on top — the piece you actually wire into a scraper.
| Orbit | retrowaver/proxy-checker | ilmlv/proxy-scraper | roll-your-own | |
|---|---|---|---|---|
| Validate proxies | ✓ | ✓ | ✓ | maybe |
| Rotation strategies | ✓ 3 | ✗ | ✗ | ✗ |
| Latency scoring | ✓ EWMA | ✗ | ✗ | ✗ |
| Cooldown + ban on failure | ✓ | ✗ | ✗ | ✗ |
Async (curl_multi) |
✓ | ✓ | partial | ✗ |
| PulsarX / cURL adapters | ✓ | ✗ | ✗ | — |
| Runtime dependencies | none | none | Guzzle | — |
With Composer
composer require vxsilisk/orbitWithout Composer — require the bundled autoloader:
require __DIR__ . '/autoload.php';Requirements: PHP ≥ 8.1.
ext-curlonly for the built-in health checker.
require __DIR__ . '/autoload.php';
$pool = Pool::fromList([
'203.0.113.1:8080',
'user:pass@203.0.113.2:3128',
'socks5://203.0.113.3:1080',
])->strategy(Strategy::LeastLatency);
$pool->check(); // probe all, score latency (async)
$proxy = $pool->next(); // best available proxy
$ok = doRequestThrough($proxy); // ... your request ...
$pool->report($proxy, $ok, $latency); // feed the outcome backProxies parse from host:port, user:pass@host:port, or a full
scheme://… URL. Scheme defaults to http.
$pool->strategy(Strategy::RoundRobin); // cycle in order (default)
$pool->strategy(Strategy::Random); // uniformly random
$pool->strategy(Strategy::LeastLatency); // fastest measured proxy firstEvery strategy only ever returns a proxy that's available — not banned, not
cooling down. When the pool is dry, next() throws OrbitException; use
tryNext() for a null-returning variant.
while ($proxy = $pool->tryNext()) {
// ... until everything is exhausted
}check() probes every proxy in parallel and folds the results back in as
success/failure — updating latency scores and cooling down the dead ones:
$summary = $pool->check('https://httpbin.org/ip', timeout: 8, concurrency: 20);
// ['checked' => 50, 'alive' => 43, 'dead' => 7]
$pool->available(); // Proxy[] ready to use now
$pool->banned(); // Proxy[] struck out
$pool->stats(); // ['total'=>50, 'available'=>43, 'cooling_down'=>4, 'banned'=>3, 'avg_latency'=>0.31, ...]The checker lives behind an interface, so tests (and exotic transports) can supply their own:
class MyProber implements ProberInterface { /* return [id => ['ok'=>bool,'latency'=>?float]] */ }
$pool->check(prober: new MyProber());Report a failure and Orbit sidelines the proxy with exponential backoff; after
banThreshold consecutive strikes it's banned outright. A single success
resets the counter.
$pool = new Pool(
strategy: Strategy::RoundRobin,
banThreshold: 5, // ban after 5 straight failures
baseCooldown: 2.0, // first cooldown, seconds
maxCooldown: 300.0, // cap
);
$pool->report($proxy, false); // 2s, then 4s, 8s, 16s … cooldown
$pool->report($proxy, true); // recovered — counter back to zero
$pool->revive($proxy); // manually lift a ban / cooldownOrbit is the scale layer of the PulsarX stack — hand each request a healthy proxy and report the result straight back:
$s = new Pulsar();
foreach ($urls as $url) {
$proxy = $pool->next();
$r = $s->get($url, server: $proxy->forPulsar());
$pool->report($proxy, $r->ok(), $r->getElapsed());
}forPulsar() returns exactly the server: array PulsarX expects; curlOptions()
does the same for a raw cURL handle.
Pool
| Method | Returns |
|---|---|
Pool::fromList(array) / fromFile(string) |
Pool |
add($proxy) / addMany(array) |
self |
strategy(Strategy) |
self |
next() / tryNext() |
Proxy / ?Proxy |
report($proxy, bool $ok, ?float $latency) |
void |
revive($proxy) |
void |
check($url, $timeout, $concurrency, ?$prober) |
array{checked,alive,dead} |
available() / banned() / all() |
Proxy[] |
count() / stats() |
int / array |
Proxy
| Method | Returns |
|---|---|
Proxy::parse(string) |
Proxy |
id() / url() |
string |
hasAuth() / isAvailable(?float $now) |
bool |
curlOptions() / forPulsar() / toArray() |
array |
| public state | latency, failures, successes, banned, cooldownUntil, lastCheck |
Strategy — RoundRobin · Random · LeastLatency
Orbit/
├── autoload.php # zero-dependency autoloader
├── composer.json
├── example.php # runnable demo (no network needed)
├── tests/run.php # framework-free test suite (fake prober + injected clock)
└── src/
├── Pool.php # rotation · cooldown · banning · scoring · checks
├── Proxy.php # one endpoint + its health state
├── Strategy.php # RoundRobin | Random | LeastLatency
├── ProberInterface.php # health-check contract
├── CurlProber.php # default async curl_multi prober
└── OrbitException.php
Run
php example.phpfor a live demo, orphp tests/run.phpfor the test suite.
⋆ ˚ 。 ⋆ ୨ ⋆ ˚ 。 ⋆
Orbit — made by Vxsilisk · MIT License
Part of the Nebula toolkit: PulsarX (fetch) · Quasar (parse) · Photon (identity) · Orbit (scale)