Why PHP Developers Need HTTP Proxies
If you're building scrapers, integrating third-party APIs with strict rate limits, or accessing geo-restricted content, you've likely hit IP blocks. HTTP proxies solve this by routing your requests through different IP addresses—making your traffic appear to originate from various locations rather than a single source.
PHP developers have several powerful tools for proxy-aware HTTP requests: native cURL functions, Guzzle HTTP client, Symfony HTTP Client, and Laravel's wrapper ecosystem. Each has distinct advantages depending on your use case.
This guide shows you exactly how to configure PHP proxy connections across all major HTTP clients, with production-ready code you can copy and run.
Raw cURL: The Foundation of PHP Proxy Requests
PHP's native cURL extension gives you fine-grained control over proxy configuration. Every modern PHP HTTP client ultimately wraps cURL, so understanding the raw approach helps you debug and optimize.
Basic cURL Proxy Configuration
The key cURL options for proxy support are:
CURLOPT_PROXY— the proxy hostname or IPCURLOPT_PROXYPORT— the proxy portCURLOPT_PROXYUSERPWD— username:password for authenticationCURLOPT_HTTPPROXYTUNNEL— enable tunneling (recommended for HTTPS targets)
Here's a complete, runnable example using ProxyHat residential proxies:
<?php
function fetchWithProxy(string $url, string $username, string $password): string
{
$ch = curl_init();
// Basic cURL options
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// Proxy configuration
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => "{$username}:{$password}",
CURLOPT_HTTPPROXYTUNNEL => true, // Required for HTTPS targets
// SSL/TLS configuration
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt', // Adjust for your system
// User agent (avoid detection)
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new RuntimeException("cURL error: {$error}");
}
if ($httpCode >= 400) {
throw new RuntimeException("HTTP error: {$httpCode}");
}
return $response;
}
// Usage example
try {
$html = fetchWithProxy(
'https://httpbin.org/ip',
'user-country-US', // ProxyHat geo-targeting
'your-password'
);
echo $html;
} catch (RuntimeException $e) {
echo "Request failed: " . $e->getMessage();
}Geo-Targeting with ProxyHat
ProxyHat supports country and city-level targeting via the username field. Simply format your username as user-country-{CC} or user-country-{CC}-city-{city}:
<?php
// Target US IPs
$username = 'user-country-US';
// Target Berlin, Germany
$username = 'user-country-DE-city-berlin';
// Sticky session (same IP for multiple requests)
$username = 'user-session-abc123-country-US';
// Use in cURL
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "{$username}:{$password}");Guzzle HTTP Client: Modern PHP Proxy Integration
Guzzle is the most popular PHP HTTP client, offering a clean object-oriented interface. It's widely used in Laravel applications and standalone PHP projects.
Basic Guzzle Proxy Configuration
Pass proxy settings via the proxy request option:
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
function createGuzzleClient(string $username, string $password): Client
{
return new Client([
'timeout' => 30,
'connect_timeout' => 10,
'proxy' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
'verify' => true, // Enable SSL verification
]);
}
// Usage
$client = createGuzzleClient('user-country-US', 'your-password');
try {
$response = $client->get('https://httpbin.org/ip');
echo $response->getBody()->getContents();
} catch (RequestException $e) {
echo "Request failed: " . $e->getMessage();
}Per-Request Proxy Rotation with Guzzle
For Guzzle proxy rotation, override the proxy setting on individual requests. This is essential for scraping where you need a different IP for each request:
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Promise;
class ProxyRotator
{
private array $proxyCredentials = [];
private int $currentIndex = 0;
public function __construct(array $credentials)
{
$this->proxyCredentials = $credentials;
}
public function getNextProxyUrl(): string
{
$cred = $this->proxyCredentials[$this->currentIndex];
$this->currentIndex = ($this->currentIndex + 1) % count($this->proxyCredentials);
return "http://{$cred['username']}:{$cred['password']}@gate.proxyhat.com:8080";
}
public function getRotatingUsername(string $country = 'US'): string
{
// ProxyHat supports session-based rotation
$sessionId = bin2hex(random_bytes(8));
return "user-session-{$sessionId}-country-{$country}";
}
}
// Production-ready scraping function
function scrapeMultipleUrls(array $urls, string $password): array
{
$rotator = new ProxyRotator([]); // We'll use session-based rotation
$client = new Client(['timeout' => 30, 'verify' => true]);
$promises = [];
foreach ($urls as $key => $url) {
$username = $rotator->getRotatingUsername('US');
$proxyUrl = "http://{$username}:{$password}@gate.proxyhat.com:8080";
$promises[$key] = $client->getAsync($url, [
'proxy' => $proxyUrl,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept' => 'text/html,application/xhtml+xml',
],
]);
}
// Wait for all requests to complete
$results = Promise\Utils::settle($promises)->wait();
$responses = [];
foreach ($results as $key => $result) {
if ($result['state'] === 'fulfilled') {
$responses[$key] = $result['value']->getBody()->getContents();
} else {
$responses[$key] = ['error' => $result['reason']->getMessage()];
}
}
return $responses;
}
// Usage
$urls = [
'page1' => 'https://example.com/page1',
'page2' => 'https://example.com/page2',
'page3' => 'https://example.com/page3',
];
$results = scrapeMultipleUrls($urls, 'your-password');
print_r($results);Symfony HTTP Client: Async and Modern
Symfony's HTTP Client offers excellent async support and integrates well with Laravel applications. It's particularly useful for high-throughput scraping scenarios.
<?php
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
class SymfonyProxyClient
{
private string $password;
private $client;
public function __construct(string $password)
{
$this->password = $password;
$this->client = HttpClient::create([
'timeout' => 30,
'max_redirects' => 5,
'verify_peer' => true,
'verify_host' => true,
]);
}
public function fetch(string $url, ?string $country = null): string
{
$username = $this->buildUsername($country);
$response = $this->client->request('GET', $url, [
'proxy' => "http://{$username}:{$this->password}@gate.proxyhat.com:8080",
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
//getStatusCode() will throw on network errors
$statusCode = $response->getStatusCode();
if ($statusCode >= 400) {
throw new RuntimeException("HTTP error: {$statusCode}");
}
return $response->getContent();
}
public function fetchConcurrent(array $urls, ?string $country = null): array
{
$responses = [];
foreach ($urls as $key => $url) {
$username = $this->buildUsername($country);
$responses[$key] = $this->client->request('GET', $url, [
'proxy' => "http://{$username}:{$this->password}@gate.proxyhat.com:8080",
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
],
]);
}
// Process responses as they complete
$results = [];
foreach ($responses as $key => $response) {
try {
$results[$key] = $response->getContent();
} catch (\Exception $e) {
$results[$key] = ['error' => $e->getMessage()];
}
}
return $results;
}
private function buildUsername(?string $country): string
{
$sessionId = bin2hex(random_bytes(8));
$country = $country ?? 'US';
return "user-session-{$sessionId}-country-{$country}";
}
}
// Usage
$client = new SymfonyProxyClient('your-password');
// Single request
$html = $client->fetch('https://httpbin.org/ip', 'US');
echo $html;
// Concurrent requests
$urls = [
'ip1' => 'https://httpbin.org/ip',
'ip2' => 'https://httpbin.org/user-agent',
];
$results = $client->fetchConcurrent($urls, 'DE');
print_r($results);Laravel Integration: Production-Ready Proxy Service
For Laravel proxy scraping applications, you'll want a reusable service class that handles proxy rotation, retries, and logging. Here's a complete implementation:
Proxy Service Class
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
class ProxyService
{
private Client $client;
private string $proxyPassword;
private int $maxRetries;
private int $retryDelay;
public function __construct(
?string $password = null,
int $maxRetries = 3,
int $retryDelay = 1000
) {
$this->proxyPassword = $password ?? config('services.proxyhat.password');
$this->maxRetries = $maxRetries;
$this->retryDelay = $retryDelay;
$this->client = new Client([
'timeout' => config('services.proxyhat.timeout', 30),
'connect_timeout' => config('services.proxyhat.connect_timeout', 10),
'verify' => true,
]);
}
/**
* Fetch URL with automatic proxy rotation and retries.
*/
public function fetch(
string $url,
?string $country = null,
?string $sessionId = null,
array $headers = []
): string {\n $attempt = 0;
$lastException = null;
while ($attempt < $this->maxRetries) {
$attempt++;
try {
$username = $this->buildUsername($country, $sessionId);
$proxyUrl = $this->buildProxyUrl($username);
$response = $this->client->get($url, [
'proxy' => $proxyUrl,
'headers' => array_merge($this->getDefaultHeaders(), $headers),
]);
Log::info('Proxy request successful', [
'url' => $url,
'country' => $country,
'attempt' => $attempt,
]);
return $response->getBody()->getContents();
} catch (RequestException $e) {
$lastException = $e;
Log::warning('Proxy request failed', [
'url' => $url,
'attempt' => $attempt,
'error' => $e->getMessage(),
'status_code' => $e->getResponse()?->getStatusCode(),
]);
// Force new session on retry (new IP)
$sessionId = null;
if ($attempt < $this->maxRetries) {
usleep($this->retryDelay * 1000 * $attempt); // Exponential backoff
}
}
}
throw $lastException ?? new \RuntimeException('Unknown error');
}
/**
* Fetch multiple URLs concurrently.
*/
public function fetchBatch(array $urls, ?string $country = null): array
{
$promises = [];
foreach ($urls as $key => $url) {
$username = $this->buildUsername($country);
$proxyUrl = $this->buildProxyUrl($username);
$promises[$key] = $this->client->getAsync($url, [
'proxy' => $proxyUrl,
'headers' => $this->getDefaultHeaders(),
]);
}
$results = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
$responses = [];
foreach ($results as $key => $result) {
if ($result['state'] === 'fulfilled') {
$responses[$key] = $result['value']->getBody()->getContents();
} else {
$responses[$key] = ['error' => $result['reason']->getMessage()];
}
}
return $responses;
}
/**
* Create a sticky session (same IP for multiple requests).
*/
public function createStickySession(string $country = 'US'): string
{
return bin2hex(random_bytes(16));
}
private function buildUsername(?string $country, ?string $sessionId = null): string
{
$country = $country ?? config('services.proxyhat.default_country', 'US');
$sessionId = $sessionId ?? bin2hex(random_bytes(8));
return "user-session-{$sessionId}-country-{$country}";
}
private function buildProxyUrl(string $username): string
{
return "http://{$username}:{$this->proxyPassword}@gate.proxyhat.com:8080";
}
private function getDefaultHeaders(): array
{
$userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
];
return [
'User-Agent' => $userAgents[array_rand($userAgents)],
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
'Accept-Encoding' => 'gzip, deflate',
];
}
}Laravel Configuration
Add these settings to config/services.php:
<?php
// config/services.php
return [
'proxyhat' => [
'password' => env('PROXYHAT_PASSWORD'),
'timeout' => env('PROXYHAT_TIMEOUT', 30),
'connect_timeout' => env('PROXYHAT_CONNECT_TIMEOUT', 10),
'default_country' => env('PROXYHAT_DEFAULT_COUNTRY', 'US'),
],
];Usage in Laravel Jobs
<?php
namespace App\Jobs;
use App\Services\ProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ScrapeProductJob implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public function __construct(
private string $url,
private string $country = 'US'
) {}
public function handle(ProxyService $proxyService): void
{
// Create sticky session for this job (same IP throughout)
$sessionId = $proxyService->createStickySession($this->country);
try {
$html = $proxyService->fetch(
$this->url,
$this->country,
$sessionId
);
// Parse and store data
$data = $this->parseHtml($html);
// If you need to make follow-up requests,
// pass the same $sessionId to maintain the same IP
} catch (\Exception $e) {
$this->fail($e);
}
}
private function parseHtml(string $html): array
{
// Your parsing logic here
return [];
}
}
// Dispatch
ScrapeProductJob::dispatch('https://example.com/product/123', 'DE');Multi-cURL for High-Concurrency Scraping
For maximum performance, PHP's curl_multi_* functions let you execute hundreds of concurrent requests. This approach bypasses HTTP client overhead and gives you raw control:
<?php
class MultiCurlProxyScraper
{
private string $proxyPassword;
private int $concurrency;
public function __construct(string $password, int $concurrency = 50)
{
$this->proxyPassword = $password;
$this->concurrency = $concurrency;
}
public function fetchAll(array $urls, ?string $country = null): array
{
$mh = curl_multi_init();
$handles = [];
$results = [];
// Initialize all cURL handles
foreach ($urls as $key => $url) {
$sessionId = bin2hex(random_bytes(8));
$username = "user-session-{$sessionId}-country-{$country ?? 'US'}";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => "{$username}:{$this->proxyPassword}",
CURLOPT_HTTPPROXYTUNNEL => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
curl_multi_add_handle($mh, $ch);
$handles[$key] = $ch;
}
// Execute all requests
$active = null;
do {
$status = curl_multi_exec($mh, $active);
if ($active) {
curl_multi_select($mh); // Wait for activity
}
} while ($active && $status === CURLM_OK);
// Collect results
foreach ($handles as $key => $ch) {
$response = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
if ($error) {
$results[$key] = ['error' => $error];
} elseif ($httpCode >= 400) {
$results[$key] = ['error' => "HTTP {$httpCode}"];
} else {
$results[$key] = $response;
}
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
}
// Usage - fetch 100 URLs concurrently
$scraper = new MultiCurlProxyScraper('your-password', concurrency: 50);
$urls = [];
for ($i = 0; $i < 100; $i++) {
$urls["page{$i}"] = "https://httpbin.org/delay/1?n={$i}";
}
$start = microtime(true);
$results = $scraper->fetchAll($urls, 'US');
$elapsed = microtime(true) - $start;
echo "Fetched " . count($urls) . " URLs in " . round($elapsed, 2) . " seconds\n";
echo "Success rate: " . count(array_filter($results, 'is_string')) . "/" . count($urls) . "\n";TLS/SSL Configuration and CA Bundle Handling
When proxying HTTPS traffic, proper certificate verification is critical. Here's how to handle TLS/SSL correctly:
Common SSL Issues
- Missing CA bundle — PHP can't verify SSL certificates
- Outdated CA bundle — Newer certificates aren't recognized
- Self-signed certificates — Internal APIs may use custom CAs
- Proxy MITM — Some corporate proxies intercept SSL
Production SSL Configuration
<?php
class SecureProxyClient
{
private string $caBundlePath;
private string $proxyPassword;
public function __construct(string $password)
{
$this->proxyPassword = $password;
$this->caBundlePath = $this->findCaBundle();
}
/**
* Locate CA bundle on various systems.
*/
private function findCaBundle(): string
{
// Common CA bundle locations
$candidates = [
// Linux (Debian/Ubuntu)
'/etc/ssl/certs/ca-certificates.crt',
// Linux (RHEL/CentOS)
'/etc/pki/tls/certs/ca-bundle.crt',
// Linux (Alpine)
'/etc/ssl/certs/ca-certificates.crt',
// macOS (Homebrew OpenSSL)
'/usr/local/etc/openssl/cert.pem',
// macOS (system)
'/etc/ssl/cert.pem',
// Windows (XAMPP)
'C:\xampp\php\extras\ssl\cacert.pem',
// Composer CA bundle (if installed)
dirname(__DIR__) . '/vendor/cacert/cacert.pem',
];
foreach ($candidates as $path) {
if (file_exists($path)) {
return $path;
}
}
// Fallback: download Mozilla CA bundle
$bundlePath = sys_get_temp_dir() . '/cacert.pem';
if (!file_exists($bundlePath)) {
$bundle = file_get_contents('https://curl.se/ca/cacert.pem');
file_put_contents($bundlePath, $bundle);
}
return $bundlePath;
}
/**
* Create a cURL handle with proper SSL configuration.
*/
public function createSecureCurlHandle(string $url, string $username): \CurlHandle
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
// Proxy
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => "{$username}:{$this->proxyPassword}",
CURLOPT_HTTPPROXYTUNNEL => true,
// SSL/TLS - Always verify in production
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2, // Strict hostname verification
CURLOPT_CAINFO => $this->caBundlePath,
// TLS version (minimum TLS 1.2)
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
// Cipher configuration
CURLOPT_SSL_CIPHER_LIST => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256',
// Enable certificate status checking (OCSP stapling)
CURLOPT_CERTINFO => true,
]);
return $ch;
}
/**
* Guzzle client with SSL hardening.
*/
public function createSecureGuzzleClient(): \GuzzleHttp\Client
{
return new \GuzzleHttp\Client([
'timeout' => 30,
'connect_timeout' => 10,
'proxy' => "http://user-country-US:{$this->proxyPassword}@gate.proxyhat.com:8080",
'verify' => $this->caBundlePath,
'version' => 2.0, // HTTP/2 if available
'config' => [
'curl' => [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_CERTINFO => true,
],
],
]);
}
/**
* Debug SSL certificate chain.
*/
public function inspectCertificate(string $url): array
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CERTINFO => true,
CURLOPT_VERBOSE => true,
CURLOPT_CAINFO => $this->caBundlePath,
]);
curl_exec($ch);
$certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);
$sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);
curl_close($ch);
return [
'cert_info' => $certInfo,
'ssl_verify_result' => $sslVerifyResult,
];
}
}
// Usage
$client = new SecureProxyClient('your-password');
// Inspect SSL certificate
$certInfo = $client->inspectCertificate('https://example.com');
print_r($certInfo);Handling SSL Errors
<?php
try {
$response = $client->get('https://example.com');
} catch (GuzzleHttp\Exception\RequestException $e) {
$curlError = curl_errno($e->getHandlerContext());
// Common SSL error codes
$sslErrors = [
CURLE_SSL_CACERT => 'CA certificate not found',
CURLE_SSL_CERTPROBLEM => 'Certificate problem',
CURLE_SSL_CIPHER => 'Cipher negotiation failed',
CURLE_SSL_CACERT_SHA256 => 'SHA-256 certificate issue',
CURLE_SSL_PINNEDPUBKEYNOTMATCH => 'Public key mismatch',
60 => 'Peer certificate cannot be authenticated',
51 => 'SSL certificate verification failed',
];
if (isset($sslErrors[$curlError])) {
// Handle SSL-specific errors
Log::error('SSL verification failed', [
'error_code' => $curlError,
'error_message' => $sslErrors[$curlError],
'url' => $url,
]);
// Options: update CA bundle, check for MITM, or investigate cert chain
}
}Comparison: PHP HTTP Clients with Proxy Support
| Feature | Raw cURL | Guzzle | Symfony HTTP | Laravel HTTP |
|---|---|---|---|---|
| Proxy Configuration | Manual options | Request options | Request options | Middleware |
| Async Support | curl_multi | Promises | Native async | Async pool |
| Rotation | Manual | Per-request | Per-request | Service class |
| Retry Logic | Manual | Middleware | Retryable | Job retries |
| SSL Handling | Full control | Config option | Config option | Inherited |
| Best For | Maximum control | General use | High concurrency | Laravel apps |
Key Takeaways
- Raw cURL gives you maximum control over proxy configuration—use it when you need fine-tuned SSL settings or are building custom scraping pipelines.
- Guzzle is the best all-around choice for most PHP projects—its promise-based async support and middleware system handle proxy rotation elegantly.
- Symfony HTTP Client excels at high-concurrency workloads with native async support—ideal for scraping thousands of pages.
- Laravel integration should wrap proxy logic in a reusable service class that handles rotation, retries, and logging—inject it into jobs for queue-based scraping.
- SSL verification must never be disabled in production—locate and configure a proper CA bundle, and handle certificate errors gracefully.
- ProxyHat geo-targeting lets you specify country and city in the username string (
user-country-US-city-newyork), and sticky sessions maintain the same IP across multiple requests.
Ready to start scraping? Get residential proxies from ProxyHat with instant access to millions of IPs across 195+ countries.






