Why do your server logs and your analytics disagree?
Because they are counting in different places. Your analytics script runs in the visitor's browser. Your server log records requests that reach your origin. A cache sits between the two, and every request it answers is a visit one system sees and the other never hears about.
This trips people up constantly, and the usual reaction is to assume one tool is broken. Neither is. They are measuring different events, and once you know which event each one captures, the gap stops being alarming and becomes informative.
It matters because people make decisions on these numbers. If you are about to delete a page because the server log says nobody requests it, you should know what the cache is doing first.
What is actually sitting between your server and your visitor?
Usually several layers. The browser keeps its own cache. There may be a corporate proxy. There is almost certainly a content delivery network in front of your site. MDN describes an HTTP cache simply as an implementation that holds requests and responses for reusing in subsequent requests.
Each of those layers can answer a request without passing it further back. That is the entire point of them, and it is why your site is fast. The side effect is that your origin server stops being a complete record of who visited.
The distinction that matters is between a private cache serving one person and a shared cache serving many. MDN notes that Cache-Control directives control caching in browsers and in shared caches such as proxies and CDNs. A shared cache can answer thousands of people from one origin fetch.
Which visits does your origin server never see?
Any visit the cache can satisfy on its own. If a page is cached at the edge and a visitor requests it during that window, the edge answers and your origin records nothing. Ten thousand readers can arrive through one origin request.
This is why raw origin logs are close to useless for measuring popular content on a cached site. The most popular page can look quiet, because popularity is exactly what makes a page likely to be served from cache rather than fetched again.
It also explains a pattern that confuses people during a traffic spike. Origin requests stay flat while the site is clearly busy. Nothing is wrong. The cache is absorbing the spike, which is what you are paying it to do.
Does a cached page still run your analytics script?
Yes. The cache stores the HTML response, not the outcome of running it. When that stored HTML reaches a browser, the browser parses it and executes the scripts inside exactly as it would for a freshly generated response, so your client-side beacon fires normally.
This is the key asymmetry. Caching hides visits from your server and hides almost nothing from your client-side analytics, whether that is Google Analytics, Plausible or something you wrote yourself. So the two systems do not drift by a random amount, they drift in a predictable direction.
The exception is anything that stops the script running: a blocker, JavaScript disabled, a visitor leaving before the script loads, or a client that never executes scripts at all. Those are missing from analytics and often present in your logs, which is the same gap running the other way.
What does Cache-Control actually control?
It is the header that tells caches what they may store and for how long. MDN documents that it carries directives in both requests and responses, and lists the standard ones, including max-age, s-maxage, no-cache, no-store, private, public, immutable, must-revalidate and stale-while-revalidate.
Two practical details from that documentation are worth carrying around. Directives are case-insensitive, though MDN recommends lowercase because some implementations do not recognise uppercase. And multiple directives are permitted, comma separated, so a single header can say more than one thing.
The directive names are not interchangeable and the differences are the whole game. MDN's table separates request directives from response directives, and lists s-maxage next to max-age, which is your clue that these are not synonyms. Read each directive's definition in that documentation before you set it, because the names look similar and the behaviours are not. Getting them wrong is how sites end up serving stale pages from the edge long after an update.
Why do bots distort the two numbers differently?
Because most bots request the page and never execute the JavaScript. That puts them squarely in your server logs and largely absent from your client-side analytics, which is the opposite of the cache effect and can cancel it out coincidentally.
That coincidence is dangerous, because it can make two wrong numbers look reassuringly similar. A log inflated by crawlers and an analytics figure depressed by caching can land close together and tell you nothing true about human traffic.
It also means you cannot interpret either number without knowing what share is automated. Crawler volume is not noise you can wave away, particularly now that many sites are being fetched by systems that were not visiting at all a couple of years ago.
Which number should you trust for which decision?
Use client-side analytics for anything about people: which pages get read, how visitors move through the site, what converts. Use origin logs and crawl data for anything about machines: what is being fetched, how often, and by whom. Do not use either one for both.
The mistake I see most often is judging content performance from server data on a cached site and concluding that good pages are dead. The second most common is judging crawler behaviour from client-side analytics, which cannot see most of it by design.
When the two genuinely disagree about something they both should capture, that is worth investigating rather than averaging. Working out why two tools report different numbers usually teaches you something about your stack.
How do you stop the two systems drifting apart?
You do not, and you should stop trying. The drift is structural, not a misconfiguration. What you can do is document the expected direction of the gap so that nobody on your team re-discovers it in a panic every quarter.
Write down which system measures what, roughly how large the gap has been historically, and what would count as an abnormal change. That note is worth more than any reconciliation exercise, because the useful signal is a change in the gap, not the gap itself.
Then keep your measurement setup boring. A single client-side analytics implementation you understand beats three you half trust, which is the main argument for keeping your analytics install simple rather than layering tools until nothing reconciles.
What should you do next?
Take your top page and compare what your analytics reports for it against what your origin logs show for the same week. The size and direction of that gap tells you how heavily your site is cached, which is useful to know before you trust either number again.
Then check the Cache-Control header your pages actually send, rather than the one you assume they send. It takes one request to look, and the answer frequently surprises people who have never checked.
If your numbers are not adding up and you want help working out which layer is eating them, send me what you are seeing. Reconciling this kind of thing is oddly satisfying. Let's chat.
Get found, cited and the back office automated
Let's make your site the source AI engines quote and wire up the systems behind it.
Read more blogs
Let's get your website found and cited by AI
Tell me what you're working on, whether AI search is skipping your product, your back office is buried in manual work, or you need a build that does both.