Enhancements to Fathom Lite Analytics

I’ve been self-hosting a Fathom Analytics instance for basic web stats since early 2018. That’s even before Fathom reached v1.0. The open-source version of Fathom is now called Fathom Lite and the hosted version of Fathom has been completely rewritten for serverless infrastructure with many other improvements. If you’re interested in web analytics for your own websites then check out Fathom’s hosted version. It’s a fantastic option: https://usefathom.com.

My goals for website tracking are a bit different than most. You see as a WordPress management tool I want basic stats to be built into the tool itself. Stats shouldn’t be something that needs to be thought about and it also shouldn’t conflict with other tracking methods. It should just work without any configuration.

The self hosted version of Fathom has worked amazingly well. I currently have over 1200 sites receiving around 7 million page views per month all handled by a single $5/month digital ocean droplet. This happens seamlessly in the background. As WordPress sites are added to CaptainCore there is some clever communication with the Fathom instance and a must-use WordPress plugin with tracking is injected. That’s it. Stats are set up and already collecting.

Stats tab within CaptainCore. Data is pulled from Fathom Lite.

Performance issues with self-hosted Fathom.

There are two big performance issues with Fathom Lite which appear in GTmetrix’s performance reports shown below. Requests to tracker.js and collect are not served from a CDN. Solving tracker.js is fairly easy, as that file can be hosted anywhere. However, putting the collector pixel behind a CDN is not so easy. You see for a tracker to work, it needs to be pinged every time. Putting that behind a CDN would add a caching layer preventing most of the analytics from being tracked. Let dig in and solve both of these issues.

Rewriting tracker.js and serve locally for maximum performance.

Rather than put the tracker.js behind a CDN, I decided to move it locally onto each WordPress site by expanding my must-use injection method. That way, if the WordPress site is using a CDN, the locally served tracker.js is picked up and included. If that site doesn’t have a CDN then is served locally along with the other static assets. Either way, it fully satisfies the performance scanners.🎊

While moving tracker.js locally I also rewrote it for some additional improvements:

  • Minified tracker.js
  • Bundled in self-hosted tracker URL
  • Rename global JS function from fathom to fathom_lite to prevent conflict with hosted version of Fathom
  • Switched the embed method to use a single deferred Javascript tag

The default embed method requires the following:

Fathom Lite embed method

My new improved method looks like this:

<script src="/wp-content/mu-plugins/tracker.js" data-site="ABCDEFG" defer></script>

Very clean and minimalist. You can see a template for my tracker.js file on Github. If you are self-hosting Fathom Lite then you can easily switch to this improved embed method. You will need to manually swap out the Fathom tracker URL and adjust the data-site attribute in the script tag.

Failed attempts at moving the tracking pixel to the edge.

The /collect endpoint is a 1×1 tracking pixel. That’s how Fathom reads in each visitor and pageview. My first attempt was to see what would happen if I just put it behind a CDN with KeyCDN. It worked pretty much how I thought it would. It did clear up the performance-related error however I didn’t actually do anything. Uncached requests would work just fine however cache requests weren’t tracked. I could actually trick KeyCDN to serve an uncached version of the pixel which technically worked, however, defeats the whole point of a CDN, which is to move files closer to the person requesting the file.

Google Cloud has some pretty in-depth information on serverless pixel tracking. The idea is you host a 1×1 pixel on a Google Cloud storage bucket, put a load balancing in front of it with CDN enabled then parse the access logs and do some action based on those logs. However, that just gets way too complicated for a simple self-hosted Fathom instance.

Success with Cloudflare Workers as a pixel tracker relay. ⚡

I’ve been enjoying the podcast Brian on WP where he talks about WordPress and performance. His explanation of Cloudflare Workers is great and I’ve been wanting to give them a try. I thought maybe I could put a Cloudflare Worker between the visitor and the Fathom instance. After some trials and errors, success!

The following Cloudflare Worker code listens for collection requests from the Fathom tracker script. It then immediately responds with a gif image and then relays the original collection request onto the self-hosted Fathom instance. This might seem redundant but if you understand how Cloudflare Workers operate, this is truly brilliant!

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event, event.request))

async function handleRequest(event, request) {

  url = new URL( request.url )
  if ( url.pathname != "/collect" || url.search == "" ) {
    return new Response( `Not found.`, { status: 404 } )

  // Ping Fathom tracker
    new Promise( (resolve) => {
      fetch( `https://stats.anchor.host${url.pathname}${url.search}` )

  return new Response( Uint8Array.from(atob(pixel_gif_base64), c => c.charCodeAt(0)), { status: 200, headers: { "content-type": "image/gif", "cache-control": "no-cache, must-revalidate, max-age=0" } } )


Since Cloudflare Workers are deployed at the edge, using them as my pixel tracker relay significantly improves response time. That’s especially true for folks located in a different geographical location than my Fathom instance. Meanwhile, Cloudflare handles the slow response to my Fathom instance in the background.

Huge performance gains by using Cloudflare Worker as a middleware.

I’m embedding the 1×1 pixel image directly within the Cloudflare worker however I also played around with eliminating the pixel image all together by giving an empty response like this:

return new Response( "", { status: 204, headers: { "content-type": "image/gif" } } )

That seems to work just fine and saves some bandwidth however has one disadvantage. Without actually downloading the pixel image, the tracker had no way to confirm the request completed. A challenge I’ll leave for another day. For now, I’m super happy with these few improvements for Fathom Lite.