You deployed Matomo, Plausible, or Umami on a modest VPS, and everything worked fine for months. Then traffic grew. The dashboard started loading slowly, report generation began timing out, and your database ballooned past what your server could comfortably handle. This is the point where scaling self-hosted analytics becomes a real engineering task rather than a future concern.

This article is part of our complete guide to self-hosted analytics. If you are still evaluating platforms or setting up your first deployment, start there. What follows assumes you already have a working installation and need to make it handle significantly more traffic without breaking.

When Scaling Becomes Necessary

Not every performance problem requires scaling. Sometimes a slow dashboard is caused by a missing index or an unoptimized query, not insufficient hardware. Before adding servers, identify whether you are actually hitting capacity limits.

  • Dashboard load times exceed 10 seconds. If generating a simple pageview report for the last 30 days takes this long, your database is struggling with raw data volume.
  • Database size is growing faster than expected. A site with 50,000 daily pageviews should produce roughly 1-2 GB of raw data per month in Matomo. Significantly faster growth suggests missing archival processes.
  • Report generation timeouts. Matomo’s browser-triggered archiving is the most common culprit. Visitors triggering report processing on every dashboard load will overwhelm PHP workers on a busy site.
  • You consistently exceed 100,000 daily pageviews. This is where default configurations start showing strain. Below this number, a well-tuned single server handles everything.
  • CPU and memory spike during peak hours. If your server regularly hits 90% CPU or swaps memory during business hours, the analytics workload is competing with other processes.

Before optimizing, establish baselines over at least one full week: average and P95 dashboard query response time, database growth rate in megabytes per day, tracker endpoint response time, peak concurrent requests per second, and disk I/O utilization during report generation. These numbers tell you whether you have a read problem, a write problem, or both. The solutions are different for each.

Database Optimization

The database is almost always the first bottleneck. Every pageview, event, and session generates rows that accumulate quickly. Each analytics platform uses a different storage engine, so the tuning approach varies.

Matomo: Archive via Cron, Not Browser

The most impactful change for a Matomo installation is switching from browser-triggered archiving to cron-based archiving. By default, Matomo processes raw log data into aggregated reports when someone opens the dashboard. On a high-traffic site, every dashboard visit triggers expensive queries across millions of rows. Disable browser archiving in the config and set up a cron job:

[General]
browser_archiving_disabled_enforce = 1
5 * * * * /usr/bin/php /var/www/matomo/console core:archive --url=https://analytics.example.com > /dev/null 2>&1

This runs every hour. For very high-traffic sites, run it every 15 or 30 minutes. The dashboard then reads pre-computed reports instead of querying raw data on every load.

For MySQL tuning, set innodb_buffer_pool_size to about 70% of available RAM on a dedicated database server. Disable the query cache entirely since it causes contention on write-heavy workloads and was removed in MySQL 8.0. Setting innodb_flush_log_at_trx_commit to 2 trades minimal durability risk for significant write performance gains.

Plausible and Umami: PostgreSQL Tuning

Plausible CE uses PostgreSQL for configuration and ClickHouse for event data. Umami uses PostgreSQL for everything. Key PostgreSQL settings for an 8 GB server:

shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 64MB
maintenance_work_mem = 512MB
random_page_cost = 1.1
effective_io_concurrency = 200

The random_page_cost of 1.1 assumes SSD storage, which you should be using for any analytics database. Setting effective_io_concurrency to 200 tells the planner that the disk can handle many parallel requests, which is true for SSDs but not for spinning disks.

ClickHouse Performance Tuning

Plausible CE stores all event data in ClickHouse, a columnar database designed for analytical workloads. It is fast by default, but several settings are worth tuning for high-traffic self-hosted deployments.

Memory Limits

ClickHouse will consume as much memory as available by default. On a shared server, this starves other processes. Set explicit limits:

<clickhouse>
  <profiles>
    <default>
      <max_memory_usage>8000000000</max_memory_usage>
      <max_memory_usage_for_all_queries>12000000000</max_memory_usage_for_all_queries>
    </default>
  </profiles>
</clickhouse>

This caps individual queries at 8 GB and all concurrent queries at 12 GB. Always leave at least 2-3 GB for the operating system and other services.

MergeTree and Compression Settings

Plausible’s ClickHouse tables use the MergeTree engine family. Increase parts_to_delay_insert and parts_to_throw_insert from their defaults to give the merge process more headroom before throttling or rejecting inserts during traffic spikes.

For compression, ZSTD level 3 compresses analytics data to roughly 10-15% of its original size while maintaining fast decompression. Analytics data has many repeated values in columns like country codes, browser names, and paths, which ZSTD handles efficiently:

<compression>
  <case>
    <min_part_size>10000000000</min_part_size>
    <min_part_size_ratio>0.01</min_part_size_ratio>
    <method>zstd</method>
    <level>3</level>
  </case>
</compression>

CDN for the Tracking Script

Every visitor downloads the tracking JavaScript file. On a site with 500,000 daily pageviews, that is 500,000 requests just for the script, before any tracking data is sent. Serving it through a CDN eliminates that load from your origin server.

In Cloudflare, point a subdomain like t.example.com to your analytics server with the proxy enabled. Set a Cache Rule for the script path with an Edge Cache TTL of one day and Browser Cache TTL of one hour. On the origin, configure nginx to send matching headers:

location ~* /js/.*\.js$ {
    add_header Cache-Control "public, max-age=3600, s-maxage=86400";
    add_header Vary "Accept-Encoding";
}

The one-hour browser cache ensures script updates propagate quickly without requiring a CDN cache purge.

Custom Subdomain for Ad-Blocker Bypass

Many ad blockers maintain filter lists that block requests to known analytics domains and script paths. Using a custom subdomain on your own domain with a non-standard script path reduces false positives from broad lists that incidentally block self-hosted analytics alongside advertising trackers. Configure a reverse proxy that maps a neutral path to the actual tracking script:

location /api/v1/resource.js {
    proxy_pass https://analytics-internal.example.com/js/script.js;
    proxy_set_header Host analytics-internal.example.com;
}

Data Retention and Archiving

A site with 200,000 daily pageviews generates roughly 50-70 million rows per year in raw event tables. You need a retention strategy that keeps recent data fast while preserving historical trends.

Matomo distinguishes between raw logs and aggregated archives. You can delete raw logs after a few months while keeping aggregated reports indefinitely. The reports contain everything needed for trend analysis without the storage cost of individual records:

[Deletelogs]
delete_logs_enable = 1
delete_logs_older_than = 180
delete_logs_schedule_lowest_interval = 7

Umami has no built-in retention mechanism. Run a monthly cron job that deletes old rows from the website_event and session tables, then run VACUUM ANALYZE to reclaim disk space. Plausible CE handles retention through ClickHouse’s built-in TTL mechanism, which you can configure at the table level.

PostgreSQL Partitioning with pg_partman

For PostgreSQL-backed tools, pg_partman automates time-based partitioning. Instead of one massive table, data splits into monthly partitions. Queries for recent dates only scan recent partitions, and dropping a month of data is an instant metadata operation rather than a slow DELETE across millions of rows:

SELECT partman.create_parent(
    p_parent_table := 'public.website_event',
    p_control := 'created_at',
    p_type := 'native',
    p_interval := '1 month',
    p_premake := 3
);

Horizontal Scaling Strategies

When a single server is no longer enough, split the workload across multiple machines. The analytics pipeline has two distinct loads: ingesting tracking data (writes) and serving dashboard queries (reads). These should be scaled independently.

Separating Ingestion from Reporting

Run separate server groups for the tracking endpoint and the dashboard. Both connect to the same database, but tracking servers handle writes while dashboard servers handle reads. For Matomo, this means running matomo.php on one set of servers and the web interface on another. For Plausible, the /api/event endpoint handles ingestion while the web UI handles reporting.

Put a load balancer in front of tracking servers. The tracker is stateless, so round-robin distribution works:

upstream tracker_backends {
    server tracker1.internal:8000;
    server tracker2.internal:8000;
}

server {
    listen 443 ssl;
    server_name t.example.com;
    location /api/event {
        proxy_pass http://tracker_backends;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Read Replicas and Container Orchestration

For PostgreSQL-backed systems, add read replicas using streaming replication and point dashboard queries at the replica. This offloads expensive analytical queries from the server handling real-time ingestion.

If you already use Docker for Plausible or Umami, Docker Swarm provides straightforward multi-instance scaling. Kubernetes offers more control but adds significant operational overhead. Unless you already run a cluster, Docker Swarm or a simple load balancer across VPS instances is the pragmatic choice.

Monitoring Your Analytics Stack

An analytics platform that silently drops data is worse than no analytics at all. Grafana and Prometheus are the standard tools for this job. For a deeper dive into visualization, see our guide on building a custom analytics dashboard with Grafana and Matomo.

Key metrics to track: database query time (alert if P95 exceeds 5 seconds), ingestion rate (a sudden drop means problems), disk usage (alert at 80% capacity), memory usage (critical for ClickHouse), and HTTP error rate on the tracking endpoint.

A minimal monitoring stack alongside your analytics deployment:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
  postgres-exporter:
    image: prometheuscommunity/postgres-exporter:latest
    environment:
      - DATA_SOURCE_NAME=postgresql://user:pass@db:5432/analytics?sslmode=disable
    ports:
      - "9187:9187"

Add Prometheus alerting rules for long-running queries, low disk space (below 20%), and HTTP error rates above 1%. Import community Grafana dashboards for node exporter (ID 1860) and PostgreSQL (ID 9628) to get comprehensive monitoring with minimal configuration time.

When to Move to Cloud

There is a crossover point where engineering time maintaining infrastructure exceeds managed service costs. Calculate your true self-hosted cost by adding server fees, engineering hours for updates and maintenance valued at your actual hourly rate, opportunity cost of what else your team could build, and the risk cost of potential data loss.

Compare against Plausible Cloud (from $9/month for 10K pageviews, scaling to $99/month for 1M), Matomo Cloud (from $23/month), or Umami Cloud (free tier up to 100K events). For many small to medium sites, managed hosting becomes cheaper once you factor in engineering time honestly.

A hybrid approach splits the difference: run data ingestion on your own infrastructure for full control over raw data collection, but use a managed service or cloud database for the reporting layer. This keeps visitor data within your infrastructure while offloading expensive dashboard queries to external compute resources.

Bottom Line

Scaling self-hosted analytics is a progression, not a single project. Start with database optimization and cron-based archiving, which alone can extend a single server’s capacity by an order of magnitude. Add a CDN for the tracking script and implement data retention before considering additional servers. When you do scale horizontally, separate ingestion from reporting first since these workloads have fundamentally different resource profiles.

Monitor everything from day one. A Grafana and Prometheus stack takes an hour to deploy and saves you from discovering problems through missing data weeks later. And be honest about the crossover point where self-hosting stops making economic sense. The goal is accurate, privacy-respecting analytics, not infrastructure for its own sake.

For platform-specific setup instructions, see our guides on Matomo self-hosted setup, Umami Docker deployment, and Plausible CE self-hosted deployment.