One Plugin, 900 Blogs: Aggregating Analytics Across a Multi-Multisite Fleet

Managing analytics for a single WordPress site is straightforward. Managing analytics for roughly 900 blogs spread across 25 separate WordPress multisite instances is another matter entirely. The goal here was to present a unified view of Google Analytics data inside the WordPress admin, pulling from every one of those blogs, while keeping the implementation practical and secure.

The scenario is a "multi-multisite" setup: separate WordPress multisite installations, each run independently, rather than one massive network of networks. For this project, each customer has their own multisite instance containing anywhere from one to hundreds of blogs. The task: build a plugin that sits on all 25 client sites exposing their data, and on a single dashboard site that ingests, caches, and displays that data. The final screen, based on a Figma comp, aggregates everything into a single data set for sorting by date, author, and metadata.

The REST API as a Data Backbone

WordPress's built-in REST API is the natural conduit for this kind of data sharing. Core already exposes posts, authors, and other content. By registering custom endpoints, the plugin can expose specific analytics data from each client site. The plugin defines an array of endpoint URLs, then loops through that array to register each one. Decoupling the endpoint definitions from the registration logic makes it trivial to reuse that same list elsewhere, such as passing it to JavaScript via wp_localize_script(), which bundles the URLs as a global JSON variable.

Going Async: Why JavaScript Won

The first instinct might be to handle everything in PHP. But doing so hits two walls: PHP executes synchronously, so requests to all 25 multisites would happen one after another, and production environments typically cap execution time at 60 seconds. Querying analytics for hundreds of blogs, at one to two seconds per blog, blows past that limit.

JavaScript solves the problem. Its asynchronous nature lets the dashboard site ping all 25 multisites at once, collect the list of blogs, and then fire requests to all 900 blogs concurrently. The dramatic speedup comes with a caveat: browsers limit concurrent connections per domain, and hosts enforce request rate limits. Watching Chrome's network panel suggests a ceiling of roughly 50-100 concurrent requests, and the production host started responding with 429 Too Many Requests errors. The fix was staggering requests by 150 milliseconds each. The total scrape time landed around 170 seconds — acceptable only because the results are cached, so the wait happens once per session.

Gluing PHP and JavaScript Together

Connecting PHP-registered endpoints to JavaScript calls is a common friction point. The clean bridge is wp_localize_script(), which injects endpoint URLs into the DOM as a global variable for JS to read, avoiding any side effects from re-registering routes. This keeps the architecture tidy and the two worlds in sync.

Securing the Data Flow

Security is handled in layers. First, each of the 25 client sites has an application password registered. When making Ajax calls from the dashboard site, those credentials are appended and encoded with btoa() for basic authentication.

The next hurdle is CORS. Browsers block cross-origin Ajax requests by default, so the client sites must explicitly allow requests from the dashboard site. That permission is granted narrowly, following the principle of least privilege, rather than opening the API up to any origin.

Finally, writing data back to the dashboard site requires the current user to be a logged-in WordPress admin. The REST API registration includes a permissions callback to enforce this, and the user's identity is verified with a nonce. The wpApiSettings.nonce global is available on all WordPress admin screens courtesy of core, so no extra localization is needed.

Relational Cache Over Transients

Because the user-facing dashboard requires sorting and filtering across dates, authors, and metadata, the Transients API — which stores one big blob in a single database row — feels too limiting. The amount of data and the query complexity demands a relational structure. Custom tables, designed with the help of an LLM, hold the data. The developer provided DocBlocks describing requirements, and the AI generated MySQL code that needed only one adjustment: stricter type-checking for fields that should always be numeric or date values. The takeaway is that AI worked well here as a detail filler when the architectural direction was already clear.

Initial Results and Trade-offs

An MVP is already live and gathering data. Early insights from the dashboard reveal that general-interest legal topics — food, cruise ships, germs, cannabis — attract more traffic than niche or highly specialized legal areas. Notably, smaller law firms are outperforming the largest ones in reaching broader audiences.

Looking back, jQuery handled the job despite clocking in at a couple thousand lines, though a modern framework like React would have been a reasonable alternative for managing state changes. The admin-only context made the extra library a non-issue. AWS Lambda is a tempting future path: it could fire off all 900-plus requests without browser limits and could rotate IP addresses to sidestep rate limiting. Cron is another candidate to precompile data on each client site, eliminating the initial refresh delay entirely, though debugging cron logic months later has a well-earned reputation for being painful.

No working repo or line-by-line walkthrough here. The intent was to share high-level architecture decisions for anyone managing a multi-multisite infrastructure, valuable for the edge cases most guides don't cover.