PHP still powers a huge amount of server-side scraping and integration work, and sooner or later you need to route a request through a proxy. Whether you are on raw cURL or on Guzzle, the setup is straightforward once you know where the options go. Here is a practical guide for both, including the auth and HTTPS details that quietly break things.
Raw cURL in PHP
PHP's cURL extension mirrors the command-line tool. You set the proxy with CURLOPT_PROXY and, if needed, credentials with CURLOPT_PROXYUSERPWD.
$ch = curl_init("https://example.com");
curl_setopt_array($ch, [
CURLOPT_PROXY => "proxy.host:8080",
CURLOPT_PROXYUSERPWD => "user:pass",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
For a SOCKS5 proxy, set the type explicitly so cURL does not assume HTTP:
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
SOCKS5_HOSTNAME resolves DNS at the proxy, the PHP equivalent of socks5h, so your lookups do not leak locally.
Guzzle
Guzzle wraps cURL but exposes a cleaner API. The proxy goes in the request options, and you can put credentials right in the URL.
use GuzzleHttp\Client;
$client = new Client([
"proxy" => "http://user:pass@proxy.host:8080",
"timeout" => 20,
]);
$res = $client->get("https://example.com");
echo $res->getStatusCode();
Guzzle also lets you set different proxies per scheme, which is handy when you want HTTP and HTTPS handled distinctly:
"proxy" => [
"http" => "http://user:pass@proxy.host:8080",
"https" => "http://user:pass@proxy.host:8080",
"no" => ["localhost"],
],
The details that break silently
-
URL-encode special characters in the password. An
@or:in a raw proxy string splits the URL in the wrong place and produces a confusing failure. -
Verify TLS. Do not disable
CURLOPT_SSL_VERIFYPEERto "make it work." A tunnel problem is a proxy or agent issue; turning off verification hides real errors and creates a security hole. -
Set timeouts. Without
CURLOPT_TIMEOUT, a dead proxy hangs your worker indefinitely.
Rotation from PHP
PHP scrapers usually run as workers or cron jobs, so the clean pattern is to point every request at one rotating endpoint and let the pool cycle exits. You keep one proxy string in config, and each request lands on a fresh address without you maintaining a list. When a job must hold a session, pin it to a stable exit instead.
What the pool needs
WinGate fits PHP scraping directly: private IPv4 proxies with SOCKS5 and standard auth, so CURLOPT_PROXYUSERPWD and Guzzle's URL credentials both just work. The rotating pool cycles exits from a worldmix, traffic is unlimited so long-running workers do not meter you, and it speaks HTTP, HTTPS, and SOCKS5, so raw cURL and Guzzle share the same account. There is a free 2 hour test, so wire it into a small script and confirm the exit rotates before you commit.
An honest note: the proxy config stops the address from being the reason your PHP scraper gets blocked, it does not hide automation or exempt you from a site's terms. Pacing and rotation still do the real anti-blocking work.
The takeaway: in raw cURL use CURLOPT_PROXY and CURLOPT_PROXYUSERPWD, set SOCKS5_HOSTNAME for SOCKS, in Guzzle put the proxy in request options, encode special characters, keep TLS verification on, and route everything through a rotating pool. Do that and PHP handles proxied requests as cleanly as anything else.

Top comments (0)