A Varnish cache_miss and an s_pass both result in a backend fetch, and that surface similarity is where the confusion starts. Teams see backend request rates climbing, glance at the counters, and reach for the wrong fix. A miss is a cache lookup that found nothing and will try to cache the response. A pass is a deliberate VCL decision to bypass the cache entirely via return(pass). The difference matters because the two paths have different storage behavior, different tuning levers, and different operational consequences.
The core distinction
Miss (cache_miss). A cache lookup was performed, no matching object was found, and Varnish fetches from the backend with the intent to cache the result. The response body goes to your configured cache storage (malloc or file). If the response is cacheable, it becomes a cached object and the next request for the same key produces a cache_hit.
Pass (s_pass). VCL decided this request should bypass the cache entirely. The request goes through vcl_pass, then to the backend. The response body goes to transient storage, which is unbounded by default. The response is never cached, regardless of what headers the backend sends. Every subsequent request for the same key repeats the full round trip.
The practical consequence: if you lump these together and see “high miss rates,” you might try to fix it by increasing cache size or extending TTLs. But if the traffic is actually passes (intentional or not), those fixes do nothing. You need to either fix the VCL logic causing unintentional passes, or accept that this traffic is correctly uncacheable and plan backend capacity accordingly.
How it works
Every client request enters vcl_recv, where VCL makes the routing decision. The request either takes the pass path or proceeds to vcl_hash for a cache lookup.
flowchart TD
A["Client request"] --> B{"vcl_recv decision"}
B -->|"return(pass)"| C["s_pass: bypass cache"]
B -->|"default or return(hash)"| D{"Cache lookup result"}
D -->|"valid object"| E["cache_hit"]
D -->|"nothing found"| F["cache_miss: fetch, will cache"]
D -->|"hit-for-miss obj"| G["cache_hitmiss: refetch, no coalescing"]
D -->|"hit-for-pass obj"| H["cache_hitpass: short-circuit to pass"]
F --> I["Body to cache storage"]
C --> J["Body to transient storage"]
H --> J
G --> JThe pass path
When vcl_recv returns pass, the request skips the cache lookup entirely. It proceeds to vcl_pass, then to the backend. The backend response is delivered to the client through transient storage but never inserted into the cache. The s_pass counter increments.
Common legitimate reasons to pass:
- Authenticated or personalized content where caching would serve the wrong user
- Non-idempotent methods (POST, PUT, DELETE), which the built-in VCL passes by default
- Content with
Set-Cookieheaders that VCL has decided not to strip
The problem path: a VCL bug or overly broad cookie logic causes requests that should be cached to pass instead. You see elevated s_pass, backend request rates climb, and hit ratio suffers. But the standard hit rate formula may not reveal this clearly, because passes are excluded from the denominator.
The miss path
When vcl_recv does not return pass, the request proceeds to vcl_hash to compute the cache key, then performs a cache lookup. If no matching object exists, cache_miss increments. Varnish fetches from the backend with the intent to cache. The response body goes to configured cache storage (SMA.s0 for malloc, SMF.* for file-backed). If the backend response is cacheable, the object is stored and served as a cache_hit on the next request.
Hit-for-miss and hit-for-pass: cached decisions not to cache
Varnish caches the decision that certain responses are uncacheable, so it does not waste time attempting cache lookups for them on subsequent requests. These cached decisions are called hit-for-miss and hit-for-pass objects.
When a backend response is uncacheable (TTL <= 0, Set-Cookie, Cache-Control: no-store, Vary: *, and similar), the built-in VCL in Varnish 5.0+ creates a hit-for-miss object. This object tells Varnish: for the next request with this cache key, skip the cache lookup, fetch from the backend, and do not coalesce concurrent requests. The cache_hitmiss counter tracks how many requests hit these objects.
Before Varnish 5.0, the default behavior created hit-for-pass objects instead. The cache_hitpass counter tracks hits on these. The behavioral difference matters: hit-for-pass preserves conditional request headers (If-Modified-Since, If-None-Match), so the backend can return a lightweight 304 response. Hit-for-miss strips conditional headers and fetches the full body every time.
Starting in Varnish 5.1, you can explicitly create hit-for-pass objects using return(pass(DURATION)) in vcl_backend_response. This gives you the conditional-request efficiency of hit-for-pass when you know the backend supports it.
Pipe: complete bypass
return(pipe) in VCL is a third path that bypasses Varnish’s object machinery entirely. The request and response are forwarded byte-for-byte between client and backend. Pipe is used for WebSocket upgrades and other non-HTTP-caching traffic. Piped traffic does not consume transient storage the way pass does; the data flows through socket buffers. But large pipe bodies still consume memory.
Where it shows up in production
Unintentional pass storms
The most common operational problem is VCL that unintentionally passes traffic. A typical pattern: VCL strips cookies before the cache lookup, but the stripping logic has a bug that catches requests it should not. Or an application deploy adds Set-Cookie to responses that were previously cacheable, triggering hit-for-miss objects that suppress caching for their TTL window.
Symptoms: s_pass or cache_hitmiss elevated, backend_req rate climbing, hit ratio declining. The hit rate formula cache_hit / (cache_hit + cache_miss + cache_hitpass) may understate the problem because passes are not in the denominator at all.
Transient storage growth
Every pass body goes to transient storage. Transient storage is malloc-backed and unbounded by default. If a significant fraction of your traffic is pass traffic, transient storage grows without limit.
This is the silent OOM path. The configured cache storage (say, -s malloc,8G) may be well within limits, but process RSS climbs past it as transient storage accumulates pass bodies. Eventually the OOM killer fires and Varnish dies with no warning from the cache storage counters.
You can assign transient to a sized storage: -s Transient=malloc,1G. This caps the growth but means pass traffic can fail if the cap is hit.
Hit-for-miss TTL trapping
Hit-for-miss objects have their own TTL, commonly 120s in many versions. During this window, Varnish will not attempt to cache the response, even if the application fixes the caching headers. If an application deploy temporarily sends Set-Cookie on cacheable responses, you are stuck with hit-for-miss objects until their TTLs expire. A ban or purge is needed to clear them immediately.
Tradeoffs
Pass is correct for authenticated content where the cache key cannot disambiguage users, non-idempotent methods, and responses that are truly per-user. In these cases, elevated s_pass is expected. The operational concern is backend capacity planning, not cache tuning.
Pass is a bug when content that should be cached is being passed due to VCL logic errors, application header changes, or cookie stripping that is too aggressive or not aggressive enough. The fix is always in VCL or the application, not in cache sizing or TTL tuning.
Hit-for-miss vs hit-for-pass
The built-in VCL uses hit-for-miss for uncacheable responses in Varnish 5.0+. This is the right default for most workloads because it prevents request coalescing on uncacheable content. However, for large objects that change infrequently and where the backend supports conditional requests, explicit hit-for-pass via return(pass(DURATION)) is more efficient: it preserves conditional headers and can produce 304 responses, avoiding unnecessary body transfers.
Signals to watch in production
| Signal | Why it matters | Warning sign |
|---|---|---|
MAIN.s_pass rate | Counts requests taking the pass path | Elevated rate when you expect caching |
MAIN.cache_miss rate | Counts lookups that found nothing, will fetch with intent to cache | Spike correlates with backend load increase |
MAIN.cache_hitpass rate | Counts hits on cached pass decisions | Unexpectedly high means content marked uncacheable that should not be |
MAIN.cache_hitmiss rate | Counts hits on cached miss decisions | Elevated means application sending uncacheable headers on cacheable content |
SMA.Transient.g_bytes | Memory consumed by pass, hit-for-pass, and hit-for-miss bodies | Monotonic growth indicates pass traffic accumulating without bound |
MAIN.backend_req rate | Total requests forwarded to backends | Ratio to client_req reveals cache effectiveness including pass traffic |
MAIN.n_objectcore | Object count including hit-for-miss, hit-for-pass, and busy objects | Disproportionately high relative to n_object indicates many cached non-cache decisions |
Reading the hit rate formula correctly
The standard hit rate formula is:
cache_hit / (cache_hit + cache_miss + cache_hitpass)
This formula excludes s_pass entirely. A high pass rate can mask low effective cache efficiency because passes do not appear in either the numerator or the denominator. If 40% of your traffic is pass traffic and your hit rate on the remaining 60% is 90%, your formula reports 90%, but your backend is still handling 46% of all client requests (10% misses plus 40% passes).
To get the full picture, compute backend request rate as a fraction of client request rate: backend_req / client_req. This captures the actual load ratio regardless of how that load is distributed across miss, pass, and hit-for-miss paths.
How Netdata helps
- Per-second counter collection lets you see
s_pass,cache_miss,cache_hitpass, andcache_hitmissrates change in real time, not at 10-second or 60-second resolution where pass storms appear as step functions. - Correlating
s_passwithbackend_reqon a single dashboard distinguishes “more misses because of cold cache” from “more passes because of a VCL change” without switching tools. - Transient storage tracking via
SMA.Transient.g_bytesalongside process RSS gives early warning before pass-driven memory growth triggers an OOM kill. - ML anomaly detection on hit rate, pass rate, and backend request rate surfaces the moment a VCL deploy or application change shifts traffic from the miss path to the pass path, even if absolute values have not crossed a static threshold.
n_objectcorerelative ton_objectas a derived view reveals when hit-for-miss or hit-for-pass objects are accumulating, indicating that the application is making previously cacheable content uncacheable.






