Conhecimento

Como Usar Proxies Residenciais em PHP com cURL e Guzzle

Proxies em PHP: CURLOPT_PROXY e CURLOPT_PROXYUSERPWD do cURL, a opção proxy do Guzzle, CURLOPT_HTTPPROXYTUNNEL e rotação geográfica por requisição.

Chris Collins

Chris Collins

26 de julho de 2026 · 10 min de leitura

PHP ainda movimenta uma quantidade enorme do tráfego servidor-a-servidor da web, e a maior parte passa por um de dois caminhos: cURL puro, ou Guzzle em cima do cURL. Conectar um proxy residencial a qualquer um deles é questão de algumas opções, não uma reescrita. O atrito está nos detalhes que cada um trata de forma diferente: como a autenticação do proxy é passada, uma configuração que decide se o HTTPS através do proxy funciona ou não, e o comportamento de reuso de conexão que separa um scraper rápido de um que paga um handshake completo a cada requisição.

Este é o capítulo de PHP da mesma série de proxies residenciais com Python, com Playwright, e em Go: o código que funciona tanto para cURL quanto para Guzzle, além das armadilhas específicas do PHP.

Tudo abaixo usa o gateway residencial da Shifter: um único endpoint, p.shifter.io:443, com toda a segmentação codificada no nome de usuário. Troque o host e as credenciais por outro provedor; a estrutura é a mesma.

O modelo de gateway em um parágrafo

O nome de usuário do proxy carrega sua autenticação e sua segmentação. Você não troca de endpoint para mudar o país ou a sessão, você muda a string do nome de usuário:

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

country-us direciona para os Estados Unidos, sid fixa uma sessão persistente, ttl mantém esse IP por N segundos. Omita sid/ttl e cada nova conexão gira. A senha permanece constante. Essa string inteira é o nome de usuário que você entrega ao cURL ou ao Guzzle.

cURL puro

O cURL recebe o host do proxy em CURLOPT_PROXY e as credenciais em CURLOPT_PROXYUSERPWD. Você também pode colocar user:pass@host inline na string do proxy, mas manter as credenciais em sua própria opção é mais limpo e evita dores de cabeça com URL-encoding devido ao nome de usuário longo.

<?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, // túnel CONNECT para HTTPS através do proxy
CURLOPT_CONNECTTIMEOUT => 10, // apenas a fase de conexão
CURLOPT_TIMEOUT => 30, // toda a transferência
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, 'curl error: ' . curl_error($ch) . "\n");
} else {
echo $body, "\n"; // um IP residencial dos EUA
}
curl_close($ch);

Duas coisas para internalizar. A string $user inclui os sinalizadores de segmentação (-country-us), porque a geolocalização está ali. E CURLOPT_HTTPPROXYTUNNEL é o que faz o HTTPS através de um proxy funcionar: ele diz ao cURL para abrir um túnel CONNECT para que o TLS seja negociado de ponta a ponta com o destino, não com o proxy. Deixe desativado e as requisições HTTPS através de um proxy HTTP falham ou se comportam de forma estranha. Este é o erro mais comum em PHP com proxy.

Guzzle

O Guzzle recebe o proxy através da opção proxy (de requisição ou de cliente), com as credenciais inline na URL. Como o Guzzle é um wrapper do cURL, o mesmo comportamento de túnel se aplica por baixo dos panos, mas o Guzzle trata o CONNECT para destinos https:// automaticamente.

<?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";
// Construa o cliente UMA VEZ e reutilize-o (veja a nota sobre reuso de conexão abaixo).
$client = new Client([
'proxy' => $proxy,
'connect_timeout' => 10, // fase de conexão
'timeout' => 30, // requisição inteira
]);
$res = $client->get('https://api.ipify.org');
echo $res->getBody(), "\n"; // um IP residencial dos EUA

Note os valores separados connect_timeout e timeout. Essa divisão entre conexão e total é exatamente o que torna os timeouts diagnosticáveis quando algo trava: uma conexão lenta aponta para o seu lado ou para uma falta de IP correspondente, um total lento aponta para o destino.

O Guzzle também aceita a opção proxy por requisição e como um array indexado por esquema, o que permite rotear apenas https através do proxy ou definir uma lista de exceção no:

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

Armadilha 1: reutilize o cliente Guzzle (e o handle do cURL)

Um novo GuzzleHttp\Client a cada requisição, ou um novo curl_init() a cada requisição, paga um handshake TCP + TLS completo através do proxy toda vez, exatamente a sobrecarga que o guia de latência existe para eliminar. O Guzzle mantém um pool de handles do cURL por baixo e reutiliza conexões quando você reutiliza o cliente. Construa um cliente na inicialização, injete-o, e mantenha-o.

Para cURL puro, reutilize o handle entre requisições e mude apenas a URL entre as chamadas, para que a conexão permaneça ativa:

$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); // reutiliza o handle, mantém a conexão ativa
$body = curl_exec($ch);
// ... trata $body
}
curl_close($ch);

Armadilha 2: o padrão do PHP é buscar uma URL por vez

Os manipuladores de requisição do PHP são síncronos por padrão. Um scraper que itera sobre curl_exec busca estritamente uma URL por vez, o que é aceitável para trabalhos pequenos e dolorosamente lento em escala. Duas formas de superar isso:

Guzzle assíncrono com um pool limitado, de modo que N requisições estejam em andamento, mas nunca mais que isso:

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, // limite de requisições em andamento
'fulfilled' => function ($response, $i) { /* trata */ },
'rejected' => function ($reason, $i) { /* registra + tenta novamente */ },
]);
$pool->promise()->wait();

Ou curl_multi_* se você estiver usando cURL puro. De qualquer forma, limite a concorrência por host de destino, não globalmente, para que um site frágil não seja sobrecarregado enquanto um mais permissivo fica ocioso. Mais paralelismo além da tolerância de um destino compra bloqueios, não throughput (como evitar ser bloqueado).

Armadilha 3: consuma a resposta, e verifique o erro de transporte, não apenas o status

O cURL retorna false em uma falha de transporte (proxy recusado, túnel falhou, timeout) e uma string de corpo em uma resposta HTTP, mesmo que seja um 407 ou 502. Verifique o retorno de curl_exec contra false e leia curl_error/curl_errno antes de confiar no código de status. No Guzzle, uma falha de conexão lança ConnectException enquanto um 4xx/5xx lança RequestException apenas se http_errors estiver ativo (está, por padrão). Trate ambos, e sempre leia o corpo para que o handle fique livre para reuso:

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
try {
$res = $client->get($url);
$body = (string) $res->getBody(); // esvazia o corpo
} catch (ConnectException $e) {
// transporte: proxy/túnel/timeout — tenta novamente com uma identidade nova
} catch (RequestException $e) {
// status HTTP — inspeciona $e->getResponse()->getStatusCode()
}

Rotacionando geolocalização e sessões

Como a segmentação está no nome de usuário, uma identidade diferente é uma string de credencial diferente.

cURL puro: defina CURLOPT_PROXYUSERPWD com o novo nome de usuário antes da chamada. O mesmo handle pode carregar identidades diferentes entre iterações.

Guzzle: passe a opção proxy por requisição para sobrescrever o padrão do cliente, de modo que um único cliente atenda muitas identidades sem precisar ser reconstruído:

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]);

Dê a cada unidade lógica de trabalho seu próprio sid e rotacione entre unidades, não no meio de um fluxo (sticky vs. rotativo cobre essa distinção), e mapeie o trabalho para identidades da forma como o post sobre balanceamento de carga descreve.

Verifique se você está realmente usando o proxy

Antes de fazer benchmark ou depurar qualquer outra coisa, confirme o IP de saída:

$res = $client->get('http://ip-api.com/json');
echo $res->getBody(), "\n"; // espera-se um IP residencial no país segmentado

Seu próprio IP significa que a opção de proxy não está sendo aplicada. Uma travada significa que a saída local está bloqueada. Ambos os casos são cobertos no guia de diagnóstico de timeout.

Perguntas frequentes

Por que minhas requisições HTTPS falham através do proxy em cURL puro? Você quase certamente está sem CURLOPT_HTTPPROXYTUNNEL. HTTPS através de um proxy HTTP precisa de um túnel CONNECT para que o TLS seja negociado com o destino, não com o proxy. Defina CURLOPT_HTTPPROXYTUNNEL => true. O Guzzle faz isso automaticamente para destinos https://, e é por isso que essa armadilha só afeta o cURL puro.

Devo colocar as credenciais do proxy na URL ou em uma opção separada? Ambos funcionam. Em cURL puro, CURLOPT_PROXYUSERPWD mantém o longo nome de usuário do gateway fora da URL e evita problemas de URL-encoding. No Guzzle, colocar user:pass@host inline na string proxy é o caminho normal. Escolha um e seja consistente.

Meu scraper em PHP está lento mesmo com o proxy sendo rápido. Por quê? O mais provável é que você esteja criando um novo cliente Guzzle (ou curl_init) a cada requisição, pagando um handshake novo toda vez, ou esteja buscando estritamente uma URL por vez. Reutilize um único cliente/handle, e use um Pool limitado do Guzzle ou curl_multi para executar várias requisições simultaneamente.

Como rotacionar IPs por requisição em PHP? Varie o nome de usuário do proxy. Em cURL puro, defina CURLOPT_PROXYUSERPWD com o novo nome de usuário antes de cada chamada. No Guzzle, passe a opção proxy por requisição. Ambos permitem que um cliente ou handle compartilhado atenda muitas identidades.

cURL ou Guzzle para scraping? O Guzzle oferece pools assíncronos, middleware, retentativas e PSR-7 com uma pequena dependência; o cURL puro é livre de dependências e ligeiramente mais rápido por chamada. Use o Guzzle quando quiser concorrência e estrutura, cURL puro para scripts enxutos e mínimos. A configuração de proxy e as armadilhas acima se aplicam a ambos, já que o Guzzle roda sobre o cURL.

Conclusão

PHP mais proxies residenciais é sólido desde que você respeite as regras de cada cliente: em cURL puro, defina o host do proxy e CURLOPT_PROXYUSERPWD, e nunca esqueça CURLOPT_HTTPPROXYTUNNEL para HTTPS; no Guzzle, passe a opção proxy e deixe que ele faça o túnel por você. Reutilize um único cliente ou handle para que as conexões permaneçam ativas, execute requisições simultaneamente com um pool limitado em vez de uma por vez, limite a concorrência por host de destino, e verifique o erro de transporte, não apenas o código de status. Varie o nome de usuário do proxy para mudar a geolocalização ou a sessão.

Acerte isso e ambos os caminhos terão o desempenho que o PHP deveria ter em escala. Aponte-os para o gateway residencial, e lembre-se de que a qualidade do pool decide com que frequência você precisa tentar novamente (reputação de IP). A página de preços tem os planos por GB para testar contra seus próprios destinos.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $0,75/GB.

Começar