A scraping job that worked perfectly in staging starts returning exit code 35 at 03:00 on a Tuesday. The on-call engineer re-runs the same command from a laptop and it succeeds. The pipeline keeps failing. Nothing changed in the code, nothing changed in the target site, and the proxy dashboard shows green.
This is the most common shape of a curl proxy incident: an error that is reproducible in production, invisible locally, and described by a two-digit number that tells you almost nothing on its own. curl is deliberately terse. It reports what failed at the transport layer, not why the upstream path behaved the way it did, and when a proxy sits in the middle there are at least three independent places a connection can break.
This guide is about reading those failures correctly. Not "how to set --proxy", but how to tell a TLS verification failure apart from a TLS handshake rejection, how to distinguish a dead exit node from a throttled one, and why a redirect loop is usually a geolocation or cookie problem rather than a curl problem. The goal is a diagnostic process you can encode into a production pipeline so that failures classify themselves instead of waking someone up.
The Anatomy of a Proxied curl Request
Before debugging anything, it helps to be precise about what happens when curl talks to an HTTPS target through a proxy.
With an HTTP proxy and an HTTPS target, curl opens a TCP connection to the proxy, sends a CONNECT host:443 HTTP/1.1 request, and waits for a 200 Connection established response. Only then does it begin a TLS handshake with the origin server, tunnelled through that connection. Two TLS sessions can exist here: one between curl and the proxy (if the proxy endpoint itself is HTTPS), and one between curl and the origin inside the tunnel. Modern curl treats these as separate configuration domains, which is why --cacert and --proxy-cacert both exist.
With a SOCKS5 proxy, there is no CONNECT verb. curl negotiates a SOCKS handshake, optionally authenticates, and then the proxy opens a TCP connection to the destination. Crucially, socks5 resolves DNS locally while socks5h resolves it at the proxy. That single character has caused more silent geo-targeting failures than almost any other configuration detail.
Every curl error you will see maps to one of four phases: DNS resolution, TCP connect to the proxy, proxy negotiation (CONNECT or SOCKS plus authentication), and the origin-side TLS handshake and HTTP exchange. Identifying the phase is 80 percent of the diagnosis.
Making curl Tell You Which Phase Failed
The fastest instrument is the write-out timing breakdown. Run this and you get a machine-parsable phase map for every request:
curl -s -o /dev/null \ -x http://user:[email protected]:8000 \ -w '{"dns":%{time_namelookup},"connect":%{time_connect},"tls":%{time_appconnect},"pretransfer":%{time_pretransfer},"ttfb":%{time_starttransfer},"total":%{time_total},"code":%{http_code},"redirects":%{num_redirects}}' \ https://target.example.com/product/123
Read it this way. If connect is near zero and everything else is zero, you never reached the proxy. If connect is populated but tls is zero, the proxy accepted TCP but the tunnel or handshake never completed. If tls completes quickly but ttfb is enormous, the exit node is alive and the origin is stalling or throttling you. If total is small but redirects is at your ceiling, you have a loop, not a network fault.
Log that JSON blob for every production request. It costs nothing and it converts vague incident reports into a histogram you can actually reason about.
For interactive debugging, -v shows the CONNECT exchange and the TLS negotiation, and --trace-ascii trace.log captures the full byte-level conversation including proxy response headers that -v summarises away. When a provider gateway returns a meaningful error body inside a failed CONNECT, the trace is usually the only place you will see it.
SSL and TLS Failures: Four Different Problems Wearing One Mask
TLS errors are where engineers waste the most time, because several unrelated causes produce superficially similar output.
Error 60: Certificate Verification Failed
Exit code 60 (SSL certificate problem) means curl completed a handshake but rejected the certificate chain. Through a proxy, there are two chains in play, and the first question is always which one failed.
If the message references the origin hostname, the usual causes are a stale CA bundle on the container image, a target serving an incomplete chain that your local store cannot complete, or a captive-style interception somewhere on the path. Containers built from minimal base images are repeat offenders: ca-certificates is either absent or frozen at the image build date, so newly rotated roots fail months later with no code change.
If the message references the proxy hostname, you are hitting the proxy-side chain. Point curl at the correct bundle with --proxy-cacert, not --cacert, and never reach for --proxy-insecure as a permanent fix. Disabling verification on the leg that carries your proxy credentials is a credential-exposure decision, not a debugging shortcut.
Error 35: Handshake Rejected, Not Certificate Rejected
Exit code 35 (SSL connect error) is different and more interesting. The handshake itself failed, often before any certificate was presented. In proxy pipelines this frequently is not a configuration bug at all: it is the target refusing your TLS client profile.
Anti-bot systems increasingly act on the ClientHello. If your curl build negotiates an unusual cipher order, omits ALPN entries a real browser would send, or advertises extensions in a sequence that does not match the user agent you claim, the edge can drop the handshake rather than return a 403. The visible symptom is an SSL error; the actual cause is a fingerprint mismatch. A useful tell: the same command succeeds against a neutral endpoint through the same proxy, and fails only against the protected target. That pattern rules out the proxy and points at the TLS layer of your client.
The other common producer of error 35 is protocol mismatch. Forcing --http2 through a proxy that only tunnels HTTP/1.1 cleanly, or pinning --tlsv1.3 against an origin that terminates at an older stack, both surface here. Test with --http1.1 and with default TLS version negotiation before blaming the network.
Error 56 and Error 52: Mid-Stream Collapse
Recv failure: Connection reset by peer (56) and Empty reply from server (52) after a successful handshake usually indicate the connection died after the request was sent. Through residential or mobile exits, this can be an ordinary peer dropping off. Repeated consistently against one target, it more often means the origin accepted the handshake and then terminated the session on inspection of the request, which is a behavioural block dressed as a network error.
Error 7 and 407: You Never Got Past the Proxy
Failed to connect (7) means TCP to the proxy endpoint failed: wrong port, firewall egress rule, or a gateway hostname that resolves differently inside your VPC. A 407 Proxy Authentication Required surfacing through curl usually means the credentials were sent but rejected, and the single most frequent cause is unencoded special characters in the password when credentials are embedded in the proxy URL. Move them to --proxy-user or percent-encode them. A # or @ in a password silently truncates the URL and produces an authentication failure that looks like an account problem.
Timeouts: Distinguishing Dead From Slow
Timeouts are the error class most often handled badly, because teams set a single --max-time and treat every expiry identically.
curl offers two primary controls and they answer different questions. --connect-timeout bounds the time to establish the connection, which through a proxy includes the proxy handshake. --max-time bounds the entire operation including transfer. If you only set the second one, a slow 4 MB response and a completely unreachable exit node produce the same exit code 28, and your retry logic cannot tell them apart.
A sane production baseline looks like this:
curl --connect-timeout 10 --max-time 45 \ --retry 2 --retry-delay 2 --retry-max-time 90 \ -x socks5h://user:[email protected]:1080 \ https://target.example.com/
The connect timeout should be tight: if a residential exit has not completed negotiation in ten seconds, it is unlikely to become healthy inside the same request. The max time should be generous enough for the heaviest legitimate payload you expect, because truncating large responses produces partial data that silently corrupts downstream tables.
Reading Timing Signatures
Use the timing breakdown to classify:
High time_namelookup. DNS is slow or being resolved in the wrong place. With socks5 (no h), resolution happens on your machine, which both adds latency and defeats geo-targeting because the target may be served the record for your datacenter region rather than the exit country.
High time_connect with low everything else. Congestion or queueing at the proxy gateway, or an overloaded exit. In rotating pools this is normal for a small fraction of requests and pathological when it affects a sustained share of them.
High time_appconnect. Slow TLS negotiation, often a sign of a constrained mobile exit or an origin applying deliberate handshake delay to suspect clients.
High time_starttransfer with normal connect and TLS. The origin is deliberating. This is the classic signature of soft rate limiting: the server has decided to serve you, slowly. Raising concurrency here makes it worse, not better.
Normal everything, low size_download. You are getting a challenge page or an interstitial, not your data. A timeout alert will never fire, but the pipeline is already broken.
That last case deserves emphasis. Treating HTTP 200 as success is the single most expensive assumption in scraping infrastructure. Validate response size and a content assertion, and classify a 200 that fails the assertion as a distinct failure type with its own retry and rotation policy.
Redirect Loops: Almost Never a curl Bug
curl exits with code 47 (Maximum (N) redirects followed) when -L chases a chain past --max-redirs. The loop itself is a symptom. In proxied pipelines, four causes account for most of them.
Missing cookie persistence. The target sets a session or consent cookie, redirects you to the same URL expecting it to be present, and you arrive cookieless because you did not supply -b and -c with a jar. Every hop sets the cookie again and redirects again. Fix the jar, not the redirect limit. Remember that a single shared cookie jar across concurrent workers using different exit IPs creates its own inconsistency, so scope jars per session.
Geolocation mismatch. Your proxy exits in Germany, your Accept-Language header says en-US, and your request hits a storefront that redirects to a locale subdomain, which then re-evaluates and redirects back. Loops like this are a consistency failure between IP geolocation and request headers. Align language, locale path, and currency parameters with the exit country before increasing --max-redirs.
HTTP to HTTPS upgrade conflicts. A request made over plain HTTP through a proxy that rewrites or a target with aggressive HSTS behaviour can bounce between schemes. Always request the HTTPS URL explicitly and avoid relying on the origin to upgrade you.
Authentication and challenge walls. Many anti-bot layers implement challenges as redirect chains. A loop that only appears on certain exit IPs is a reputation signal, not a configuration issue. Rotate to a different pool segment and compare.
Two flags matter when you debug chains. --max-redirs 5 keeps failures fast and explicit instead of letting curl wander. -L -v prints every Location header so you can see whether the chain is cycling between two URLs (loop) or progressively adding query parameters (challenge flow). Be careful with --location-trusted: it forwards credentials across hosts on redirect, which is exactly how proxy or API credentials end up in an unintended request.
One more subtlety for POST-based pipelines. On a 301 or 302, curl converts the method to GET unless you pass --post301 or --post302. If a form submission mysteriously becomes an empty GET after a redirect, that is the behaviour you are seeing, and -L alone will not preserve it.
Environment-Level Mistakes That Produce Phantom Failures
Several recurring issues have nothing to do with the request itself.
Environment variables overriding intent. http_proxy, https_proxy, ALL_PROXY and NO_PROXY are read by curl automatically. A pipeline that explicitly passes -x on some calls and relies on env vars on others will route inconsistently, and a stray NO_PROXY=* in a base image will send traffic direct while your logs insist a proxy was configured. Verify with curl -v and confirm the CONNECT line is actually present.
Scheme assumptions. -x host:port with no scheme defaults to HTTP proxy semantics. If your endpoint expects SOCKS5, the failure can look like a protocol error rather than a misconfiguration. Always write the scheme explicitly.
Version drift. Proxy-specific TLS options, HTTPS proxy support, and HTTP/2 behaviour have all changed across curl releases. A command that works on a developer machine running a current build can fail on a CI runner pinned to a years-old distro package. Log curl --version in the pipeline and treat it as part of your dependency surface.
libcurl binding differences. If you call curl through a language binding, the option names map but the defaults do not always. Verify that connect timeout, redirect limit, and CA path are being set explicitly rather than inherited.
Where Proxies Fit In: Infrastructure Choices That Prevent Half of These Errors
A large share of curl proxy errors are not client bugs at all. They are the visible edge of a proxy pool that does not match the workload.
Exit quality determines how often you see codes 35, 52 and 56. Exits on heavily recycled subnets get handshake-rejected or reset by protected origins long before any application logic runs. Pool composition determines timeout distribution: residential and mobile exits are inherently more variable than ISP or datacenter addresses, so a pipeline pinned to a strict --max-time against a residential pool will record failures that are really just normal variance. Session control determines redirect behaviour, because a chain that requires a consistent IP across four hops will loop forever if your gateway rotates mid-session.
This is where infrastructure selection does real engineering work. Having access to rotating residential proxy pools alongside ISP, datacenter and mobile options means you can match pool type to failure mode rather than fighting symptoms in curl flags. Timeout-sensitive bulk fetching belongs on low-variance pools. Multi-step authenticated flows belong on sticky sessions where the exit IP holds for the full redirect chain. Targets that reject on handshake almost always need higher-trust exits rather than a different cipher suite.
EnigmaProxy positions itself in the professional tier on exactly these axes: multiple pool types under one account, broad geo-coverage so locale headers can be aligned to exit country, ethically sourced residential and mobile IPs, and session controls that let you hold an exit for the duration of a multi-hop flow. Predictable pricing matters here too, because the honest answer to intermittent proxy errors is retries, and retries consume bandwidth you need to budget for.
Before blaming your pipeline, isolate the variable. Running a candidate endpoint through a proxy testing tool to confirm reachability, geolocation and basic response health separates "this exit is unhealthy" from "this target is blocking my client profile" in about thirty seconds, which is considerably faster than bisecting curl flags at 03:00.
Building Diagnosis Into the Pipeline
The difference between a mature data pipeline and a fragile one is not fewer errors. It is error classification.
Start by mapping exit codes to policies. Codes 7 and 28-at-connect indicate an unhealthy exit: rotate immediately and do not count the attempt against the target. Code 35 against a specific target and not others indicates a client profile problem: escalate to a different request strategy rather than burning IPs. Codes 52 and 56 after a successful handshake suggest behavioural blocking: slow down, rotate, and reduce concurrency. Code 47 indicates a session or locale consistency failure: fix headers and cookie scope, not the redirect ceiling. Code 60 is almost always a trust store or configuration issue on your side: fix the image, never disable verification.
Record the timing JSON for every request and alert on distribution shifts rather than individual failures. A rise in median time_starttransfer is an early warning of soft throttling that typically precedes hard blocks by hours. A rise in time_connect variance usually means pool health, not target behaviour.
Finally, run canaries. A small, continuous set of requests to a neutral endpoint through each pool gives you a baseline that separates "the internet is having a day" from "our configuration broke". Without a control group, every incident becomes a guess.
Strategic Outlook: What Changes Next
Four shifts are worth planning for.
TLS-layer identity keeps getting stricter. Handshake-level rejection is becoming a standard filter, which means error 35 will increasingly reflect client profile rather than network fault. Teams running curl in production should expect to maintain TLS behaviour deliberately, and to align it with the headers and exit type they present.
HTTP/3 and QUIC complicate proxy paths. UDP-based transport does not tunnel through classic CONNECT the way TCP does, and support across proxy infrastructure remains uneven. Expect a period where forcing HTTP/1.1 or HTTP/2 through proxies is the pragmatic default while tooling catches up.
Post-quantum key exchange is enlarging handshakes. Hybrid key agreement increases ClientHello size, and some middleboxes handle oversized handshakes poorly. The failure signature looks like a generic connect error, and it will become a more common cause of hard-to-explain TLS faults over the next few years.
Observability moves from logs to error taxonomies. The teams that scale cleanly are the ones treating network errors as structured categories with owners and policies, not as stack traces. Expect retry logic to become pool-aware: routing decisions made from live success-rate telemetry rather than static configuration.
Conclusion
curl is an honest tool. It tells you precisely where a connection died, and it refuses to speculate about why. When a proxy sits in the path, the job is to translate that terseness into a phase: DNS, proxy negotiation, TLS, or HTTP exchange. Once you know the phase, the fix is usually obvious and often has nothing to do with curl at all.
The practical takeaways are straightforward. Instrument every request with the timing breakdown. Set connect and total timeouts separately so your retries can distinguish dead from slow. Treat redirect loops as session, cookie, or geolocation consistency problems. Never disable certificate verification as a fix, and never treat HTTP 200 as proof of success.
And recognise the limit of client-side tuning. If a meaningful share of your failures originate from exhausted or poorly matched exits, no combination of flags will repair them. Pairing a disciplined error taxonomy with a provider such as EnigmaProxy, offering diverse pool types, ethical sourcing and dependable session control, removes an entire category of failure from the debugging queue so your team can spend its time on the problems that are genuinely yours to solve.