All guides
DEVELOPER GUIDES · 4 MIN READ

How to configure a Guzzle proxy in PHP

Set a Guzzle HTTP proxy, control NO_PROXY behavior, and verify the exit IP. Includes timeout, authentication, and response checks.

How to configure a Guzzle proxy in PHP

Quick answer

To send a Guzzle request through a proxy, set the proxy request option to an explicit HTTP proxy URI and stream the body through a local byte cap, so one oversized response cannot burn the traffic budget. Accept the result only when the same configured client returns the expected exit IP and the page contains the expected marker; a rotating proxy can change the address between requests.

Diagram: Without a cap, Guzzle can keep reading a large body through the proxy. With the proxy option and a streamed byte cap, the response stops at the limit and the marker check passes.

Warning: These requests consume proxy traffic and may incur charges; a body-size check cannot refund bytes already transferred. This example uses an unencrypted HTTP client-to-proxy connection, so credentials can be exposed on that link.

Start with a sandbox or test account, set the provider budget, and use a trusted network. Remove any credential binding that reaches logs.

Audience: PHP developers who know Composer, namespaces, and exceptions.

Check prerequisites

Tested with PHP 8.4.1 and Guzzle 8.2.0. Install PHP from the official PHP downloads page, then install the exact Guzzle version in the application with Composer:

composer require guzzlehttp/guzzle:8.2.0

Set PROXY_HOST and PROXY_PORT in the process environment; bind optional PROXY_USER and PROXY_PASSWORD from a secret store. The example passes NO_PROXY explicitly because a custom Guzzle proxy option does not automatically inherit that bypass list.

For a local shell, set the non-secret values with export PROXY_HOST=proxy.example.net PROXY_PORT=8080.

Send a bounded GET

Save this as proxy-get.php beside the application's vendor directory. The pinned streaming handler uses a 15-second request option for setup and headers, then a 10-second idle limit for each streamed body read. This single GET follows no redirects and caps streamed response bodies at 1 MiB.

<?php

declare(strict_types=1);

require __DIR__.'/vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Handler\StreamHandler;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\ResponseInterface;

const TARGET_URL = 'https://example.com/';
const EXIT_CHECK_URL = 'https://api.ipify.org?format=json';
const EXPECTED_MARKER = 'Example Domain';
const REQUEST_TIMEOUT = 15.0;
const READ_TIMEOUT = 10.0;
const MAX_BODY_BYTES = 1_048_576;

function requiredEnv(string $name): string
{
    $value = getenv($name);
    if ($value === false || trim($value) === '') {
        throw new RuntimeException("Missing environment binding: {$name}");
    }

    return trim($value);
}

function readBoundedBody(ResponseInterface $response): string
{
    $stream = $response->getBody();
    $body = '';
    try {
        while (! $stream->eof()) {
            $chunk = $stream->read(min(8192, MAX_BODY_BYTES + 1 - strlen($body)));
            if ($chunk === '' && ! $stream->eof()) {
                throw new RuntimeException('Response read made no progress');
            }
            $body .= $chunk;
            if (strlen($body) > MAX_BODY_BYTES) {
                throw new LengthException('Response exceeded the 1 MiB limit');
            }
        }

        return $body;
    } finally {
        $stream->close();
    }
}

if (! filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN)) {
    throw new RuntimeException('Enable allow_url_fopen for the streaming handler');
}

$host = requiredEnv('PROXY_HOST');
$port = filter_var(getenv('PROXY_PORT'), FILTER_VALIDATE_INT);
if ($port === false || $port < 1 || $port > 65535) {
    throw new RuntimeException('PROXY_PORT must be a TCP port from 1 through 65535');
}
$user = getenv('PROXY_USER');
$password = getenv('PROXY_PASSWORD');
if (($user === false) !== ($password === false)) {
    throw new RuntimeException('Set both proxy credential bindings or neither');
}
$proxy = 'http://';
if ($user !== false) {
    $proxy .= rawurlencode($user).':'.rawurlencode($password).'@';
}
$proxy .= $host.':'.$port;
$noProxy = array_values(array_filter(array_map('trim', explode(',', getenv('NO_PROXY') ?: ''))));
$client = new Client([
    'allow_redirects' => false,
    'handler' => HandlerStack::create(new StreamHandler()),
    'http_errors' => false,
    'proxy' => ['http' => $proxy, 'https' => $proxy, 'no' => $noProxy],
    'timeout' => REQUEST_TIMEOUT,
]);

foreach ([TARGET_URL, EXIT_CHECK_URL] as $url) {
    try {
        $response = $client->request('GET', $url, [
            'read_timeout' => READ_TIMEOUT,
            'stream' => true,
        ]);
    } catch (Throwable) {
        throw new RuntimeException('Request failed during transport or proxy negotiation');
    }
    $status = $response->getStatusCode();
    try {
        $body = readBoundedBody($response);
    } catch (LengthException) {
        throw new RuntimeException('Response exceeded the 1 MiB limit');
    } catch (Throwable) {
        throw new RuntimeException('Response read failed; check the read timeout and stream');
    }
    if ($status !== 200) {
        throw new RuntimeException("Request returned HTTP {$status}");
    }
    if ($url === TARGET_URL && ! str_contains($body, EXPECTED_MARKER)) {
        throw new RuntimeException('Target marker was absent');
    }
    if ($url === EXIT_CHECK_URL) {
        $ipResult = json_decode($body, true);
        $exitIp = is_array($ipResult) ? ($ipResult['ip'] ?? null) : null;
        if (! is_string($exitIp) || filter_var($exitIp, FILTER_VALIDATE_IP) === false) {
            throw new RuntimeException('IP-check response did not contain a valid IP address');
        }
    }
    printf("status=%d body_bytes=%d\n", $status, strlen($body));
    if ($url === EXIT_CHECK_URL) {
        printf("exit_ip=%s\n", $exitIp);
    }
}

Run the script from the project root:

php proxy-get.php

The example builds the proxy URI in process memory. It replaces caught transport and stream exceptions with fixed messages that omit that URI and its credentials; an uncaught failure still prints the controlled message and a PHP stack trace. On success the example sends two application GET requests and follows no redirects; it makes no application-level retries.

With stream=true, the pinned 8.2.0 StreamHandler returns the body before consuming it. read_timeout limits each idle read; the request timeout does not impose a deadline on subsequent body consumption.

A slow stream can therefore outlive 15 seconds. Use an external worker deadline when a total duration limit is required. The 1 MiB limit is a local acceptance cap, not a provider traffic quota.

Illustrative output. The IP is replaced with a documentation address; body byte counts can vary. HTTP 200 means the request succeeded:

status=200 body_bytes=559
status=200 body_bytes=23
exit_ip=203.0.113.10

Distinguish proxy and target responses

Observation Likely boundary Next check
Transfer fails before an HTTP response Client, proxy, TLS, or CONNECT Check proxy scheme, host, port, and credentials without dumping the exception
HTTP 407 Proxy authentication Repair proxy credentials or entitlement; do not retry the same secret
HTTP 401 or 403 Target policy Check target-side login or permission
HTTP 502, 503, or 504 Gateway or target availability Defer; retry only under an explicit bounded policy that honors Retry-After
HTTP 200 without the marker Application validation Reject the response and inspect content assumptions
Body exceeds 1 MiB Local resource policy Reject it or design a different bounded stream policy

The IP-check response shows the address seen for that request. It does not establish geographic accuracy or a stable exit across rotating requests. See the curl proxy guide for inspecting a CONNECT tunnel and the HTTP 407 guide for proxy challenges.

Sources and further reading

ProxyLaneTraffic never expires

Premium residential proxies
From $2/GB

Scale your workflows with the tools you already use. Rotating or sticky sessions up to 72 hours.

Try 350 MB for $1.95 View plans Proxies for web scraping

Keep reading

Amazon Scraper API vs Proxy: Choose a Product Data Source

Compare seller APIs, licensed Amazon product data and proxy page checks by record, access scope and reuse rights.

Read guide

Antidetect browser proxy setup: choose a profile guide

Choose the right anti-detect browser proxy guide, map the shared verification steps, and keep account access separate from the network route.

Read guide

Best Proxies for Market Research: Choose by Sample and Cost

Our picks for market research: ProxyLane for small, intermittent projects, plus options for regional tracking, monthly collection and managed extraction.

Read guide