How Server-Side Pageview Tracking Works: Beacons, Edge Collection, and Accuracy
EngineeringOctober 3, 202612 min read

How Server-Side Pageview Tracking Works: Beacons, Edge Collection, and Accuracy

Your tracking pixel just got blocked by uBlock Origin. Again. Here's what happens when you move pageview collection server-side—from Beacon API mechanics to IP-to-region resolution without storing IPs.

I opened the analytics dashboard last Tuesday expecting a normal traffic day. Instead, I saw a 34% drop. Not gradual—cliff edge. The site hadn't gone down. Conversions were fine. Users were clearly visiting.

Turned out uBlock Origin had pushed an update that caught our analytics script. Just ours. Specifically. Someone had submitted our CDN pattern to EasyList and boom—a third of our traffic vanished from the dashboard overnight.

That's when I finally committed to understanding server-side tracking properly. Not just "use it because client-side gets blocked," but the actual mechanics. How beacons work. How edge collectors process requests. How you resolve IP addresses to locations without actually storing those IPs (which would tank your GDPR compliance). The whole chain from browser to dashboard.

What and Why

Server-side pageview tracking moves data collection from the browser to your backend infrastructure. Instead of loading a third-party script that fires requests to an external analytics domain, you send pageview data to your own server—usually as a lightweight beacon—and your server forwards or processes it.

Why does this matter right now? Ad blockers are winning. Brave blocks trackers by default. Safari's Intelligent Tracking Prevention nukes cross-site cookies. Firefox has Enhanced Tracking Protection. Chrome's "Privacy Sandbox" keeps adding restrictions. A study from PageFair (now Blockthrough) found 42% of users run some form of ad blocking as of late 2025. That number keeps climbing.

If a third of your traffic is invisible, your analytics are fiction.

Conversion rates skew high because you're only counting users who don't block. Geographic distribution gets weird because ad blocking adoption varies by country—Germany runs heavier than the US, tech audiences heavier than general consumer. Your data lies to you by omission. I've seen teams make six-figure marketing decisions based on dashboards that were missing 40% of their actual traffic. That's not analytics. That's expensive guesswork. If you're running paid ads, this blind spot also compounds with click fraud detection challenges—you can't spot bots you can't even count.

Server-side tracking fixes this. It also unlocks better accuracy for other reasons—like capturing pageviews during navigation events that would otherwise get cancelled.

Core Concepts

The Beacon API

Traditional analytics scripts use XHR or fetch() to send data. Problem: when users navigate away or close the tab, the browser cancels pending requests. Your pageview never arrives.

The Beacon API (navigator.sendBeacon()) solves this. It queues a small POST request that the browser guarantees to send, even after your JavaScript has stopped executing. Fire-and-forget. The browser handles transmission in the background, potentially after the page has unloaded.

// Basic beacon usage
navigator.sendBeacon('/api/collect', JSON.stringify({
  event: 'pageview',
  url: location.pathname,
  referrer: document.referrer,
  timestamp: Date.now()
}))

Beacons are limited to around 64KB per payload—plenty for analytics events, not suitable for large uploads. They're also one-way: no response handling. You can't know if the server received it.

That bothered me at first. Coming from request-response patterns, the idea of "fire and hope" felt wrong. But honestly? You're aggregating millions of events. A few dropped beacons won't move your numbers. And the reliability gain from surviving page unloads more than compensates.

First-Party Collection Endpoints

The "server-side" in server-side tracking means your server, not Cloudflare's or Google's or Plausible's. The browser sends data to yourdomain.com/api/collect or similar. From the browser's perspective, this is a same-origin request to your application backend. Same domain, same cookies, same trust level.

Ad blockers cannot distinguish this from any other API request your app makes. Blocking yourdomain.com would break your entire application. So they don't.

This is why first-party data collection survives. The analytics payload is indistinguishable from legitimate application traffic because it IS legitimate application traffic—just traffic that happens to contain pageview data.

Edge Collection

Running collection logic at the edge (Cloudflare Workers, Vercel Edge Functions, AWS Lambda@Edge, Fastly Compute) lets you process requests close to users. Sub-50ms latency regardless of where your origin servers live.

More importantly for privacy: edge functions can resolve IP addresses to geographic regions and discard the IP before the data ever reaches your database. The transformation happens in memory during request processing. The IP exists for a few milliseconds, gets converted to "United States, California, San Francisco," and disappears. GDPR compliant by architecture, not by policy.

I'm genuinely annoyed this wasn't the default from day one. The technology existed. We just collectively decided to store IPs "because we might need them" and then spent years dealing with compliance headaches.

IP-to-Region Resolution Without Storage

Here's how this actually works. The edge function receives the HTTP request. The request includes the source IP (inevitable—that's how TCP/IP works). The function calls a geolocation lookup—MaxMind's GeoIP database is the standard, but Cloudflare and Vercel provide built-in geo headers.

// Cloudflare Worker example
export default {
  async fetch(request) {
    const geo = request.cf  // Cloudflare provides this automatically

    const pageviewData = {
      url: new URL(request.url).pathname,
      country: geo.country,        // "US"
      region: geo.region,          // "CA"
      city: geo.city,              // "San Francisco"
      timestamp: Date.now()
      // Note: no IP address stored
    }

    // Forward to your analytics backend or queue
    await analyticsQueue.send(pageviewData)

    return new Response('ok', { status: 200 })
  }
}

The IP itself never gets logged, stored, or forwarded. The geographic data exists; the IP doesn't. You can tell a German regulator exactly which city your users came from without being able to identify those users by IP. This is the architecture that makes cookieless analytics GDPR-defensible.

For teams already dealing with compliance requirements, we covered the full GDPR session replay approach in our PII masking guide.

The Complete Process

Let's trace a pageview from browser to dashboard.

Step 1: Page Loads, Script Initializes

Your under-5KB analytics script loads. It immediately checks if navigator.sendBeacon exists (it does in every modern browser). It registers handlers for the events you care about: pageview on load, maybe scroll depth, maybe clicks on specific elements.

// Simplified initialization
(function() {
  const siteId = document.currentScript.dataset.site
  const endpoint = '/api/collect'  // Same domain

  function send(eventType, data) {
    navigator.sendBeacon(endpoint, JSON.stringify({
      site: siteId,
      type: eventType,
      ...data,
      ts: Date.now()
    }))
  }

  // Fire pageview immediately
  send('pageview', {
    url: location.pathname + location.search,
    ref: document.referrer,
    title: document.title
  })

  // Handle SPA navigations
  const originalPushState = history.pushState
  history.pushState = function() {
    originalPushState.apply(this, arguments)
    send('pageview', { url: location.pathname + location.search })
  }
})()

For single-page applications, you need the History API hooks. We covered that in depth in our SPA route tracking tutorial.

Step 2: Beacon Fires to Your Endpoint

The browser queues the beacon. If the user navigates away immediately—clicked a link, closed the tab—the beacon still sends. This is the key reliability improvement over traditional XHR-based tracking.

The request hits your domain. Because it's same-origin, no CORS preflight. Because you're using POST (beacons default to POST), it works with Content-Type text/plain or application/x-www-form-urlencoded, avoiding the complexity of JSON content-type headers.

Step 3: Edge Function Intercepts

Your edge function catches requests to /api/collect. It extracts the geo data from request headers or platform-provided context. It parses the beacon payload (typically JSON, though some implementations use URL-encoded data for smaller payloads).

// Vercel Edge Function example
export const config = { runtime: 'edge' }

export default async function handler(request) {
  const body = await request.json()
  const geo = request.geo  // Vercel provides this

  const event = {
    siteId: body.site,
    eventType: body.type,
    url: body.url,
    referrer: body.ref,
    title: body.title,
    timestamp: body.ts,
    country: geo?.country || 'unknown',
    region: geo?.region || 'unknown',
    city: geo?.city || 'unknown'
    // IP intentionally excluded
  }

  // Write to your analytics store
  await writeToClickHouse(event)  // or BigQuery, or Kafka, or...

  return new Response('', { status: 204 })
}

Step 4: Data Reaches Your Analytics Backend

The edge function forwards the enriched (but IP-stripped) event to your analytics database. ClickHouse, TimescaleDB, BigQuery—pick your poison. The data arrives with geographic context but no PII.

Step 5: Dashboard Aggregates and Displays

Your dashboard queries the aggregated data. Pageviews by URL. Traffic by country. Referrer breakdown. The numbers are accurate because they weren't filtered by ad blockers. They're privacy-compliant because you never stored the IPs that would constitute personal data.

If you're running error tracking alongside analytics, JustAnalytics handles both in the same pipeline. One edge endpoint, one script, correlated errors and funnel data.

Advanced Tips

Fingerprint-free visitor counting. You can approximate unique visitors without cookies or fingerprinting by hashing daily salted combinations of user-agent and IP at the edge, then discarding the IP. The hash identifies "probably the same browser" for deduplication within a 24-hour window without being reversible to a specific user. This is the approach Plausible and Fathom use. Is it perfect? No. Users switching networks mid-day get double-counted. But it's accurate enough for most decisions, and privacy-respecting by default. I'll take that tradeoff.

Batch beacons for high-frequency events. If you're tracking scroll depth, mouse movements, or other high-frequency events, don't fire individual beacons. Buffer events client-side and send batches every 5-10 seconds, plus a final batch on page unload. Reduces request volume by 90%+.

Service worker backup. For maximum reliability, install a service worker that catches failed beacons and retries them when connectivity returns. Overkill for most sites, but useful if you're tracking offline-capable PWAs.

A/B test server-side vs. client-side. Before full migration, run both collection methods simultaneously for a week. Compare totals. The delta is your ad-blocked traffic—useful to quantify the problem before presenting the solution to stakeholders.

Common Mistakes

Forgetting SPA route changes. Server-side collection doesn't magically track client-side navigations. If your SPA uses pushState, you still need client-side code to detect route changes and fire beacons. Server-side is about where data goes, not how it's triggered. I made this mistake on my first implementation and wondered why my React app showed one pageview per session. Embarrassing in retrospect.

Logging IPs "temporarily" in edge functions. If your edge function writes logs that include request IPs, you've defeated the privacy benefit. Check your logging configuration. Cloudflare Workers, by default, don't log request bodies or IPs unless you explicitly send them somewhere.

Sending too much data per beacon. Beacons have size limits (64KB nominal, but browsers may truncate). Keep payloads small—URL, referrer, timestamp, maybe a few custom dimensions. If you need to send more, use batching.

Blocking your own endpoints. Some overzealous security configs (CSP, WAF rules) block POST requests to non-API paths. Make sure /api/collect or whatever you call it is whitelisted.

Not handling beacon failures silently. Beacons are fire-and-forget by design. Don't wrap them in try-catch expecting to handle errors—there's nothing to catch. If delivery fails, you won't know. That's the tradeoff for guaranteed async transmission.

Assuming edge functions run in one region. Edge means many regions. If your analytics backend is in US-East and your edge function is in Sydney, you've added latency. Use an analytics backend with global write endpoints, or queue locally and batch-ship.

Look, most of these mistakes I've made personally. The "temporarily logging IPs" one cost me a weekend rewriting our pipeline and purging logs. Learn from my pain.

Frequently Asked Questions

Why does server-side tracking avoid ad blockers while client-side scripts get blocked?

Ad blockers maintain filter lists of known tracking domains and script patterns. Client-side scripts load from recognizable CDNs like googletagmanager.com or analytics.js patterns that match these lists. Server-side tracking sends data to your own domain—the same domain serving your app—which ad blockers cannot block without breaking the site itself. The payload looks like any other API request to your backend.

How does server-side tracking determine user location without storing IP addresses?

Edge functions resolve the IP to a geographic region (country, city, sometimes postal code) at request time using MaxMind or similar databases, then immediately discard the IP before logging. The final record contains "Germany, Berlin" but not the IP that produced it. This happens in memory during the request—the IP never hits disk or database.

What is the Beacon API and why is it better than XHR for analytics?

The Beacon API (navigator.sendBeacon) queues small payloads to be sent asynchronously, even if the page is unloading. XHR requests get cancelled when users navigate away, losing the pageview. Beacon guarantees delivery because the browser handles transmission after your JavaScript has stopped running. Fire-and-forget by design—you lose the ability to confirm receipt, but gain reliability when it matters most.

Does server-side tracking work for single-page applications?

Yes, but you need to fire beacons on route changes, not just initial page load. SPAs use the History API to change URLs without full page reloads. Your client-side code detects pushState/popstate events and sends a beacon for each navigation. The server-side collector receives these identically to traditional pageviews.

Resources


Try JustAnalytics

All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro $49/month ($39 annual).

Start free → · AI Command Center MCP

JP
JustAnalytics Platform TeamContributor

Author at JustAnalytics.

Related posts