PHP cURL Proxy: Setup, Authentication, SOCKS5 and HTTPS

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_PROXYto a full URL (http://,https://,socks5://orsocks5h://) and addCURLOPT_PROXYUSERPWDfor 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
- Choosing the proxy type
- Proxy authentication in PHP cURL
- Using a SOCKS5 proxy with PHP cURL
- HTTPS: two different things
- A reusable function with error handling
- Confirm that the proxy is really used
- Troubleshooting common errors
- Rotating proxies and scraping loops
- Why the proxy type matters for what you collect
- Try it with real carrier IPs
- FAQ
- Sources
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:
CURLOPT_RETURNTRANSFERreturns the body as a string instead of printing it.- 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. - A strict
=== falsecheck. 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 string | Type | Who resolves the target hostname |
|---|---|---|
http://host:port | HTTP proxy | The proxy |
https://host:port | HTTPS proxy (TLS to the proxy) | The proxy |
socks5://host:port | SOCKS5 | Your PHP server |
socks5h://host:port | SOCKS5 with hostname resolution | The 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.

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.
- 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. - An HTTPS proxy. The connection from PHP to the proxy itself is encrypted. Here the proxy URL starts with
https://, or you setCURLPROXY_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_PROXYandCURLOPT_NOPROXYoverride 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
| Symptom | Likely cause | What to try |
|---|---|---|
CURLE_COULDNT_RESOLVE_PROXY | Wrong proxy hostname or DNS failure on your server | Check the host spelling, then resolve it from the same machine |
| Connection timed out | Wrong port, firewall rule, or dead proxy | Test the port from the server; lower CURLOPT_CONNECTTIMEOUT to fail fast |
| HTTP 407 | Missing or wrong proxy credentials | Confirm you used CURLOPT_PROXYUSERPWD, not CURLOPT_USERPWD |
| TLS or certificate error | HTTPS proxy certificate or target certificate | Identify which hop failed; use the proxy TLS options for the proxy hop |
| Works in CLI, fails on web server | Different PHP or cURL build, different environment | Print curl_version() in both runtimes |
| Same IP every time | Proxy bypassed, or a sticky session is in effect | Check 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_PROXYline 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.

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
- CURLOPT_PROXY, libcurl
- CURLOPT_PROXYTYPE, libcurl
- Proxies, everything curl
- CURLOPT_SOCKS5_AUTH, libcurl
- curl tutorial
- PHP manual: predefined cURL constants
- PHP manual: curl_errno
- PHP manual: curl_getinfo
- PHP manual: curl_setopt
- CURLINFO_PROXY_ERROR, libcurl
- How to use a proxy with PHP cURL