Try 500 MB of US mobile proxy data free for 30 days.Start free trial
November 1, 202613 min read

PHP cURL Proxy: Setup, Authentication, SOCKS5 and HTTPS

Layered geometric planes representing proxy configuration

To send a PHP cURL request through a proxy, set CURLOPT_PROXY on the handle before you call curl_exec. Use a scheme URL such as http://host:port, add credentials with CURLOPT_PROXYUSERPWD if the proxy requires them, and check curl_errno and the HTTP status afterward. This guide gives you working code for each case: plain HTTP proxies, authenticated proxies, SOCKS5, HTTPS proxies, and the debugging steps for when a request fails.

TL;DR

  • Set CURLOPT_PROXY to a full URL (http://, https://, socks5:// or socks5h://) and add CURLOPT_PROXYUSERPWD for credentials.
  • Use socks5h:// when you want the proxy, not your server, to resolve the destination hostname.
  • Keep credentials in environment variables, never in source control, and always set a timeout.
  • Treat transport errors (curl_errno) and HTTP status codes (CURLINFO_HTTP_CODE) as two separate checks.
  • Verify the exit IP with a test request before you run a real job.

Table of Contents

The minimal PHP cURL proxy request

The smallest working example needs two options: the target URL and the proxy address. The libcurl documentation for CURLOPT_PROXY describes the value as the proxy host, port and optionally a scheme. Everything else is optional.

<?php
$ch = curl_init('https://httpbin.org/ip');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_PROXY          => 'http://proxy.example.com:8080',
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);

$body = curl_exec($ch);

if ($body === false) {
    echo 'cURL error ' . curl_errno($ch) . ': ' . curl_error($ch);
} else {
    echo $body;
}

curl_close($ch);

Three habits in this snippet are worth keeping in every script:

  1. CURLOPT_RETURNTRANSFER returns the body as a string instead of printing it.
  2. Two timeouts. A connect timeout catches a dead proxy quickly, and a total timeout stops a hung transfer from blocking your worker. A practical PHP proxy guide also recommends setting CURLOPT_TIMEOUT.
  3. A strict === false check. An empty response body is not the same as a failed transfer.

The proxy host is the part you replace. If you use a provider, its dashboard gives you the host, port and credentials. If you run your own proxy, use that server's address.

Choosing the proxy type

If your proxy string has a scheme, cURL reads the type from it. If it has none, you tell cURL with CURLOPT_PROXYTYPE. The CURLOPT_PROXY page lists the supported types: HTTP, HTTPS, HTTP/1.0, SOCKS4, SOCKS4a, SOCKS5 and SOCKS5 with hostname resolution. The matching constants are documented in the CURLOPT_PROXYTYPE reference: CURLPROXY_HTTP, CURLPROXY_HTTPS, CURLPROXY_HTTP_1_0, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A, CURLPROXY_SOCKS5 and CURLPROXY_SOCKS5_HOSTNAME.

Proxy stringTypeWho resolves the target hostname
http://host:portHTTP proxyThe proxy
https://host:portHTTPS proxy (TLS to the proxy)The proxy
socks5://host:portSOCKS5Your PHP server
socks5h://host:portSOCKS5 with hostname resolutionThe proxy

The resolution column comes from the everything curl proxy chapter, which explains that socks5:// resolves names locally while socks5h:// hands resolution to the proxy.

If your string is just host:port, set the type explicitly:

curl_setopt($ch, CURLOPT_PROXY, 'proxy.example.com:1080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);

Putting the scheme in the URL is usually clearer, because the type travels with the address and a reader of your config can see it at a glance.

Proxy authentication in PHP cURL

Most paid proxies require a username and password. There are two documented ways to pass them. The PHP constants manual lists CURLOPT_PROXYUSERPWD for a combined user:password string, and separate CURLOPT_PROXYUSERNAME and CURLOPT_PROXYPASSWORD options. The libcurl docs note that credentials can sit inside the proxy URL, but the separate options are the documented alternative, and they save you from URL-encoding special characters in a password.

$user = getenv('PROXY_USER');
$pass = getenv('PROXY_PASS');

curl_setopt_array($ch, [
    CURLOPT_PROXY         => 'http://proxy.example.com:8080',
    CURLOPT_PROXYUSERNAME => $user,
    CURLOPT_PROXYPASSWORD => $pass,
    CURLOPT_PROXYAUTH     => CURLAUTH_BASIC,
]);

CURLOPT_PROXYAUTH is optional. It constrains which authentication method cURL may use with the proxy. If you leave it out, cURL negotiates.

Keep credentials out of your code

Read them from environment variables or a secret manager. The same practical guide recommends keeping proxy credentials outside source code, and it is the habit that prevents the most embarrassing leaks: a password in a Git history stays there long after you delete the line.

Proxy credentials are not site credentials

Proxy authentication and authentication to the destination server are separate. The official curl tutorial draws that line and shows separate proxy credentials for HTTPS requests. In PHP, CURLOPT_USERPWD is for the target site and CURLOPT_PROXYUSERPWD is for the proxy. Mixing them up is a common reason for a request that keeps failing with a 407 status even though the password looks right. For the full story on that status code, see our guide to fixing 407 errors with HTTP proxy authentication.

Using a SOCKS5 proxy with PHP cURL

SOCKS5 works at a lower level than an HTTP proxy. It relays TCP connections without interpreting HTTP, which is why it suits non-HTTP traffic and why DNS behavior matters.

$ch = curl_init('https://httpbin.org/ip');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_PROXY          => 'socks5h://proxy.example.com:1080',
    CURLOPT_PROXYUSERPWD   => getenv('PROXY_USER') . ':' . getenv('PROXY_PASS'),
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);

$body = curl_exec($ch);

socks5 or socks5h

This one letter changes where DNS lookups happen. With socks5://, your PHP server resolves the hostname and sends the IP to the proxy. That exposes the lookup to your server's resolver and can return a different address than the one a user in the proxy's location would get. With socks5h://, the proxy resolves the name. For geo-targeted work, socks5h:// is usually the better default, because the lookup then happens from the same network position as the request. We compare the trade-offs in 5 tests to pick SOCKS5 or HTTP proxy.

Comparison of local and proxy DNS resolution

SOCKS5 authentication

For SOCKS5, pass credentials with CURLOPT_PROXYUSERPWD. The CURLOPT_SOCKS5_AUTH documentation says the supported methods are CURLAUTH_BASIC and CURLAUTH_GSSAPI, and that CURLAUTH_ONLY can require authentication. The same page notes that this setting controls how access to the proxy is negotiated, not the security of the request that follows. In practice, username and password via CURLOPT_PROXYUSERPWD covers nearly every commercial SOCKS5 service.

Chaining through a SOCKS pre-proxy

Some setups need to reach an HTTP(S) proxy by way of a SOCKS server first. The PHP manual lists CURLOPT_PRE_PROXY as available from PHP 7.3.0 and cURL 7.52.0 for exactly that. Most scraping jobs never need it.

HTTPS: two different things

"HTTPS proxy" gets used for two situations, and mixing them up wastes hours.

  1. An HTTPS website through a normal proxy. Your proxy URL is http://..., and cURL asks the proxy to open a tunnel to the HTTPS site with a CONNECT request. The TLS session runs end to end between PHP and the site. The proxy sees the destination host and port, not the page content. Our explainer on how the HTTPS proxy CONNECT tunnel works walks through it.
  2. An HTTPS proxy. The connection from PHP to the proxy itself is encrypted. Here the proxy URL starts with https://, or you set CURLPROXY_HTTPS.

The CURLOPT_PROXY page identifies https:// as the HTTPS proxy scheme. The CURLOPT_PROXYTYPE page adds that CURLPROXY_HTTPS uses HTTP/1, CURLPROXY_HTTPS2 attempts HTTP/2, and CURLPROXY_HTTPS3 attempts experimental HTTP/3 and needs a build with HTTP/3 proxy support.

// HTTPS website through a plain HTTP proxy (most common)
curl_setopt($ch, CURLOPT_PROXY, 'http://proxy.example.com:8080');

// TLS to the proxy itself
curl_setopt($ch, CURLOPT_PROXY, 'https://proxy.example.com:8443');

The two cases also have separate certificate checks. The PHP manual documents CURLOPT_PROXY_CAINFO and related proxy TLS options as applying to verification of an HTTPS proxy, which is distinct from verification of the target server's certificate. If you see a certificate error, work out first which of the two connections is failing. Do not switch off CURLOPT_SSL_VERIFYPEER to make the error go away. If you want the decision framework, read HTTP vs HTTPS proxy: 5 decision rules.

A reusable function with error handling

Here is a helper that you can drop into a project. It separates transport failures from HTTP failures, which matters because a proxy can deliver a perfectly valid 403 or 429 from the target site.

function fetchViaProxy(string $url, string $proxy, ?string $user = null, ?string $pass = null): array
{
    $ch = curl_init($url);

    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_PROXY          => $proxy,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; MyCollector/1.0)',
    ];

    if ($user !== null) {
        $opts[CURLOPT_PROXYUSERNAME] = $user;
        $opts[CURLOPT_PROXYPASSWORD] = $pass;
    }

    curl_setopt_array($ch, $opts);

    $body   = curl_exec($ch);
    $errno  = curl_errno($ch);
    $error  = curl_error($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    return [
        'ok'     => $errno === 0 && $status >= 200 && $status < 300,
        'status' => $status,
        'errno'  => $errno,
        'error'  => $error,
        'body'   => $body === false ? null : $body,
    ];
}

The curl_errno manual says the function returns the numeric error of the last cURL operation, including errors such as CURLE_COULDNT_RESOLVE_PROXY. The curl_getinfo manual documents CURLINFO_HTTP_CODE for the response status. The practical guide cited earlier makes the same point: check transport failures separately from the HTTP status.

A note on PHP versions. As the curl_setopt manual states, PHP 8.0 made curl_setopt throw a ValueError for an invalid option and changed handles from resources to CurlHandle objects. If you moved from PHP 7 and your code type-checks handles, update it.

Confirm that the proxy is really used

Do not assume. Run a request to an IP echo endpoint, once without the proxy and once with it, and compare.

$direct = file_get_contents('https://httpbin.org/ip');
$proxied = fetchViaProxy('https://httpbin.org/ip', 'http://proxy.example.com:8080', getenv('PROXY_USER'), getenv('PROXY_PASS'));

echo $direct . PHP_EOL;
echo $proxied['body'] . PHP_EOL;

If the two addresses match, the proxy is being bypassed. Check two things:

  • Environment variables. The libcurl documentation says CURLOPT_PROXY and CURLOPT_NOPROXY override proxy environment variables, and an empty proxy string explicitly disables proxy use. A variable that is empty or misspelled in your code can silently turn the proxy off.
  • The runtime. Test in the same PHP runtime that runs the application. The practical guide stresses reproducible verification in a production-like environment, since the CLI and your web server can load different PHP builds and cURL versions.

For geo-targeted work, go one step further and check that the exit IP geolocates to the city you expect. Our list of proxy checker tools for rotation and speed covers ways to do that.

Troubleshooting common errors

SymptomLikely causeWhat to try
CURLE_COULDNT_RESOLVE_PROXYWrong proxy hostname or DNS failure on your serverCheck the host spelling, then resolve it from the same machine
Connection timed outWrong port, firewall rule, or dead proxyTest the port from the server; lower CURLOPT_CONNECTTIMEOUT to fail fast
HTTP 407Missing or wrong proxy credentialsConfirm you used CURLOPT_PROXYUSERPWD, not CURLOPT_USERPWD
TLS or certificate errorHTTPS proxy certificate or target certificateIdentify which hop failed; use the proxy TLS options for the proxy hop
Works in CLI, fails on web serverDifferent PHP or cURL build, different environmentPrint curl_version() in both runtimes
Same IP every timeProxy bypassed, or a sticky session is in effectCheck proxy variables; check your session settings

For deeper detail on proxy failures, PHP 8.2.0 added CURLINFO_PROXY_ERROR to curl_getinfo, as the curl_getinfo manual notes. The official CURLINFO_PROXY_ERROR page explains that the detailed proxy code is useful when the transfer returns CURLE_PROXY.

if ($errno === CURLE_PROXY && defined('CURLINFO_PROXY_ERROR')) {
    $detail = curl_getinfo($ch, CURLINFO_PROXY_ERROR);
    echo "Proxy error detail: $detail";
}

Run that before curl_close. When you want to isolate PHP from the problem entirely, reproduce the request on the command line with the same proxy. Our page of copy-paste curl proxy commands is built for this. If the command-line request works and PHP does not, the bug is in your options. If both fail, it is the proxy, the network or the credentials.

Rotating proxies and scraping loops

A single proxy address is fine for testing. For real data collection, you usually want either rotation or session pinning, and the choice depends on the job.

  • Rotating IPs suit large, independent requests such as price checks across many product pages, where each request can come from a different address.
  • Sticky sessions suit multi-step flows such as logging in, paginating through results or filling a cart, where changing IP mid-flow would break the session.

With a provider that handles rotation behind one endpoint, your PHP code stays simple: the same CURLOPT_PROXY value on every request, and the gateway changes the exit IP. If you manage a list of proxies yourself, pick one per request and retry on a different one when you see a transport error:

$proxies = [
    'http://proxy-a.example.com:8080',
    'http://proxy-b.example.com:8080',
];

foreach ($urls as $url) {
    for ($attempt = 0; $attempt < 3; $attempt++) {
        $result = fetchViaProxy($url, $proxies[array_rand($proxies)], getenv('PROXY_USER'), getenv('PROXY_PASS'));
        if ($result['ok']) {
            break;
        }
        usleep(500000 * ($attempt + 1));
    }
}

The short back-off protects both the proxy and the target site. For a feature checklist when choosing a provider, see 8 proxy API features data teams should compare first.

Why the proxy type matters for what you collect

PHP cURL treats every proxy the same way. The target site does not. Many sites score requests by the network they come from, and datacenter ranges tend to look different from the addresses real phones use. If your scraper collects prices, search results or ads that vary by location, the exit IP decides what you see, so the proxy you plug into CURLOPT_PROXY is part of your data quality, not just your plumbing.

Our perspective

We build MaskLabs around one idea: if your code asks a site what a user in a given city sees, the request should come from a network a real user would use. That is why we use real US carrier IPs rather than datacenter proxies, with targeting across 46 US cities.

From the PHP side, we want the integration to be boring. One proxy URL, credentials from the environment, the same CURLOPT_PROXY line whether you pick a rotating or a sticky session. We support HTTP, HTTPS and SOCKS5, so the examples in this guide work without changing your approach. Billing is metered per byte, so you pay for the data your scraper moves, and teams can manage credentials themselves from the dashboard without a sales call.

Our advice for any cURL proxy setup is the one we give our own users: test the exit IP first, set timeouts, separate transport errors from HTTP status, and match the session type to the job. Do those four things and most proxy problems become a five-minute fix.

Jon

Try it with real carrier IPs

If you are building a PHP collector that needs location-specific results, MaskLabs gives you real US mobile carrier IPs, city-level targeting, rotating and sticky sessions, and API access. Plans start with Starter at $30 per month, with Basic at $100 per month, Advanced at $500 per month and Scale at $1000 per month. Prepaid data add-ons are billed per GB, and there is a free trial with limited data, so you can run the code from this guide against a live endpoint. See the plans and pricing or read about our US mobile proxies on real carrier IPs.

Masklabs

FAQ

How do I use a proxy with PHP cURL?

Set CURLOPT_PROXY to the proxy URL, for example http://host:port, before calling curl_exec. Add CURLOPT_PROXYUSERPWD if the proxy needs a username and password. Afterward, check curl_errno for transport errors and CURLINFO_HTTP_CODE for the response status.

How do I make PHP cURL use a SOCKS5 proxy?

Use socks5h://host:port as the CURLOPT_PROXY value, or set CURLOPT_PROXYTYPE to CURLPROXY_SOCKS5_HOSTNAME if your string has no scheme. The h variant makes the proxy resolve the destination hostname. Pass credentials with CURLOPT_PROXYUSERPWD.

Why do I get a 407 error with my PHP cURL proxy?

A 407 means the proxy rejected your credentials or did not receive any. Check that you set CURLOPT_PROXYUSERPWD (or the separate username and password options), not the options meant for the target site. Also confirm the values reach PHP, since an unset environment variable produces an empty password.

Does PHP cURL work with an HTTPS proxy?

Yes. Use an https:// proxy URL or CURLPROXY_HTTPS when the connection to the proxy itself is encrypted. Requesting an HTTPS website through a normal http:// proxy is a different case and works through a CONNECT tunnel. The proxy hop and the target hop are verified separately.

How do I know the proxy is being used?

Request an IP echo endpoint with and without the proxy and compare the addresses. If they match, check for an empty proxy string or proxy settings overridden elsewhere. Run the test in the same PHP runtime your application uses.

Sources

Recommended