# Squid

> Run and debug a Squid forward proxy: http_access rule order, ACLs, caching, authentication, parent proxies and access.log analysis.

Canonical: https://www.wiki.jodisand.me/squid/
Reviewed: 2026-09-24
Related: [HTTP and curl](https://www.wiki.jodisand.me/http/index.md), [TLS and certificates](https://www.wiki.jodisand.me/tls/index.md), [DNS](https://www.wiki.jodisand.me/dns/index.md), [systemd](https://www.wiki.jodisand.me/systemd/index.md), [Linux performance](https://www.wiki.jodisand.me/linux-performance/index.md)


## 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` |

> [!NOTE] 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](https://wiki.squid-cache.org/Features/CacheManager/Index).

## First checks for a failing proxy

```sh
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 request
```

Each 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](https://www.wiki.jodisand.me/systemd/#a-failing-service) 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.

```text
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 all
```

If 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.

```text
debug_options ALL,1 28,3
```

```sh
squid -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:

```text
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.

```text
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 header
```

```sh
squid -z                          # create cache_dir structure; run once with Squid stopped
systemctl enable --now squid      # needs root
squid -k parse && squid -k reconfigure   # after every change
```

`squid -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).

```text
# 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%   4320
```

`override-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 |

```sh
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`.

```text
acl PURGE method PURGE
http_access allow localhost PURGE
http_access deny PURGE
```

```sh
curl -s -o /dev/null -w '%{http_code}\n' -X PURGE -x http://localhost:3128 http://example.com/file
```

```text
200
```

A `404` means the object was not cached.

## Authentication

```text
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 authenticated
```

The helper path is distribution-specific: `/usr/lib64/squid/` on Fedora and RHEL, `/usr/lib/squid/` on Debian and Ubuntu.

```sh
htpasswd -c /etc/squid/passwd alice   # -c creates or overwrites the file; omit it to add users
```

The `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

```text
# 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 all
```

Keep 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.

> [!WARNING] 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](https://wiki.squid-cache.org/Features/SslPeekAndSplice) page for your release. See [TLS](https://www.wiki.jodisand.me/tls/) for how the generated chain is validated.

## Upstream proxies

```text
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 parent
```

Without `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.

```sh
awk '{print $9}' /var/log/squid/access.log | cut -d/ -f1 | sort | uniq -c
```

```text
  8123 DEFAULT_PARENT
   212 HIER_DIRECT
    40 HIER_NONE
```

`HIER_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.

```sh
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](https://www.wiki.jodisand.me/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.

```sh
#!/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.

```sh
#!/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.

```sh
#!/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](https://www.wiki.jodisand.me/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](https://www.wiki.jodisand.me/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.

```sh
# 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 -3
```

## Cache 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.

```text
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.

```text
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/O
```

Run `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](http://www.squid-cache.org/Doc/config/) — every directive with its default and version
- [Squid wiki: cache manager](https://wiki.squid-cache.org/Features/CacheManager/Index) and [SslPeekAndSplice](https://wiki.squid-cache.org/Features/SslPeekAndSplice)
- [Squid release notes](http://www.squid-cache.org/Versions/) — behaviour changes and removed directives per major version


