Integration · HTTP clients
How to use a proxy with PHP Guzzle and cURL
Guzzle uses a proxy through its proxy request option: a URL such as http://user:pass@host:port, set on the client or on a single request. Plain PHP cURL does the same with CURLOPT_PROXY and CURLOPT_PROXYUSERPWD. Guzzle's default handler is cURL, so both behave alike: HTTPS targets go through a CONNECT tunnel, and the ARC username decides pool, country, city and session.
Reviewed 30 Oct 2026
http://USERNAME _pool-arcfast _country-de:PASSWORD@ proxy.arcproxies.io:8000The short answer
- Set
'proxy' => 'http://user:pass@host:port'on the client; override it per request for a different session or country. - Guzzle's
connect_timeoutandtimeoutdefault to 0, which waits indefinitely. Set both. - A refused tunnel throws
ConnectExceptionwith "CONNECT tunnel failed, response 407"; stop there instead of retrying. - With plain cURL,
CURLINFO_HTTP_CONNECTCODEgives the proxy's answer to CONNECT.
Setup
Set up PHP Guzzle with a proxy, step by step
- 01
Install Guzzle
Guzzle 7 needs Composer. The plain cURL examples further down need only the
curlextension.shell1 linecomposer require guzzlehttp/guzzle:^7 - 02
Send a request and check the exit IP
Put the credentials in the proxy URL. If the password contains
@,:or/, encode it withrawurlencode()first.exit_ip.php13 lines<?phprequire __DIR__ . '/vendor/autoload.php';use GuzzleHttp\Client;$client = new Client([ 'proxy' => 'http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000', 'connect_timeout' => 10, 'timeout' => 30,]);$res = $client->get('https://api.ipify.org', ['query' => ['format' => 'json']]);echo $res->getStatusCode(), ' ', $res->getBody(), PHP_EOL; - 03
Hold one exit for a task
Pass a sticky username in the request's own
proxyoption; it overrides the client's. Every request with the same handle leaves from the same IP for up to_sesstime-minutes.sticky.php21 lines<?phprequire __DIR__ . '/vendor/autoload.php';use GuzzleHttp\Client;function stickyProxy(int $minutes = 10): string{ $handle = bin2hex(random_bytes(4)); return "http://USERNAME_pool-arcfast_country-de_session-{$handle}_sesstime-{$minutes}:PASSWORD@proxy.arcproxies.io:8000";}$client = new Client(['connect_timeout' => 10, 'timeout' => 30, 'http_errors' => false]);$proxy = stickyProxy();foreach ([1, 2, 3] as $page) { $res = $client->get('https://example.com/search', [ 'proxy' => $proxy, 'query' => ['q' => 'lamp', 'page' => $page], ]); echo $page, ' ', $res->getStatusCode(), PHP_EOL;} - 04
Target a country or city
The array form maps target schemes to proxies and takes a
nolist of hosts to reach directly. Guzzle fillsnofromNO_PROXYonly when you do not pass your ownproxyoption.geo.php21 lines<?phprequire __DIR__ . '/vendor/autoload.php';use GuzzleHttp\Client;$markets = [ 'de-hamburg' => 'USERNAME_pool-arcfast_country-de_city-hamburg', 'it-milan' => 'USERNAME_pool-arcfast_country-it_city-milan', 'au-melbourne' => 'USERNAME_pool-arcfast_country-au_city-melbourne',];foreach ($markets as $market => $user) { $proxy = "http://{$user}:PASSWORD@proxy.arcproxies.io:8000"; $client = new Client([ 'proxy' => ['http' => $proxy, 'https' => $proxy, 'no' => ['localhost']], 'connect_timeout' => 10, 'timeout' => 30, 'http_errors' => false, // return 4xx/5xx instead of throwing ]); echo $market, ' ', $client->get('https://example.com/price/123')->getStatusCode(), PHP_EOL;} - 05
Retry rate limits, stop on 407
Middleware::retrytakes a decider and a delay in milliseconds. Retry 429, 5xx and connection failures; a refused tunnel is also aConnectException, so check the message for the 407 first.retry.php32 lines<?phprequire __DIR__ . '/vendor/autoload.php';use GuzzleHttp\Client;use GuzzleHttp\Exception\ConnectException;use GuzzleHttp\HandlerStack;use GuzzleHttp\Middleware;use Psr\Http\Message\RequestInterface;use Psr\Http\Message\ResponseInterface;$stack = HandlerStack::create();$stack->push(Middleware::retry( function (int $retries, RequestInterface $req, ?ResponseInterface $res = null, ?\Throwable $err = null): bool { if ($retries >= 3) { return false; } if ($err instanceof ConnectException) { return !str_contains($err->getMessage(), 'response 407'); // wrong credentials: stop } return $res !== null && ($res->getStatusCode() === 429 || $res->getStatusCode() >= 500); }, fn (int $retries): int => 1000 * 2 ** $retries // 2 s, 4 s, 8 s));$client = new Client([ 'handler' => $stack, 'proxy' => 'http://USERNAME_pool-arcfast_country-de:PASSWORD@proxy.arcproxies.io:8000', 'connect_timeout' => 10, 'timeout' => 30,]);echo $client->get('https://example.com/')->getStatusCode(), PHP_EOL; - 06
Plain PHP cURL
Without Guzzle, set
CURLOPT_PROXYandCURLOPT_PROXYUSERPWD. After a failure,CURLINFO_HTTP_CONNECTCODEholds the proxy's answer to CONNECT: 407 means the credentials were rejected. Reusing the handle reuses the connection, and with it the exit.curl.php16 lines<?php$ch = curl_init('https://api.ipify.org?format=json');curl_setopt_array($ch, [ CURLOPT_PROXY => 'http://proxy.arcproxies.io:8000', CURLOPT_PROXYUSERPWD => 'USERNAME_pool-arcfast_country-de:PASSWORD', CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30,]);$body = curl_exec($ch);if ($body === false) { $proxyStatus = curl_getinfo($ch, CURLINFO_HTTP_CONNECTCODE); // 407 = bad credentials exit("failed: " . curl_error($ch) . " (proxy said {$proxyStatus})" . PHP_EOL);}echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $body, PHP_EOL;
Guzzle proxy option forms
| Form | Example | Use it for |
|---|---|---|
| String on the client | 'proxy' => 'http://user:pass@host:port' | One pool and country for everything |
| String on a request | $client->get($url, ['proxy' => $sticky]) | A session or market per task |
| Array | ['http' => $p, 'https' => $p, 'no' => ['localhost']] | Different proxies per scheme, or hosts to skip |
| SOCKS5 | 'proxy' => 'socks5h://user:pass@host:port' | Same gateway and port; DNS resolved at the proxy |
Guzzle's documentation shows HTTP proxy URLs; the socks5h:// form is passed through to the cURL handler, and worked against our test gateway. With plain cURL the equivalent is CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME.
Connection reuse and rotation in PHP
One Guzzle client keeps its cURL handles and reuses connections: two requests to the same host through the same proxy shared one tunnel, and so one exit, in our test. A request with a different proxy URL opened a new tunnel. To spread requests over exits, give each a different session handle; to keep an exit, reuse the handle. Rotating vs sticky sessions explains the trade-offs, and handling 429 rate limits the backoff.
PHP-FPM workers each run their own process, so a client created per web request starts with no open connections. For crawling, a long-running CLI worker reuses tunnels and saves the handshake each time.
The problem
Common pitfalls
No timeouts
Guzzle's connect_timeout and timeout default to 0, meaning no limit. A stalled tunnel then blocks the worker indefinitely.
Retrying every ConnectException
A refused tunnel is a ConnectException too. Check for "response 407" before retrying, or the retries just repeat the wrong password.
Relying on NO_PROXY with an explicit proxy option
Guzzle only reads NO_PROXY when it builds the proxy from the environment. With your own proxy option, list skipped hosts under no.
An unencoded password in the URL
A @ or : in the password breaks the proxy URL. Use rawurlencode() on it, or with plain cURL pass it through CURLOPT_PROXYUSERPWD.
FAQ
Questions
Something not covered? Write to contact@arcproxies.io.
How do I set a proxy with authentication in Guzzle?
Pass the credentials in the proxy URL: new Client(['proxy' => 'http://user:pass@host:port']). Guzzle hands it to cURL, which sends Basic proxy authentication, including on the CONNECT request for HTTPS sites.
How do I use a different proxy per request in Guzzle?
Pass proxy in the request options: $client->get($url, ['proxy' => $otherProxy]). Request options override the client's defaults, so one client can serve several sessions or countries.
What does "cURL error 7: CONNECT tunnel failed, response 407" mean?
The proxy refused to open the HTTPS tunnel because the credentials did not match. Copy the username and password again from the dashboard and encode special characters. Retrying will not help.
How do I set a proxy in plain PHP cURL?
Set CURLOPT_PROXY to http://host:port and CURLOPT_PROXYUSERPWD to user:pass with curl_setopt_array(). For SOCKS5 with remote DNS, add CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME.
Does Guzzle support SOCKS5 proxies?
With the default cURL handler, yes: socks5h://user:pass@host:port in the proxy option is passed to cURL. socks5h makes the proxy resolve the hostname.
Sources & review
Last reviewed by the ARC Proxies team. Prices are read from the live catalog at build time.
- [1]Guzzle: Request Options: proxy, timeout, connect_timeout
- [2]Guzzle: Middleware::retry (src/Middleware.php)
- [3]PHP: curl_setopt
- [4]PHP: curl_getinfo
Next
What people build with it
Integrations
Other http clients
Get started
Get credentials and paste them in.
Sign up, buy a gigabyte, generate a username in the dashboard. Code WELCOME15 takes 15% off your first purchase; bandwidth never expires.