Squid
Run and debug a Squid forward proxy: http_access rule order, ACLs, caching, authentication, parent proxies and access.log analysis.
On this page
Cheatsheet#
| Task | Command |
|---|---|
| Check config syntax before reload | squid -k parse |
| Apply config without a restart | squid -k reconfigure |
| Rotate logs | squid -k rotate |
Create cache directories (first run, new cache_dir) | squid -z |
| Runtime statistics | curl -s http://localhost:3128/squid-internal-mgr/info |
| Requests in flight | curl -s http://localhost:3128/squid-internal-mgr/active_requests |
| Test a URL through the proxy | curl -x http://localhost:3128 -sI https://example.com |
| Recent denials | grep TCP_DENIED /var/log/squid/access.log | tail |
| Purge one cached object | curl -s -X PURGE -x http://localhost:3128 http://example.com/file |
| Trace ACL decisions | debug_options ALL,1 28,3 then read cache.log |
squidclient was removed in Squid 7
The cache manager answers plain HTTP requests at /squid-internal-mgr/<page> on the proxy port. Use curl for every mgr: page older guides fetch with squidclient. The cache_object:// URL scheme stopped working after Squid 6.5. See the cache manager docs.
First checks for a failing proxy#
squid -k parse # config errors, exits non-zero on failure
systemctl status squid # running, last start error
journalctl -u squid -n 50 --no-pager # startup and reconfigure messages
curl -x http://localhost:3128 -sI https://example.com # end-to-end from the proxy host
tail -n 20 /var/log/squid/access.log # what Squid decided for each requestEach access.log line carries a result code such as TCP_MISS/200 or TCP_DENIED/403. A TCP_DENIED means an http_access rule refused the request. A /5xx with a non-denied tag means Squid accepted it but could not reach the origin (DNS, routing, parent proxy). No line at all means the request never reached Squid (client proxy settings, firewall). See systemd if the service itself will not start.
How a request is decided#
Squid evaluates http_access lines top to bottom and stops at the first line whose ACLs all match. The action on that line (allow or deny) is final, so an allow placed after a matching deny never runs. ACL names on one line are ANDed; separate lines are ORed. Prefix an ACL name with ! to negate it.
acl localnet src 10.0.0.0/8
acl SSL_ports port 443
acl Safe_ports port 80 443 21 70 210 1025-65535
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localnet
http_access deny allIf no line matches, Squid applies the opposite of the last line’s action. A list ending in allow localnet therefore denies everything else, while a list ending in a deny allows everything else. Always end with an explicit http_access deny all so the default never depends on the last rule.
To see which rule matched, raise debug section 28 (access control) and read cache.log. Higher levels log more of each ACL check; 9 is the most verbose.
debug_options ALL,1 28,3squid -k reconfigure
tail -f /var/log/squid/cache.log | grep -E 'ACL|checking'Revert debug_options afterwards: high levels grow cache.log quickly under load.
ACL types#
| Type | Matches | Example |
|---|---|---|
src / dst | Client / server IP address or range | acl office src 192.0.2.0/24 |
dstdomain | Destination host name | acl allowed dstdomain .example.com |
dstdom_regex | Host name regex | acl ads dstdom_regex -i (^|\.)ads?\. |
url_regex | Full URL regex | acl media url_regex -i \.(mp4|iso)$ |
port | Destination port | acl SSL_ports port 443 |
method | HTTP method | acl CONNECT method CONNECT |
time | Day and time window | acl work time MTWHF 08:00-18:00 |
proxy_auth | Authenticated user name | acl users proxy_auth REQUIRED |
maxconn | Client concurrent connections above a limit | acl heavy maxconn 20 |
req_mime_type | Request Content-Type | acl upload req_mime_type -i ^multipart/form-data |
A leading dot in dstdomain matches the domain and all subdomains; without the dot the match is exact. For CONNECT requests Squid only sees the host name and port, so url_regex cannot match paths inside HTTPS. Keep long lists in files, one entry per line:
acl allowed dstdomain "/etc/squid/allowed-domains.txt"Regex ACLs run on every request and are slower than dstdomain. Prefer dstdomain where a domain list does the job.
Minimal working configuration#
A forward proxy for a private network, with caching and no authentication.
http_port 3128
visible_hostname proxy.example.com
acl localnet src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
acl SSL_ports port 443
acl Safe_ports port 80 443
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localnet
http_access deny all
cache_dir ufs /var/spool/squid 10000 16 256 # 10000 MB, 16 first-level, 256 second-level dirs
maximum_object_size 512 MB
cache_mem 512 MB
coredump_dir /var/spool/squid
access_log daemon:/var/log/squid/access.log squid
logfile_rotate 7
forwarded_for delete # drop X-Forwarded-For so internal client IPs do not leak upstream
via off # drop the Via headersquid -z # create cache_dir structure; run once with Squid stopped
systemctl enable --now squid # needs root
squid -k parse && squid -k reconfigure # after every changesquid -k reconfigure re-reads the config in the running process. Existing tunnels normally survive. Changing workers or the cache_dir layout needs a full restart (and squid -z for a new cache_dir).
Caching#
Squid follows origin cache headers. Cache-Control: no-store or private stops storage. A response with no Expires, max-age or Last-Modified gets heuristic freshness from refresh_pattern, which only applies when the origin gave no explicit freshness (unless an override option is set).
# regex min(min) percent max(min) options
refresh_pattern -i \.(deb|rpm|whl|tgz)$ 10080 90% 43200 override-expire
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern . 0 20% 4320override-expire and similar options break HTTP semantics for the matched URLs. Use them only for content you know is immutable, such as versioned package files.
HTTPS through CONNECT is an opaque TCP tunnel. Squid sees the host name and byte counts, caches nothing, and logs TCP_TUNNEL. Caching package mirrors therefore needs plain-HTTP mirrors, or SSL bumping (see below).
| Log tag | Meaning |
|---|---|
TCP_HIT | Served from disk cache |
TCP_MEM_HIT | Served from memory cache |
TCP_REFRESH_UNMODIFIED | Revalidated with the origin, which returned 304 |
TCP_REFRESH_MODIFIED | Revalidated, origin sent new content |
TCP_MISS | Fetched from origin (or parent) |
TCP_DENIED | Refused by http_access or another access list |
TCP_TUNNEL | CONNECT tunnel; contents never cacheable |
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'hit ratio|StoreEntries|Storage'
curl -s http://localhost:3128/squid-internal-mgr/storedir | grep -E 'Maximum|Current'Purging needs an ACL for the PURGE method, placed before deny all. Without it Squid answers 403.
acl PURGE method PURGE
http_access allow localhost PURGE
http_access deny PURGEcurl -s -o /dev/null -w '%{http_code}\n' -X PURGE -x http://localhost:3128 http://example.com/file200A 404 means the object was not cached.
Authentication#
auth_param basic program /usr/lib64/squid/basic_ncsa_auth /etc/squid/passwd
auth_param basic children 20
auth_param basic realm Proxy
auth_param basic credentialsttl 2 hours
acl authenticated proxy_auth REQUIRED
http_access allow authenticatedThe helper path is distribution-specific: /usr/lib64/squid/ on Fedora and RHEL, /usr/lib/squid/ on Debian and Ubuntu.
htpasswd -c /etc/squid/passwd alice # -c creates or overwrites the file; omit it to add usersThe proxy_auth ACL triggers a 407 Proxy Authentication Required challenge when it is evaluated and no credentials are present. Put it after rules for unauthenticated traffic you want to allow, or those clients get prompted too.
Basic authentication sends the password base64-encoded on every request. Accept it only on a trusted network or with TLS between client and proxy (https_port). For directory accounts use basic_ldap_auth; on domain-joined clients negotiate_kerberos_auth avoids sending passwords at all.
Transparent and reverse modes#
# Intercept: the router or nftables redirects port 80 to 3129; clients have no proxy setting
http_port 3129 intercept
# Reverse proxy (accelerator) in front of one origin
http_port 80 accel defaultsite=app.example.com
cache_peer 192.0.2.5 parent 8080 0 no-query originserver name=app
acl our_sites dstdomain app.example.com
http_access allow our_sites
cache_peer_access app allow our_sites
cache_peer_access app deny allKeep a separate, non-intercept http_port 3128 for clients that set the proxy explicitly. Intercept ports disable proxy authentication and expect NAT-redirected traffic.
Inspecting HTTPS needs SSL bump: Squid terminates TLS with certificates generated from a CA that clients trust, then opens its own TLS connection to the origin. peek and splice let Squid read the SNI and pass a connection through without decrypting it, which is how you exempt categories.
SSL bump decrypts user traffic
Get written authorisation and legal review before deploying it, and splice (do not bump) banking, health and other sensitive categories. Certificate-pinned applications break when bumped. Configuration details are version-specific: follow the SslPeekAndSplice page for your release. See TLS for how the generated chain is validated.
Upstream proxies#
cache_peer upstream.example.com parent 3128 0 no-query default
acl internal dstdomain .internal.example.com
never_direct deny internal # internal hosts may go direct
never_direct allow all # everything else must use a parentWithout never_direct, Squid is free to connect to origins directly, for example when the parent is down. That leaks connections in a network that only permits egress through the parent. always_direct allow <acl> does the reverse and makes matching requests skip all peers. Confirm with the hierarchy field in access.log: FIRSTUP_PARENT/ or DEFAULT_PARENT/ means the parent was used, HIER_DIRECT/ means it was not.
awk '{print $9}' /var/log/squid/access.log | cut -d/ -f1 | sort | uniq -c 8123 DEFAULT_PARENT
212 HIER_DIRECT
40 HIER_NONEHIER_NONE is normal for denials and cache hits.
Logs and monitoring#
The native squid log format has these fields, which the one-liners below rely on:
| Field | Content |
|---|---|
$1 | Timestamp (Unix seconds with milliseconds) |
$2 | Elapsed time in ms |
$3 | Client IP |
$4 | Result code / HTTP status, e.g. TCP_MISS/200 |
$5 | Bytes sent to client |
$6 | Method |
$7 | URL (host:port for CONNECT) |
$8 | User name or - |
$9 | Hierarchy code / peer, e.g. HIER_DIRECT/203.0.113.10 |
$10 | Content type |
A custom logformat changes these positions. To keep the native format and add a second log for another tool, add a second access_log line with a different format name.
curl -s http://localhost:3128/squid-internal-mgr/info # uptime, hit ratios, FD usage
curl -s http://localhost:3128/squid-internal-mgr/5min | grep -E 'client_http|server_http'
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'file desc'Alert on the denied-request ratio, file descriptor usage and cache disk fullness. A Prometheus exporter such as squid-exporter can scrape the cache manager; see Prometheus. Running out of file descriptors shows as random connection failures under load.
Scripts#
A health check suitable for a monitoring probe or a systemd ExecStartPost. It exits non-zero when the config is invalid, the process is down, or a canary URL fails through the proxy.
#!/usr/bin/env bash
set -euo pipefail
proxy=${1:-http://localhost:3128}
canary=${2:-https://example.com}
squid -k parse >/dev/null 2>&1 || { echo "config invalid"; exit 1; }
systemctl is-active --quiet squid || { echo "squid not running"; exit 1; }
code=$(curl -x "$proxy" -o /dev/null -sw '%{http_code}' --max-time 10 "$canary" || true)
[[ $code =~ ^(200|204|301|302)$ ]] || { echo "canary failed: HTTP $code"; exit 1; }
fd=$(curl -s "$proxy/squid-internal-mgr/info" | awk -F'\t' '/Number of file desc.*avail/ {print $2}')
echo "ok: canary $code, fds available $fd"An access.log summary for the last N minutes: request rate, hit ratio, denial rate and the top offenders. Run it from cron and pipe the output into a chat webhook or a report.
#!/usr/bin/env bash
set -euo pipefail
log=${1:-/var/log/squid/access.log}
mins=${2:-15}
since=$(( $(date +%s) - mins*60 ))
awk -v s="$since" '$1 >= s {
n++; bytes += $5
if ($4 ~ /HIT/) hit++
if ($4 ~ /DENIED/) deny++
host=$7; sub(/^[a-z]+:\/\//,"",host); sub(/[:\/].*/,"",host); dst[host]++
cli[$3]++
}
END {
if (!n) { print "no requests in window"; exit }
printf "requests: %d (%.1f/s)\n", n, n/(60*'"$mins"')
printf "hit ratio: %.1f%% denied: %.1f%% traffic: %.1f MB\n", 100*hit/n, 100*deny/n, bytes/1048576
print "top clients:"; for (c in cli) print " " cli[c], c | "sort -rn | head -5"
close("sort -rn | head -5")
print "top destinations:"; for (d in dst) print " " dst[d], d | "sort -rn | head -5"
}' "$log"A safe bulk purge: read a list of URLs and issue PURGE for each, reporting which were cached (200) and which were not (404). Requires the PURGE method ACL shown above.
#!/usr/bin/env bash
set -euo pipefail
proxy=${PROXY:-http://localhost:3128}
while IFS= read -r url; do
[[ -z $url || $url == \#* ]] && continue
code=$(curl -s -o /dev/null -w '%{http_code}' -X PURGE -x "$proxy" "$url")
printf '%s %s\n' "$code" "$url"
done < "${1:-urls.txt}"Troubleshooting#
| Symptom | Likely cause | Check |
|---|---|---|
| Everything denied | An earlier deny matched, or the implicit default denied | TCP_DENIED lines; debug_options 28,3 |
| HTTPS fails, HTTP works | CONNECT denied by SSL_ports / Safe_ports | Log shows TCP_DENIED/403 ... CONNECT host:443 |
| Clients prompted for a password unexpectedly | proxy_auth ACL evaluated before an allow rule | Order of http_access lines |
| Works by IP, fails by name | Squid’s resolver cannot resolve | dns_nameservers; cache.log DNS errors; DNS |
| Slow or failing on dual-stack hosts | Broken IPv6 path on the proxy; dns_v4_first was removed in Squid 5 | connect_timeout; IPv6 routing on the proxy |
| Slow first byte | DNS, parent proxy or origin latency, not caching | $2 elapsed times; HTTP timing |
Too many open files | FD limit | Raise LimitNOFILE in the unit and max_filedescriptors |
| Cache never hits | Origin sends no-store / private, or traffic is all TCP_TUNNEL | Result codes in access.log |
PURGE returns 403 | No PURGE method ACL allowed | See the purge ACL above |
| Reconfigure had no effect | Config error, or a directive that needs a restart | squid -k parse; journalctl -u squid |
| Intercepted traffic loops or 4xx | Explicit-proxy clients pointed at the intercept port, or NAT lookup failed | Separate ports; cache.log |
| Disk cache never fills | cache_dir too small, or objects exceed maximum_object_size | storedir mgr page; raise the size ceilings |
| High latency only on cache misses | Slow origin or parent, or ufs storage stalling the main thread | Elapsed times on MISS lines; switch to aufs/rock |
WARNING: swapfile ... not found in cache.log | Cache index and disk disagree after an unclean stop | Stop Squid, squid -z, restart; corruption clears on rebuild |
commBind: Cannot bind socket ... Address already in use | Another process on http_port, or a previous Squid still running | ss -ltnp sport = :3128; kill the stale process |
| Client gets the wrong site’s certificate | SSL bump generating a cert for the default site, or wrong SNI peek | cache.log at debug_options 83,3; check ssl_bump peek order |
refresh_pattern ignored | An earlier pattern matched first, or the origin sent explicit freshness | Rules are first-match top to bottom; check origin Cache-Control |
Oneliners#
These assume the native squid log format.
# Result codes by count
awk '{print $4}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Top destination hosts
awk '{print $7}' /var/log/squid/access.log | sed -E 's#^[a-z]+://##; s#[:/].*##' | sort | uniq -c | sort -rn | head
# Top clients by request count
awk '{print $3}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Bytes served per client
awk '{b[$3]+=$5} END {for (c in b) printf "%12d %s\n", b[c], c}' /var/log/squid/access.log | sort -rn | head
# Denied requests with client and URL
awk '$4 ~ /DENIED/ {print $3, $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Hit ratio from the log
awk '{t++; if ($4 ~ /HIT/) h++} END {printf "%.1f%% of %d\n", 100*h/t, t}' /var/log/squid/access.log
# Requests per minute (strftime needs gawk)
gawk '{print strftime("%H:%M", $1)}' /var/log/squid/access.log | uniq -c | tail -20
# Status and total time for a site through the proxy, from a client
curl -x http://proxy.example.com:3128 -o /dev/null -sw '%{http_code} %{time_total}\n' https://example.com
# Show the running http_access rules (the "config" page is refused unless cachemgr_passwd allows it)
curl -s http://localhost:3128/squid-internal-mgr/config | grep -E '^http_access'
# Validate then reload
squid -k parse && squid -k reconfigure
# Slowest requests: elapsed time, status, URL
sort -k2 -rn /var/log/squid/access.log | awk '{print $2, $4, $7}' | head
# Bandwidth per destination host (bytes sent to client)
awk '{h=$7; sub(/^[a-z]+:\/\//,"",h); sub(/[:\/].*/,"",h); b[h]+=$5} END {for (x in b) printf "%12d %s\n", b[x], x}' /var/log/squid/access.log | sort -rn | head
# CONNECT (HTTPS) targets, which never cache
awk '$6=="CONNECT" {print $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Requests per authenticated user
awk '$8!="-" {print $8}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Average and max elapsed time in ms
awk '{s+=$2; if ($2>m) m=$2} END {printf "avg=%.0f max=%d n=%d\n", s/NR, m, NR}' /var/log/squid/access.log
# 5xx responses with the origin and hierarchy code
awk '$4 ~ /\/5[0-9][0-9]$/ {print $4, $9, $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head
# Cache hit ratio for a single content type (e.g. images)
awk '$10 ~ /^image/ {t++; if ($4 ~ /HIT/) h++} END {if (t) printf "%.1f%% of %d\n", 100*h/t, t}' /var/log/squid/access.log
# File descriptor usage right now
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'file descriptor|Number of file'
# Live memory and disk cache utilisation
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'Storage (Mem|Swap) size|Maximum'
# Median object size served (needs numeric sort of bytes column)
awk '$5 ~ /^[0-9]+$/ {print $5}' /var/log/squid/access.log | sort -n | awk '{a[NR]=$1} END {print a[int(NR/2)]}'
# Clients exceeding a request-rate threshold in the last 10000 lines
tail -10000 /var/log/squid/access.log | awk '{c[$3]++} END {for (x in c) if (c[x] > 500) print c[x], x}' | sort -rn
# Which parent handled each miss
awk '$4 ~ /MISS/ {print $9}' /var/log/squid/access.log | cut -d/ -f1 | sort | uniq -c | sort -rn
# Open the cache manager menu (lists every mgr page available)
curl -s http://localhost:3128/squid-internal-mgr/menu
# Confirm the running binary and build options
squid -v | head -3Cache tuning#
Cache effectiveness depends on object size limits, the split between memory and disk, and the replacement policy. The defaults are conservative; raise them deliberately for a caching proxy that fronts package mirrors or large static assets.
cache_mem 2048 MB # in-memory hot objects and in-transit data
maximum_object_size_in_memory 1 MB # objects larger than this never sit in cache_mem
maximum_object_size 4 GB # ceiling for a single cached object on disk
minimum_object_size 0 KB
cache_replacement_policy heap LFUDA # keep popular objects; better hit ratio than the default LRU for mixed sizes
memory_replacement_policy heap GDSF # favour keeping many small objects in RAM
cache_swap_low 90 # start replacement at 90% full
cache_swap_high 95 # replace aggressively above 95%heap LFUDA (Least Frequently Used with Dynamic Ageing) maximises byte hit ratio by keeping popular objects regardless of size; heap GDSF (Greedy Dual Size Frequency) maximises object hit ratio by preferring small objects. Use LFUDA on disk where bandwidth saving matters and GDSF in memory where request-count hits matter. Both need Squid built with --enable-removal-policies, which every distribution package enables.
For a large disk cache prefer rock storage or aufs over the classic ufs. ufs performs cache I/O on the main thread and stalls it; aufs uses threads, and rock packs small objects into a database file with far less filesystem overhead.
cache_dir rock /var/spool/squid/rock 20000 max-size=32768 # small objects, one file, low overhead
cache_dir aufs /var/spool/squid/aufs 40000 16 256 min-size=32769 # larger objects, threaded I/ORun squid -z after adding or resizing any cache_dir, with Squid stopped, or it will not create the directory structure and logs Failed to verify one of the swap directories.
Further reading#
- Squid configuration directives — every directive with its default and version
- Squid wiki: cache manager and SslPeekAndSplice
- Squid release notes — behaviour changes and removed directives per major version