We Designed a Monitoring Stack, Then Decided Not to Build It

The plan was a good plan. Continuous frontend performance monitoring for 30 to 40 WordPress sites: sitespeed.io runners on Hetzner Cloud, a time-series store, artifacts in object storage, Grafana on top, all of it in Terraform. Three architecture decision records worked through runner topology and sizing, the split between time series and artifacts, and a two-tier regression detector with budgets and baseline deviation.

The fourth ADR asked a question the first three had skipped: how does this compare to just buying it? The answer retired the entire build.

TL;DR: On a full-cost basis — own time priced at €800/day — the self-hosted stack runs about €4,900 in year two, of which only €1,300 is infrastructure. Grafana Cloud Synthetic Monitoring lands at €2,200–3,600. The break-even for building sits around 600 URL profiles, roughly 100 sites, two and a half times the current estate. The interesting part isn’t the number, it’s that three ADRs of careful sizing work argued over a €40/month line item while a €300/month line item sat unexamined.

What Made the Earlier Calculation Wrong?

Not arithmetic. Scope. The first three ADRs costed infrastructure, and infrastructure is the smaller half:

Eigenbau Infrastruktur    ~€110 / Monat
Eigenbau Betriebszeit     ~3 h x 100 €  =  ~€300 / Monat

Three hours a month of database, backup, and update work — a conservative figure for a stack with that many moving parts — costs nearly three times the servers it runs on. And the moment that line appears, the preceding analysis reads differently. ADR-0001 had weighed dedicated against shared vCPU in real detail. That decision was worth roughly €40 a month. It was a careful answer to a question the operating-time line renders almost irrelevant.

That’s the recurring failure mode, and it isn’t specific to monitoring: cost analysis naturally goes where the numbers are easy to look up. Server prices are on a page. Your own time isn’t on any page, so it silently gets priced at zero, and the whole comparison tilts toward whichever option consumes more of it.

The full-cost picture, both options over two years:

Eigenbau
Infrastruktur    €110 x 12                     =  €1.320
Setup            4 Tage x 800 €                =  €3.200   (einmalig)
Betrieb          3 h/Monat x 100 € x 12        =  €3.600
                                       Jahr 1  =  €8.120
                                  Folgejahre   =  €4.920

Grafana Cloud
Lizenz                                          =  €1.044 – €2.436
Setup            1 Tag x 800 €                  =    €800   (einmalig)
Betrieb          1 h/Monat x 100 € x 12         =  €1.200
                                       Jahr 1   =  €3.044 – €4.436
                                  Folgejahre    =  €2.244 – €3.636

The managed option is cheaper at the top of its range. There was no version of the spreadsheet where building won.

Where Is the Break-Even, Exactly?

Worth computing rather than asserting, because "it depends on scale" is not a decision:

Eigenbau, monatlich       €110 Infrastruktur + €300 Betrieb        = €410
Grafana Cloud, monatlich  €100 Betrieb + €17 Plattform + variabel
Variable Kosten           $50 / 10.000 Executions ≈ €0,0046 je Lauf

408 = 117 + (N - 10.000) x 0,0046   →   N ≈ 73.000 Läufe / Monat
73.000 / (4 x 30) ≈ 600 URL-Profile

At three URLs and two profiles per site, 600 profiles is around 100 sites — two and a half to three times the current estate. And at that volume the self-hosted option needs three or four runners rather than two, which pushes the crossing point further out still.

So the build isn’t wrong in principle. It’s wrong at this size. That distinction is worth writing into the decision record explicitly, along with the condition under which it flips, because "we looked at this once and said no" ages badly compared to "we said no below 600 URL profiles."

The first three ADRs stayed in the repository for the same reason. Sizing, retention, cardinality, and alert logic don’t depend on who executes the runs — the regression-detection ADR is nearly unchanged on Grafana Cloud, since it’s the same Grafana alerting either way. They’re build instructions for the day the condition trips, not deleted work.

Which Arguments for Building Didn’t Survive?

Two, and both are the kind that feel decisive right up until you write them down.

Data sovereignty. Synthetic monitoring fetches publicly reachable pages with a browser. No personal data is produced — only load times for public URLs. The sovereignty argument belongs to RUM, where real users are measured, and that decision was already made elsewhere. Claiming it here would have been dishonest. What does survive is smaller and more specific: the list of monitored URLs reveals the customer structure. That’s an argument for an EU contract, not against SaaS.

The learning effect. Building a Terraform/Hetzner stack has genuine value. It is just not a monitoring cost. Booked honestly it’s professional development, and if both are wanted, the cleaner arrangement is production monitoring on managed infrastructure and the infrastructure-as-code project running separately, without operational pressure. Bundling them means the learning project inherits a pager.

The one that partly survived is lock-in, but not in the direction usually claimed. June 2026’s Hetzner repricing — plus 169 percent on dedicated vCPU — is a reminder that self-hosting doesn’t protect you from price increases either. The real difference is escape time: servers can be moved, a SaaS contract has to be cancelled and its history exported. Which converts a vague worry into two concrete practices: keep the test definitions as code, and export the time series on a schedule.

The genuinely painful comparison wasn’t build-versus-buy at all. It was between two vendors: DebugBear is the better tool for this job, noticeably rather than marginally, at roughly twice the price. It lost to an explicit "cost decides" mandate, with a named correction condition — if root-cause analysis starts failing regularly for want of HAR archives, the fix is DebugBear, not a rebuild.

What Did the Estimate Get Wrong Anyway?

Two things, both found while writing the Terraform that implements the decision. Documented as a correction inside the ADR rather than quietly edited into the original numbers.

The cadence isn’t configurable. The plan called for four runs a day. Grafana Cloud accepts 60 to 3600 seconds between runs, so hourly is the sparsest option available. Four runs a day became 24 — a factor of six on a bill that’s charged per run.

But one check may visit several pages. Billing is per probe location and per started minute, so a script walking a site’s three URL profiles costs the same as one loading a single page. That absorbs most of the factor of six.

Net effect, recomputed:

bought self-built
1 location €38 / month €366 / month
3 locations €170 / month €454 / month

The decision holds; the margin narrows from 4.7× to 2.7×. What changes is the expectation. The free tier — 10,000 browser executions a month — covers 13 sites at one location, not 20. The stack costs money from day one, not from some later growth threshold. That belongs on the table before a contract is signed, not in the first invoice.

One more revision arrived from the implementation, and this one moved in the other direction. The €170 figure above assumes one check per site. It isn’t possible — the vendor caps browser checks at ten per tenant — and the grouping that works around the cap also cuts the bill, because billing counts started minutes per location rather than pages. Nine grouped checks at three locations, hourly, come to 19,440 executions and about €61 a month. So the estimate that mattered was wrong by a factor of nearly three in the favourable direction, for a reason nobody could have priced in advance: the constraint that forced the redesign was undocumented until the first apply returned a 403.

What Doesn’t This Generalize To?

A build-versus-buy calculation is only as good as its inputs, and two of these deserve to be flagged rather than trusted.

Several vendor pricing pages were unreachable from the build environment, so the figures came from search summaries carrying explicit confidence ratings — high for Grafana Cloud and Datadog, medium for DebugBear and SpeedCurve, where the relevant tier is quote-based anyway. A number with a confidence rating attached is usable. The same number presented flat, as though it were verified, is a trap for whoever reads the ADR next year.

And the two load-bearing assumptions got measured rather than assumed: test runtime came in at 1.0 s median against a billing minute — worst single value 4.1 s, still 7 percent of a billed minute — and the scope came in at 75 URL profiles rather than the 210 originally assumed. That second measurement moved the break-even from roughly three times the current estate to ten.

Both measurements pointed the same way the decision already leaned, which is the outcome you should be most suspicious of. They’re worth doing precisely because the alternative — a decision resting on two unmeasured numbers that happen to favour the preferred answer — is indistinguishable from a good decision until it isn’t.

What’s the Generalizable Lesson Here?

  • Cost analysis drifts toward whatever has a published price. Servers have one, your own time doesn’t, so operating time gets implicitly priced at zero and every comparison tilts toward the option that consumes more of it. Put an hourly rate on it before comparing anything.
  • Detailed sizing work before a build-versus-buy check is effort spent in the wrong order. Three ADRs optimized a €40/month decision inside a €410/month option that shouldn’t have been chosen.
  • A "no" with a named return condition — 600 URL profiles, or a measurement scenario the vendor can’t express — is worth far more than a plain "no." It tells the next reader when to reopen the question, and it lets the superseded design documents stay in the repository as instructions rather than embarrassment.
  • Test your favourite arguments for honesty. Data sovereignty is a real concern about real user data and doesn’t transfer to fetching public URLs; a learning effect is real value booked under the wrong heading. Arguments that don’t survive being written down were never load-bearing.
  • Self-hosting is not a hedge against price increases — a 169 percent repricing can arrive from either direction. What differs is escape time, and the way to buy escape time is keeping definitions as code and exporting history on a schedule.
  • Measure the two numbers your decision actually rests on, especially when you expect them to confirm what you already believe. Here they did, and the decision is stronger for it being checkable.

The Terraform that implements this decision has its own surprises — a vendor cap of ten browser checks that isn’t on any pricing page, and a billing model that reshapes the whole configuration. That’s the next post.