Wissen

Residential Proxies in PHP mit cURL und Guzzle verwenden

Proxies in PHP: cURLs CURLOPT_PROXY und CURLOPT_PROXYUSERPWD, Guzzles proxy-Option und Verbindungs-Wiederverwendung, CURLOPT_HTTPPROXYTUNNEL und Geo-Rotation pro Anfrage.

Chris Collins

Chris Collins

26. Juli 2026 · 8 Min. Lesezeit

PHP bewegt nach wie vor eine enorme Menge des Server-zu-Server-Verkehrs im Web, und das meiste davon läuft über einen von zwei Wegen: reines cURL oder Guzzle, das auf cURL aufsitzt. Einen Residential Proxy in einen der beiden einzubinden sind ein paar Optionen, keine Neuimplementierung. Die Reibung steckt in den Details, die jeder anders handhabt: wie die Proxy-Authentifizierung übergeben wird, eine Einstellung, die darüber entscheidet, ob HTTPS über den Proxy überhaupt funktioniert, und das Verhalten der Verbindungs-Wiederverwendung, das einen schnellen Scraper von einem trennt, der bei jeder Anfrage einen vollständigen Handshake zahlt.

Dies ist der PHP-Beitrag derselben Serie wie Residential Proxies mit Python, mit Playwright und mit Go: der Code, der für cURL und für Guzzle funktioniert, plus die PHP-spezifischen Fallen.

Alles unten nutzt Shifters Residential Gateway: ein Endpunkt, p.shifter.io:443, mit dem gesamten Targeting im Benutzernamen kodiert. Tausche Host und Zugangsdaten für einen anderen Anbieter aus; die Form bleibt gleich.

Das Gateway-Modell in einem Absatz

Der Proxy-Benutzername trägt deine Authentifizierung und dein Targeting. Du wechselst nicht den Endpunkt, um Land oder Session zu ändern, du änderst die Benutzernamen-Zeichenkette:

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us zielt auf die USA, sid fixiert eine Sticky Session, ttl hält diese IP für N Sekunden. Lässt du sid/ttl weg, rotiert jede neue Verbindung. Das Passwort bleibt konstant. Diese ganze Zeichenkette ist der Benutzername, den du cURL oder Guzzle übergibst.

Reines cURL

cURL nimmt den Proxy-Host in CURLOPT_PROXY und die Zugangsdaten in CURLOPT_PROXYUSERPWD. Du kannst user:pass@host auch inline in die Proxy-Zeichenkette schreiben, aber die Zugangsdaten in ihrer eigenen Option zu halten ist sauberer und erspart URL-Kodierungs-Kopfschmerzen mit dem langen Benutzernamen.

<?php
$user = getenv('SHIFTER_USER') . '-country-us';
$pass = getenv('SHIFTER_PASS');
$ch = curl_init('https://api.ipify.org');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'p.shifter.io:443',
CURLOPT_PROXYUSERPWD => "$user:$pass",
CURLOPT_HTTPPROXYTUNNEL => true, // CONNECT-Tunnel für HTTPS über den Proxy
CURLOPT_CONNECTTIMEOUT => 10, // nur Verbindungsphase
CURLOPT_TIMEOUT => 30, // gesamter Transfer
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, 'curl error: ' . curl_error($ch) . "\n");
} else {
echo $body, "\n"; // eine US-Residential-IP
}
curl_close($ch);

Zwei Dinge zum Verinnerlichen. Die $user-Zeichenkette enthält die Targeting-Flags (-country-us), weil die Geo dort lebt. Und CURLOPT_HTTPPROXYTUNNEL ist das, was HTTPS-über-einen-Proxy funktionieren lässt: es weist cURL an, einen CONNECT-Tunnel zu öffnen, damit TLS Ende-zu-Ende mit dem Ziel ausgehandelt wird, nicht mit dem Proxy. Lässt du es aus, schlagen HTTPS-Anfragen über einen HTTP-Proxy fehl oder verhalten sich seltsam. Das ist der mit Abstand häufigste PHP-Proxy-Fehler.

Guzzle

Guzzle nimmt den Proxy über die proxy-Option der Anfrage (oder des Clients), mit den Zugangsdaten inline in der URL. Da Guzzle ein cURL-Wrapper ist, gilt dasselbe Tunnel-Verhalten darunter, aber Guzzle erledigt den CONNECT für https://-Ziele automatisch.

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$user = getenv('SHIFTER_USER') . '-country-us';
$pass = getenv('SHIFTER_PASS');
$proxy = "http://$user:$pass@p.shifter.io:443";
// Baue den Client EINMAL und verwende ihn wieder (siehe Hinweis zur Verbindungs-Wiederverwendung unten).
$client = new Client([
'proxy' => $proxy,
'connect_timeout' => 10, // Verbindungsphase
'timeout' => 30, // gesamte Anfrage
]);
$res = $client->get('https://api.ipify.org');
echo $res->getBody(), "\n"; // eine US-Residential-IP

Beachte die getrennten connect_timeout und timeout. Genau diese Trennung Verbindung-gegen-Gesamt macht Timeouts diagnostizierbar, wenn etwas hängt: eine langsame Verbindung deutet auf deine Seite oder eine fehlende passende IP, eine langsame Gesamtzeit auf das Ziel.

Guzzle akzeptiert die proxy-Option auch pro Anfrage und als nach Schema indiziertes Array, so leitest du nur https über den Proxy oder setzt eine no-Ausnahmeliste:

$res = $client->get('https://example.com', [
'proxy' => [
'http' => $proxy,
'https' => $proxy,
'no' => ['localhost', '127.0.0.1'],
],
]);

Falle 1: den Guzzle-Client (und das cURL-Handle) wiederverwenden

Ein frischer GuzzleHttp\Client pro Anfrage oder ein frisches curl_init() pro Anfrage zahlt jedes Mal einen vollständigen TCP- + TLS-Handshake über den Proxy, den Overhead, den der Latenz-Leitfaden beseitigen soll. Guzzle hält darunter einen cURL-Handle-Pool und verwendet Verbindungen wieder, wenn du den Client wiederverwendest. Baue einen Client beim Start, injiziere ihn und halte ihn.

Für reines cURL verwende das Handle über Anfragen hinweg wieder und ändere zwischen den Aufrufen nur die URL, damit die Verbindung warm bleibt:

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'p.shifter.io:443',
CURLOPT_PROXYUSERPWD => "$user:$pass",
CURLOPT_HTTPPROXYTUNNEL => true,
]);
foreach ($urls as $url) {
curl_setopt($ch, CURLOPT_URL, $url); // Handle wiederverwenden, Verbindung halten
$body = curl_exec($ch);
// ... $body verarbeiten
}
curl_close($ch);

Falle 2: PHP holt standardmäßig eine URL nach der anderen

PHP-Request-Handler sind standardmäßig synchron. Ein Scraper, der über curl_exec iteriert, holt strikt eine URL nach der anderen, was für kleine Jobs in Ordnung und im großen Maßstab schmerzhaft langsam ist. Zwei Wege nach oben:

Guzzle asynchron mit einem begrenzten Pool, sodass N Anfragen in Flug sind, aber nie mehr:

use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
$requests = function ($urls) {
foreach ($urls as $u) { yield new Request('GET', $u); }
};
$pool = new Pool($client, $requests($urls), [
'concurrency' => 8, // Anfragen in Flug begrenzen
'fulfilled' => function ($response, $i) { /* verarbeiten */ },
'rejected' => function ($reason, $i) { /* log + Retry */ },
]);
$pool->promise()->wait();

Oder curl_multi_*, wenn du auf reinem cURL bist. So oder so, begrenze die Nebenläufigkeit pro Ziel-Host, nicht global, damit eine fragile Seite nicht malträtiert wird, während eine großzügige verhungert. Mehr Parallelität jenseits der Toleranz eines Ziels kauft Blockaden, keinen Durchsatz (wie man Blockaden vermeidet).

Falle 3: die Antwort konsumieren und den Transportfehler prüfen, nicht nur den Status

cURL gibt false bei einem Transportfehler zurück (Proxy abgelehnt, Tunnel fehlgeschlagen, Timeout) und eine Body-Zeichenkette bei einer HTTP-Antwort, selbst bei einem 407 oder 502. Prüfe den Rückgabewert von curl_exec gegen false und lies curl_error/curl_errno, bevor du dem Statuscode traust. In Guzzle wirft ein Verbindungsfehler eine ConnectException, während ein 4xx/5xx nur dann eine RequestException wirft, wenn http_errors aktiv ist (das ist es standardmäßig). Behandle beide und lies immer den Body, damit das Handle zur Wiederverwendung frei ist:

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
try {
$res = $client->get($url);
$body = (string) $res->getBody(); // den Body leeren
} catch (ConnectException $e) {
// Transport: Proxy/Tunnel/Timeout — mit frischer Identität erneut versuchen
} catch (RequestException $e) {
// HTTP-Status — $e->getResponse()->getStatusCode() inspizieren
}

Geo und Sessions rotieren

Da das Targeting im Benutzernamen lebt, ist eine andere Identität eine andere Zugangsdaten-Zeichenkette.

Reines cURL: setze CURLOPT_PROXYUSERPWD vor dem Aufruf auf den neuen Benutzernamen. Dasselbe Handle kann über Iterationen hinweg verschiedene Identitäten tragen.

Guzzle: übergib die proxy-Option pro Anfrage, um den Client-Standard zu überschreiben, sodass ein Client viele Identitäten bedient, ohne neu gebaut zu werden:

function userFor(string $country, ?string $sid = null): string {
$u = getenv('SHIFTER_USER') . '-country-' . $country;
if ($sid !== null) { $u .= '-sid-' . $sid . '-ttl-600'; }
return $u;
}
$pass = getenv('SHIFTER_PASS');
$proxy = 'http://' . userFor('de', 'job-42') . ":$pass@p.shifter.io:443";
$res = $client->get('https://example.com', ['proxy' => $proxy]);

Gib jeder logischen Arbeitseinheit ihre eigene sid und rotiere zwischen Einheiten, nicht mitten im Fluss (Sticky vs. rotierend behandelt die Unterscheidung), und ordne Arbeit Identitäten so zu, wie es der Beitrag zum Lastausgleich beschreibt.

Prüfe, dass du wirklich über den Proxy läufst

Bevor du irgendetwas anderes benchmarkst oder debuggst, bestätige die Exit-IP:

$res = $client->get('http://ip-api.com/json');
echo $res->getBody(), "\n"; // erwarte eine Residential-IP im Zielland

Deine eigene IP bedeutet, dass die Proxy-Option nicht angewandt wird. Ein Hänger bedeutet, dass der lokale Ausgang blockiert ist. Beides wird im Leitfaden zur Timeout-Diagnose behandelt.

FAQ

Warum schlagen meine HTTPS-Anfragen über den Proxy in reinem cURL fehl? Dir fehlt fast sicher CURLOPT_HTTPPROXYTUNNEL. HTTPS über einen HTTP-Proxy braucht einen CONNECT-Tunnel, damit TLS mit dem Ziel ausgehandelt wird, nicht mit dem Proxy. Setze CURLOPT_HTTPPROXYTUNNEL => true. Guzzle macht das für https://-Ziele automatisch, weshalb die Falle nur reines cURL beißt.

Soll ich die Proxy-Zugangsdaten in die URL oder in eine separate Option packen? Beides geht. In reinem cURL hält CURLOPT_PROXYUSERPWD den langen Gateway-Benutzernamen aus der URL heraus und vermeidet URL-Kodierungsprobleme. In Guzzle ist das Inline-Schreiben von user:pass@host in die proxy-Zeichenkette der übliche Weg. Wähle einen und bleib konsistent.

Mein PHP-Scraper ist langsam, obwohl der Proxy schnell ist. Warum? Am wahrscheinlichsten erzeugst du einen neuen Guzzle-Client (oder curl_init) pro Anfrage und zahlst jedes Mal einen frischen Handshake, oder du holst strikt eine URL nach der anderen. Verwende einen Client/ein Handle wieder und nutze einen begrenzten Guzzle-Pool oder curl_multi, um mehrere Anfragen nebenläufig zu fahren.

Wie rotiere ich IPs pro Anfrage in PHP? Variiere den Proxy-Benutzernamen. In reinem cURL setze CURLOPT_PROXYUSERPWD vor jedem Aufruf auf den neuen Benutzernamen. In Guzzle übergib die proxy-Option pro Anfrage. Beides lässt einen geteilten Client oder ein Handle viele Identitäten bedienen.

cURL oder Guzzle zum Scrapen? Guzzle gibt dir asynchrone Pools, Middleware, Retries und PSR-7 für eine kleine Abhängigkeit; reines cURL ist abhängigkeitsfrei und pro Aufruf etwas schneller. Nimm Guzzle, wenn du Nebenläufigkeit und Struktur willst, reines cURL für schlanke, minimale Skripte. Die Proxy-Konfiguration und die Fallen oben gelten für beide, da Guzzle auf cURL läuft.

Das Fazit

PHP plus Residential Proxies ist solide, sobald du die Regeln jedes Clients respektierst: in reinem cURL setze den Proxy-Host und CURLOPT_PROXYUSERPWD und vergiss nie CURLOPT_HTTPPROXYTUNNEL für HTTPS; in Guzzle übergib die proxy-Option und lass es tunneln. Verwende einen Client oder ein Handle wieder, damit Verbindungen warm bleiben, fahre Anfragen nebenläufig mit einem begrenzten Pool statt eine nach der anderen, begrenze die Nebenläufigkeit pro Ziel-Host und prüfe den Transportfehler, nicht nur den Statuscode. Variiere den Proxy-Benutzernamen, um Geo oder Session zu wechseln.

Mach das richtig, und beide Wege leisten, was PHP im großen Maßstab leisten sollte. Richte sie auf das Residential Gateway, und denk daran, dass die Pool-Qualität entscheidet, wie oft du überhaupt erneut versuchst (IP-Reputation). Die Preisseite hat die Pro-GB-Tarife, um es gegen deine eigenen Ziele zu testen.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab $0.75/GB.

Jetzt starten