Squid cache_peer Upstream Proxy With Authentication
Set up a Squid cache_peer upstream proxy with login=user:pass, never_direct and LAN ACLs so tools without proxy auth can use a paid proxy. Fix TCP_MISS/503.
A squid cache_peer upstream proxy solves a narrow, annoying problem: half the tools on your network can point at a proxy host and port but have no field for a username and password. Put Squid on one box, let it hold the credentials in a cache_peer ... login= line, and every device on the LAN gets the paid proxy without knowing a secret. This guide gives you a squid.conf that works on Squid 6 and 7, then covers how rotation behaves through the extra hop, what happens to HTTPS, and how to read the TCP_MISS/503 lines that show up when the parent stops answering.
The Job: One Front Door for Tools That Can't Authenticate
The usual trigger is a mixed LAN. A smart TV app, an old Java crawler, a monitoring appliance, a Windows system proxy setting that some programs honour and others don't. Each accepts host:port. Few of them can send a Proxy-Authorization header, and the ones that can each store the password in a different place.
Squid fixes this by sitting in the middle. Clients connect to Squid with no credentials. Squid checks that they come from your LAN, then forwards every request to the paid gateway as a parent, attaching the username and password itself. The paid proxy only ever sees Squid.
LAN clients (no auth) Squid box SparkProxy gateway
192.168.1.0/24 ---> 192.168.1.10:3128 ---> gateway.sparkproxy.io:11000 ---> target
ACL: LAN only login=USER:PASS sent here sees the
never_direct rotation happens here exit IP
If the parent and child terminology is new, the short version lives in what is an upstream proxy. This post assumes you know the idea and need the config to survive real traffic. It is also not a privacy chain. Stacking hops for anonymity is a different goal with different tradeoffs, covered in what is proxy chaining. Here the second hop exists purely to hold credentials and enforce who may use them.
What to Collect Before You Edit squid.conf
Gather these first. Most broken setups we see trace back to one wrong value in this list rather than to Squid itself.
| Item | Where it comes from | Example used in this guide |
|---|---|---|
| Gateway host | SparkProxy dashboard | `gateway.sparkproxy.io` |
| Rotating HTTP port | Dashboard | `11000` |
| Sticky Sessions port | Dashboard | `11002` |
| Username and password | Dashboard | `YOUR_USER` / `YOUR_PASS` |
| Squid box LAN address | `ip -4 addr` on the box | `192.168.1.10` |
| LAN subnet allowed to use it | Your router | `192.168.1.0/24` |
| Squid box public IP (whitelist route only) | `curl -s https://api.ipify.org` from the box | `203.0.113.25` |
| Squid version | `squid -v` | 6.x or 7.x |
On the version: the Squid project lists v7.7, released 2026-08-24, as the current release series on its Getting Squid page (read 2026-10-30), and the latest commit on the GitHub repository was dated 2026-10-23 when we checked. Your distribution may ship an older 6.x package. Every directive below is documented for both v6 and v7 in the cache_peer reference.
Before touching Squid, confirm the Squid box can reach the gateway on its own. If this fails, no squid.conf will help:
# Run on the Squid box, not on a client
curl -s -x http://YOUR_USER:YOUR_PASS@gateway.sparkproxy.io:11000 https://api.ipify.org; echo
You should see an IP that is not the box's own public address. Write it down.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
A Working squid.conf for a cache_peer Upstream Proxy
Install Squid (sudo apt install squid on Debian or Ubuntu), move the stock file aside, and start from this. It is a pure forwarder: no disk cache, LAN-only access, and no way out except through the parent.
# /etc/squid/squid.conf -- Squid 6.x / 7.x
http_port 3128
# Who may use this proxy. Keep it to your real subnet.
acl localnet src 192.168.1.0/24
acl SSL_ports port 443
acl Safe_ports port 80 443
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost
http_access allow localnet
http_access deny all
# The paid gateway as the only parent, with credentials
cache_peer gateway.sparkproxy.io parent 11000 0 no-query no-digest default name=spark_rotating login=YOUR_USER:YOUR_PASS connect-timeout=10 connect-fail-limit=5
# Never connect to a target directly, even if the parent is down
never_direct allow all
# Don't leak LAN details to the target
forwarded_for delete
via off
httpd_suppress_version_string on
# Forward only, store nothing
cache deny all
Then validate and load it:
sudo squid -k parse # syntax check, prints the offending line on error
sudo systemctl restart squid
curl -s -x http://192.168.1.10:3128 https://api.ipify.org; echo # from a LAN client
What each non-obvious piece does:
cache_peer ... parent 11000 0: the gateway's HTTP port, then0for the ICP port because a commercial gateway does not speak ICP.no-queryandno-digeststop Squid from trying cache-sibling protocols against it.default: marks this as the last-resort parent. With a single peer it mostly affects how the hierarchy code appears in the log.name=spark_rotating: the label Squid uses in cache.log and incache_peer_access. You need it once you add a second peer on the same hostname, so set it from day one.login=YOUR_USER:YOUR_PASS: Squid adds a BasicProxy-Authorizationheader on every request it sends to this peer, includingCONNECTfor HTTPS.connect-timeout=10overrides the globalpeer_connect_timeout, which defaults to 30 seconds per the directive docs. Thirty seconds of hanging is a long time for a LAN client.connect-fail-limit=5: how many connect failures mark the peer DEAD. The documented default is 10.never_direct allow all: the line that makes the setup trustworthy. Without it, if the parent fails Squid may quietly fetch from your own IP, and you'd only find out from the target's logs.forwarded_for delete: Squid's default ison, which appendsX-Forwarded-For: 192.168.1.xto plain-HTTP requests it forwards. Through a paid exit, that header hands the target your internal address.deleteremoves it, andvia offdrops theViaheader Squid adds by default.
The ACL order matters. http_access rules are first-match, so deny all must be last, and the localnet range must never be widened to all on a box with a public interface. An open Squid that logs in to a paid gateway is an open proxy billed to you.
Passwords With Special Characters in login=
This one costs people an afternoon. The cache_peer docs state that the login= string "can include URL escapes (i.e. %20 for spaces). This also means % must be written as %%." So Squid decodes the value before sending it, and a password that contains % gets mangled unless you double it.
| Character in the real password | Write it in login= as | Why |
|---|---|---|
| `%` | `%%` | Squid treats `%` as the start of an escape |
| space | `%20` | Whitespace ends the directive token |
| `:` in the username | avoid it | The colon is what separates user from password |
The symptom of getting this wrong is a perfectly reachable parent that answers every request with 407. The quickest fix is boring: generate a gateway password from letters and digits only, and keep the escaping table for credentials you don't control.
Whitelist the Squid Server's IP Instead of login=
If Squid runs on a machine with a stable public IP, usually a small VPS or an office line with a static address, you can skip credentials entirely. Add the box's public IP to the whitelist in the SparkProxy dashboard and delete the login= option:
cache_peer gateway.sparkproxy.io parent 11000 0 no-query no-digest default name=spark_rotating connect-timeout=10
never_direct allow all
The gateway now authorises by source address. Two things change.
First, no password travels between Squid and the gateway. That hop is plain HTTP to port 11000, so a login= header is Base64, not encryption, readable by anything on the path. Whitelisting removes that exposure.
Second, your http_access rules become the only lock. Anyone who can reach port 3128 rides your whitelisted IP. On a VPS, localnet should be your office's public address, not a private range, and the cloud firewall should block 3128 from everywhere else:
# Squid on a VPS, used from one office
acl office src 198.51.100.40/32
http_access allow office
http_access deny all
Plans include a fixed number of whitelist slots; on 2026-10-30 the SparkProxy pricing section listed 5 on the $75/month Starter pack and 10 on the $140/month Core pack (USD, excluding VAT/GST). One Squid box uses one slot, which is the point: dozens of LAN devices, one whitelisted address. Whitelisting fails if the box's IP changes, so don't use it on a residential connection with a dynamic address. The mechanics and the dynamic-IP trap are covered in what is IP whitelisting for proxies.
| login= on cache_peer | Whitelisted Squid IP | |
|---|---|---|
| Secret stored on the box | Yes, in squid.conf | No |
| Credential on the wire to the gateway | Yes, Basic auth | No |
| Survives a public IP change | Yes | No |
| Who controls access | Gateway login plus Squid ACLs | Squid ACLs alone |
| Country or session options via username | Yes | Depends on the port and dashboard settings |
How Rotation Passes Through Squid
Squid does not rotate anything. It forwards; the gateway picks the exit. SparkProxy offers Random Rotation (the site describes it as a fresh IP per request) and Sticky Sessions, which hold one exit for 5 minutes. What Squid controls is how many upstream connections carry your traffic, and that decides how rotation looks from the client side.
Plain HTTP. Squid keeps idle connections to the parent open and reuses them, because server_persistent_connections defaults to on (docs). Several client requests can share one upstream connection. If your measurements show the same exit repeating more than you expect for plain-HTTP targets, force a new upstream connection per request:
server_persistent_connections off
You pay a TCP handshake per request to the gateway in exchange.
HTTPS. Here rotation granularity is the tunnel, and no setting changes that. A CONNECT tunnel is one TCP stream from the exit IP to the target. The gateway cannot swap the exit in the middle of it without breaking TLS. So a browser that opens one tunnel and sends forty requests over HTTP keep-alive presents one IP for all forty. A script that opens a new connection per request presents a new IP per request. Same Squid, same gateway, different client behaviour.
Don't take our word for it. Measure it on your own setup with this script, which runs from any LAN machine:
import requests
SQUID = "http://192.168.1.10:3128"
PROXIES = {"http": SQUID, "https": SQUID}
ECHO = "https://api.ipify.org"
def fresh_connections(n):
# requests.get() builds a new session each call, so each call opens a new tunnel
return [requests.get(ECHO, proxies=PROXIES, timeout=20).text.strip() for _ in range(n)]
def one_session(n):
# one Session reuses the same CONNECT tunnel while the server keeps it alive
with requests.Session() as s:
s.proxies.update(PROXIES)
return [s.get(ECHO, timeout=20).text.strip() for _ in range(n)]
if __name__ == "__main__":
a = fresh_connections(10)
b = one_session(10)
print("new tunnel per request:", len(set(a)), "distinct IPs of", len(a))
print("one reused session: ", len(set(b)), "distinct IPs of", len(b))
If both numbers come back the same, the echo service closed the connection between calls and you're seeing per-connection behaviour either way. The lesson for tools you can't modify: a browser-like client through a rotating gateway rotates per tunnel, so if you need one IP across a whole login flow, give that tool the sticky port described next instead of hoping keep-alive holds.
Separate Ports for Rotating, Sticky and Country Exits
Different tools want different exits. A price checker wants a new IP every request. A tool that logs in wants Sticky Sessions. A QA box wants German exits. Since the tools can't send usernames, encode the choice in the Squid port they connect to.
Squid can name each http_port, match on that name with a myportname ACL, and steer requests to a specific peer with cache_peer_access. Three peers on the same hostname need distinct name= values, which the docs require.
http_port 3128 name=rotating
http_port 3129 name=sticky
http_port 3130 name=de
acl port_rotating myportname rotating
acl port_sticky myportname sticky
acl port_de myportname de
cache_peer gateway.sparkproxy.io parent 11000 0 no-query no-digest name=rot login=YOUR_USER:YOUR_PASS
cache_peer gateway.sparkproxy.io parent 11002 0 no-query no-digest name=stick login=YOUR_USER:YOUR_PASS
cache_peer gateway.sparkproxy.io parent 11000 0 no-query no-digest name=de login=YOUR_USER-country-de:YOUR_PASS
cache_peer_access rot allow port_rotating
cache_peer_access rot deny all
cache_peer_access stick allow port_sticky
cache_peer_access stick deny all
cache_peer_access de allow port_de
cache_peer_access de deny all
never_direct allow all
Notes on this layout:
- The country peer uses the country-suffixed username format that the SparkProxy dashboard shows (the homepage widget displays
user-country-us). Copy the exact string from your dashboard rather than guessing a format. defaultis gone. The docs say only the firstdefaultparent counts, and a default parent could catch requests from the wrong port. Withcache_peer_accessandnever_direct, each port has exactly one eligible parent.- Each
cache_peer_accessgroup ends indeny allfor that peer, which keeps the routing explicit.
Point the smart TV at :3128, the login-heavy tool at :3129 and the QA browser at :3130. One box, three behaviours, no credentials on any client.
HTTPS and CONNECT Through the Parent
For an https:// URL, the client sends Squid CONNECT target:443. Squid checks it against http_access (the stock deny CONNECT !SSL_ports rule rejects tunnels to anything but 443), then opens its own connection to the parent and sends CONNECT target:443 there, with the login= credentials attached. Once the parent answers 200, Squid shovels bytes both ways. TLS runs end to end between the client and the target; neither Squid nor the gateway sees inside without deliberate TLS interception, which you don't want here.
Practical consequences:
- The target's TLS fingerprint is the client's. Squid doesn't rewrite it. If a target blocks your old Java crawler on its handshake, Squid can't fix that, and neither can a better exit IP.
- Non-443 HTTPS fails at Squid. A tool connecting to
https://api.sparkproxy.io:8443gets denied bydeny CONNECT !SSL_ports. Add the port toSSL_portsandSafe_portsdeliberately if you need it. - Parent auth failures look different. On HTTPS, Squid's tunneler relays the parent's error reply. curl reports it as
curl: (56) CONNECT tunnel failed, response 407. On plain HTTP you get the 407 response body itself. - Caching does nothing for tunnels. Squid can't cache what it can't read, which is one reason this config uses
cache deny all.
Reading access.log: Prove Nothing Goes Direct
Squid's default squid log format is %ts.%03tu %6tr %>a %Ss/%03>Hs %TCP_MISS/200) and the hierarchy code with the server address (FIRSTUP_PARENT/65.109.70.130). The lines below are illustrative; 65.109.70.130 is the address gateway.sparkproxy.io resolved to when we checked, and yours may differ.
1793350211.402 388 192.168.1.42 TCP_MISS/200 1342 GET http://books.sparkproxy.io/ - FIRSTUP_PARENT/65.109.70.130 text/html
1793350215.118 1904 192.168.1.42 TCP_TUNNEL/200 6120 CONNECT www.sparkproxy.io:443 - FIRSTUP_PARENT/65.109.70.130 -
1793350230.770 10012 192.168.1.42 TCP_MISS/503 4012 GET http://books.sparkproxy.io/ - HIER_NONE/- text/html
The rule is simple: with never_direct allow all you should never see HIER_DIRECT. A parent code (DEFAULT_PARENT, FIRSTUP_PARENT, ANY_OLD_PARENT) means the request went through the gateway. HIER_NONE/- means Squid never got an answer from anyone. The Squid log FAQ also lists NO_DIRECT_FAIL: the request was blocked by never_direct and no parent was available. TCP_TUNNEL marks a CONNECT, and the wiki notes the NONE tag can appear on CONNECT responses too.
This script tallies a log and flags any direct fetch:
import sys
from collections import Counter
PARENT_CODES = {"DEFAULT_PARENT", "FIRSTUP_PARENT", "ANY_OLD_PARENT", "SINGLE_PARENT"}
def summarize(path):
results, hierarchy, direct = Counter(), Counter(), []
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
parts = line.split()
if len(parts) < 10:
continue
result, hier = parts[3], parts[8].split("/")[0]
results[result] += 1
hierarchy[hier] += 1
if hier == "HIER_DIRECT":
direct.append(parts[6])
return results, hierarchy, direct
if __name__ == "__main__":
res, hier, direct = summarize(sys.argv[1] if len(sys.argv) > 1 else "/var/log/squid/access.log")
print("Top result codes:", res.most_common(8))
print("Hierarchy:", dict(hier))
went_up = sum(v for k, v in hier.items() if k in PARENT_CODES)
print("Through a parent:", went_up)
if direct:
print("WARNING: direct fetches found, check never_direct:", direct[:5])
Run it from cron and alert on any WARNING line. A direct fetch means your own IP touched a target.
Fixing TCP_MISS/503 and Other Upstream Errors
The symptom: clients get an error page, and access.log shows TCP_MISS/503. On Squid 6 and later you may see TCP_MISS_ABORTED/503 instead; the log FAQ notes that since v6 the ABORTED tag also appears when Squid cannot talk to a cache_peer, such as when the peer refuses TCP connections. The error page text tells you which template Squid used:
Connection to 65.109.70.130 failed.followed byThe system returned: ...is ERR_CONNECT_FAIL.Unable to forward this request at this time. This request could not be forwarded to the origin server or to any parent caches.is ERR_CANNOT_FORWARD. Its own text lists "All configured parent caches may be currently unreachable" and "The administrator may not allow this cache to make direct connections". That is never_direct doing its job while the parent is down.
Work through the causes cheapest first.
1. Is the error Squid's or the parent's? Squid stamps its own error pages with an X-Squid-Error header holding the template name and errno:
curl -s -o /dev/null -D - -x http://192.168.1.10:3128 http://books.sparkproxy.io/ | grep -i -E '^HTTP|x-squid-error'
# HTTP/1.1 503 Service Unavailable
# X-Squid-Error: ERR_CONNECT_FAIL 111
Errno 111 is ECONNREFUSED on Linux, 110 is ETIMEDOUT. No X-Squid-Error header and a parent hierarchy code in the log means the gateway itself answered 503, so the problem is upstream of Squid.
2. Can the box reach the gateway without Squid? Repeat the direct curl from the prerequisites section on the Squid box. If it fails too, Squid is innocent: check the host and port, the box's outbound firewall, and DNS for gateway.sparkproxy.io. Port typos are common; 11000 is rotating HTTP and 13000 is SOCKS5, and Squid's cache_peer speaks HTTP to its parent, so pointing it at the SOCKS port fails.
3. Has Squid marked the parent dead? Check cache.log:
sudo grep -E 'Connection to .* failed|Detected (DEAD|REVIVED) Parent' /var/log/squid/cache.log | tail
ERROR: Connection to spark_rotating failed repeated up to connect-fail-limit, then Detected DEAD Parent: spark_rotating, means Squid stopped using the peer. Every request after that hits ERR_CANNOT_FORWARD until Detected REVIVED Parent appears. Fix the connectivity cause, then sudo squid -k reconfigure to retry immediately.
4. Are the credentials right? A 407 rather than a 503 points at login=. Check the escaping rules above, or test with the whitelist route to rule credentials out entirely.
5. Did the target refuse the exit? If the gateway connects but the target blocks or times out, you get whatever the gateway returns, relayed through Squid with a parent hierarchy code. Rotating to a fresh exit, switching country, or slowing request rate can help here. Nothing in squid.conf will.
| What you see | Most likely cause | Proxy-side fix? |
|---|---|---|
| `ERR_CONNECT_FAIL 111` | Wrong port, gateway unreachable, outbound firewall | Fix host, port or firewall on the Squid box |
| `ERR_CONNECT_FAIL 110` | Network path drops packets, timeout | Check routing; lower `connect-timeout` to fail fast |
| `ERR_CANNOT_FORWARD` after `Detected DEAD Parent` | Parent marked dead after repeated failures | Restore connectivity, then `squid -k reconfigure` |
| 407 or `CONNECT tunnel failed, response 407` | Bad or mis-escaped `login=`, IP not whitelisted | Fix credentials or whitelist the box |
| 403 from Squid, `ERR_ACCESS_DENIED` | Client outside `localnet`, or non-443 CONNECT | Adjust ACLs |
| 503 with parent hierarchy code, no X-Squid-Error | Target-side block or gateway answer | Different exit or country; not a Squid fix |
| Target rejects the TLS handshake | Client fingerprint | No. Neither Squid nor the exit IP changes it |
Squid also retries on its own. forward_max_tries defaults to 25 (docs), while connect_retries defaults to no retries of a failed connection opening. With a single parent and never_direct, retries have nowhere else to go, so they mostly add delay before the error.
Plan Threads, max-conn and Cost
SparkProxy plans are packs priced by concurrency, with unlimited bandwidth included. "Starting from $75/month" buys the Starter pack, which the pricing section listed on 2026-10-30 at 100 threads; Core is $140/month for 250 threads, Boost $240/month for 500, and Plus $440/month for 1,000 (USD, billed every 30 days, excluding VAT/GST). Term discounts are 5% for 3 months, 10% for 6 and 15% for 12. Because traffic volume isn't billed, a Squid front door doesn't change your bill by moving more bytes. What it changes is how many simultaneous connections your LAN opens upstream.
Squid can cap that per peer with max-conn:
cache_peer gateway.sparkproxy.io parent 11000 0 no-query no-digest default name=spark_rotating login=YOUR_USER:YOUR_PASS max-conn=100
Read the documented behaviour carefully before relying on it: "A peer exceeding the limit is not used for new requests unless a standby connection is available." With one parent and never_direct, a request that arrives at the cap has no other path, so expect it to fail with an error instead of waiting in a queue. The docs also warn that max-conn "currently works poorly with idle persistent connections". Treat it as a guard rail you test under load, and set it a little under your plan's thread count. If your LAN's peak legitimately exceeds the plan, the fix is a bigger pack, not a tighter Squid.
Two cheaper habits help more than tuning. Keep long-lived background tools (update checkers, telemetry) off the proxy port so they don't hold threads. And give sticky-session tools their own port, as above, so a five-minute session isn't fighting a high-churn scraper for the same peer.
When the tool on the LAN is a scraper you control, Squid may be the wrong layer altogether. The SparkProxy Scraping API takes the target as a parameter and handles the exit for you; a plain fetch with render_js=false costs 1 credit, and new accounts get 1,000 free credits:
import requests
r = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"url": "https://www.sparkproxy.io/", "render_js": "false"},
timeout=60,
)
print(r.status_code, len(r.text))
Keep Squid for the tools you can't rewrite. That is where a single authenticated front door earns its place.
Frequently asked questions
FAQ
Add login=USERNAME:PASSWORD to the cache_peer line, for example cache_peer gateway.sparkproxy.io parent 11000 0 no-query default login=USER:PASS. Squid then sends a Basic Proxy-Authorization header to the parent on every forwarded request, including HTTPS CONNECT, so LAN clients need no credentials.
It usually means Squid could not get a usable response from the parent: the TCP connection failed (ERR_CONNECT_FAIL), or the parent was marked dead and never_direct blocked any fallback (ERR_CANNOT_FORWARD). Check the X-Squid-Error header and cache.log for Detected DEAD Parent to tell which.
It forbids Squid from fetching any request straight from the origin server, so everything must go through a cache_peer parent. If the parent is down, clients get an error instead of a silent direct fetch from your own IP. Without it, a failed parent can leak traffic.
Yes, if the Squid box has a stable public IP. Whitelist that IP in the provider dashboard and remove login= from cache_peer. No credential travels to the gateway, but your Squid http_access ACLs become the only thing stopping strangers from using your plan.
Yes. The gateway rotates, not Squid. For HTTPS the exit is fixed for the life of each CONNECT tunnel, so clients that reuse one connection keep one IP. For plain HTTP, Squid's persistent connections to the parent can be turned off with server_persistent_connections off if you need a new upstream connection per request.
Give Squid several http_port lines with names, match them with myportname ACLs, and bind each port to its own cache_peer (each with a unique name=) using cache_peer_access. Point each tool at the port whose peer uses the rotating port, the Sticky Sessions port, or a country-suffixed username.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
How to See Google Ads in Other Countries Without Clicking
How to see Google ads in other countries: Ads Preview first, then a clean browser behind a country proxy, no clicks, and a daily script that logs ad copy.
Crawlee Proxy Configuration: Rotation That Keeps Sessions
Crawlee proxy setup with proxyUrls, newUrlFunction and tieredProxyUrls: how SessionPool binds a proxy, and how to stop the session-retired loop on login crawls.
Build Your Own Rank Tracker With Python and Proxies
Build your own rank tracker in Python: a keyword CSV, SERP fetches through rotating proxies or the Scraping API, Postgres history and a weekly movement report.
