<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged monitoring</title>
<link>https://coderlegion.com/tag/monitoring</link>
<description>Powered by Question2Answer</description>
<item>
<title>A week of green runs, and the pipeline was waiting for me</title>
<link>https://coderlegion.com/32698/a-week-of-green-runs-and-the-pipeline-was-waiting-for-me</link>
<description>&lt;p&gt;On 1 September at 10:57 UTC the monthly ingest ran. It opened issue number 3 of the magazine, picked the vertical from the source registry, wrote 19 candidate sources into Postgres with &lt;code&gt;stage = 'discovery'&lt;/code&gt;, and opened a GitHub issue telling me it was my turn.&lt;/p&gt;
&lt;p&gt;Every morning after that, the daily job ran and succeeded. Eight consecutive green runs, from the first of the month to the eighth. The article count for September stayed at zero the whole time.&lt;/p&gt;
&lt;h3 id=&quot;the-gate-was-doing-its-job&quot;&gt;The gate was doing its job&lt;/h3&gt;
&lt;p&gt;The daily job does not decide anything by itself. It asks the database which stage to run, and the answer depends on a view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id, issue_id, source_url, tier, independent
FROM signals
WHERE stage = 'verify' AND (tier = 1 OR tier = 2 AND independent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing satisfies that view until I sit in Supabase Studio and tag the sources by hand. Tier 1 is a primary source, a regulator or the organisation that ran the thing. Tier 2 counts only when it is independent of whoever it describes, which rules out a consultancy writing up its own project as a case study.&lt;/p&gt;
&lt;p&gt;That judgement is mine on purpose. It is the editorial bar of the magazine, and I did not want a model deciding what counts as proof of anything.&lt;/p&gt;
&lt;p&gt;So the decision function found a draft issue with no verified signals, returned &lt;code&gt;niente&lt;/code&gt;, and printed its reason to stderr:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;advance: signal in attesa di verifica in Studio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it exited zero, which is correct. There was nothing for it to do.&lt;/p&gt;
&lt;h3 id=&quot;what-my-alarms-actually-watch&quot;&gt;What my alarms actually watch&lt;/h3&gt;
&lt;p&gt;I have three, and I was fairly pleased with them.&lt;/p&gt;
&lt;p&gt;A cron that fails opens a GitHub issue on its own. A cron that never starts is caught by a daily watchman that asks the GitHub API when each schedule last fired, because a schedule that GitHub has quietly disabled produces no failure to report. And because a watchman inside the same failure domain is not a watchman, that job also checks in with a Sentry monitor that alarms on silence from outside GitHub.&lt;/p&gt;
&lt;p&gt;Between them they cover a job that breaks and a job that never runs. Neither of those happened. What happened was a job that ran, succeeded, and did nothing, for seven days, because it was waiting for a person who had not noticed.&lt;/p&gt;
&lt;h3 id=&quot;the-information-was-already-there&quot;&gt;The information was already there&lt;/h3&gt;
&lt;p&gt;The part I find annoying is that nothing was hidden. The reason is written every single morning, in the run log, in Italian, on stderr. The job knows exactly why it is idle and says so out loud.&lt;/p&gt;
&lt;p&gt;What it does not know is how long it has been saying it. &lt;code&gt;niente&lt;/code&gt; on the second of the month and &lt;code&gt;niente&lt;/code&gt; on the eighth are the same string, and the only thing that separates them is a week of my attention going somewhere else. No alarm reads that log line, and no counter turns it into a duration.&lt;/p&gt;
&lt;p&gt;Green means the machine is healthy. I had been reading it as &quot;the work is moving&quot;, and those are different claims.&lt;/p&gt;
&lt;h3 id=&quot;how-it-ended&quot;&gt;How it ended&lt;/h3&gt;
&lt;p&gt;On the eighth I opened Studio and tagged 11 of the 19 sources: two from the regulator as tier 1, nine analyst reports as tier 2 and independent. I left out a vendor case study, which fails the independence test by construction, and seven security bulletins whose scraped body is only a title.&lt;/p&gt;
&lt;p&gt;After that the pipeline did the rest in a few minutes. Generate wrote the piece from eight sources, embed rebuilt the chunks, export produced the two markdown files, and the judge on the pull request failed me for weak attribution, which is a story for another post.&lt;/p&gt;
&lt;p&gt;The September issue went out a week late. Nothing had broken, and every run had been green.&lt;/p&gt;
&lt;p&gt;So I do not need a fourth alarm. I need the daily job to count the mornings it has spent answering &lt;code&gt;niente&lt;/code&gt;, and to say that number where I actually look.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32698/a-week-of-green-runs-and-the-pipeline-was-waiting-for-me</guid>
<pubDate>Tue, 27 Oct 2026 16:02:11 +0000</pubDate>
</item>
<item>
<title>Online Says It's Connected, HeartBeat Says It Works: Reading Two Status Signals Instead of Guessing</title>
<link>https://coderlegion.com/32601/online-says-its-connected-heartbeat-says-works-reading-status-signals-instead-guessing</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Online Says It's Connected, HeartBeat Says It Works: Reading Two Status Signals Instead of Guessing From Silence&quot;&lt;br&gt;
published: true&lt;br&gt;
tags: iot, mqtt, radar, monitoring&lt;br&gt;
canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/27/online-heartbeat-radar-health.html&quot;&gt;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/27/online-heartbeat-radar-health.html&lt;/a&gt;&lt;br&gt;
description: &quot;Two fields in a mmWave radar's MQTT output — online and heartBeat — look like the same health check and are not. One proves the link; the other proves the sensor. And neither supports a timeout.&quot;&lt;/p&gt;
&lt;h3 id=&quot;cover-image-&quot;&gt;cover_image:&lt;/h3&gt;
&lt;p&gt;Two fields in a radar's MQTT output get mistaken for each other constantly, and mistaking them is how a health monitor ends up trusted when it should not be.&lt;/p&gt;
&lt;p&gt;They answer different questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;online&lt;/code&gt; answers &quot;is the link up?&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;heartBeat&lt;/code&gt; answers &quot;is the radar itself working?&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;&lt;code&gt;online&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;heartBeat&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Question answered &lt;/td&gt;&lt;td&gt;Is this device reachable? &lt;/td&gt;&lt;td&gt;Is this device operating correctly?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reported values &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;0&quot;&lt;/code&gt; offline, &lt;code&gt;&quot;1&quot;&lt;/code&gt; online &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;0&quot;&lt;/code&gt; abnormal, &lt;code&gt;&quot;1&quot;&lt;/code&gt; normal&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Layer &lt;/td&gt;&lt;td&gt;MQTT connection and broker session &lt;/td&gt;&lt;td&gt;The radar's own operating state&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reported when &lt;/td&gt;&lt;td&gt;On state change only &lt;/td&gt;&lt;td&gt;On state change only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Available on &lt;/td&gt;&lt;td&gt;Sleep radar, fall radar, presence radar &lt;/td&gt;&lt;td&gt;Sleep radar, fall radar&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The presence radar publishes its online state on a dedicated topic — &lt;code&gt;/RadarHP/&amp;lt;DeviceID&amp;gt;/sys/online/post&lt;/code&gt;; the sleep and fall radars publish &lt;code&gt;online&lt;/code&gt; as an attribute in the same property payload as everything else. Note the asymmetry: &lt;strong&gt;there is no &lt;code&gt;heartBeat&lt;/code&gt; in the presence radar's protocol documentation.&lt;/strong&gt; The other two report it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&quot;version&quot;: &quot;1.0&quot;, &quot;method&quot;: &quot;post&quot;, &quot;params&quot;: {&quot;online&quot;: &quot;0&quot;}}
{&quot;version&quot;: &quot;1.0&quot;, &quot;method&quot;: &quot;post&quot;, &quot;params&quot;: {&quot;heartBeat&quot;: &quot;0&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-this-distinction-is-not-academic&quot;&gt;Why this distinction is not academic&lt;/h3&gt;
&lt;p&gt;Consider what has to be true for any &lt;code&gt;heartBeat&lt;/code&gt; message to reach your code: the radar has power, it is associated with the Wi-Fi access point, its MQTT session is established, and you are subscribed to its topic. &lt;strong&gt;A delivered &lt;code&gt;heartBeat&lt;/code&gt; therefore already proves the link is working.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From that, one conclusion follows immediately:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;heartBeat&lt;/code&gt; cannot report a connectivity failure. Physically and logically, it cannot — a radar that has lost its link has no way to deliver that news.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The reverse holds for &lt;code&gt;online&lt;/code&gt;. An &lt;code&gt;online: &quot;0&quot;&lt;/code&gt; message is not written by the radar that went offline; it is generated &lt;strong&gt;by the broker&lt;/strong&gt; when the session it was holding stops answering. The radar contributes nothing to that judgement.&lt;/p&gt;
&lt;p&gt;So neither field overlaps with the other, and neither can substitute for the other:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you only watch &lt;code&gt;online&lt;/code&gt;, you will see &quot;connected&quot; while the radar sits there reporting a fault about itself.&lt;/li&gt;
&lt;li&gt;If you only watch &lt;code&gt;heartBeat&lt;/code&gt;, every network outage looks like nothing at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-trap-silence-is-not-evidence&quot;&gt;The trap: silence is not evidence&lt;/h3&gt;
&lt;p&gt;Both fields are documented as &lt;strong&gt;reported on state change&lt;/strong&gt;, not on a timer. That single sentence is where most monitoring code goes wrong.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Occupancy.&lt;/strong&gt; The presence state is event-driven. A room whose state does not change for three hours produces no occupancy messages, and that is correct behaviour — not a fault.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heartbeat.&lt;/strong&gt; A radar that boots normally and stays normal has no reason to send &lt;code&gt;heartBeat&lt;/code&gt; at all, because no state change occurred.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The one periodic attribute.&lt;/strong&gt; In the sleep and fall protocols, the only attribute the documentation marks as periodically reported is &lt;code&gt;workDuration&lt;/code&gt; (4 bytes, value range &lt;code&gt;0x3C&lt;/code&gt;–&lt;code&gt;0xffffffff&lt;/code&gt;, unit seconds) — and it is marked &quot;not yet available&quot; in the same line. &lt;code&gt;workDurationReportFrequency&lt;/code&gt; carries the same note. &lt;strong&gt;Do not build a keepalive on either of them.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the tempting pattern — &quot;if I have not heard from the device in 60 seconds, it must be gone&quot; — has no foundation here. Absence of messages is the normal state of a working installation.&lt;/p&gt;
&lt;h3 id=&quot;what-you-can-do-instead&quot;&gt;What you can do instead&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;code&gt;online&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;heartBeat&lt;/code&gt; &lt;/th&gt;&lt;th&gt;Conclusion &lt;/th&gt;&lt;th&gt;Action&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;Link up, radar reports normal &lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Link up, radar reports a fault&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Raise the higher-severity alarm; this is the case only &lt;code&gt;heartBeat&lt;/code&gt; can surface&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;— &lt;/td&gt;&lt;td&gt;Session dropped, as judged by the broker &lt;/td&gt;&lt;td&gt;Investigate the link; the radar's own state is unknown&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;no messages &lt;/td&gt;&lt;td&gt;no messages &lt;/td&gt;&lt;td&gt;Most likely nothing changed &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Nothing can be concluded&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Treat the two as different classes of event with different severities. A lost link is an infrastructure ticket; a radar reporting itself abnormal is a hardware ticket, and it is the one that should wake someone up.&lt;/p&gt;
&lt;h3 id=&quot;two-rules-you-must-not-break&quot;&gt;Two rules you must not break&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Do not disable &lt;code&gt;online&lt;/code&gt; or &lt;code&gt;heartBeat&lt;/code&gt; with &lt;code&gt;limit_set&lt;/code&gt;.&lt;/strong&gt; The Wi-Fi protocols expose a reporting-suppression command: &lt;code&gt;limit_set&lt;/code&gt; / &lt;code&gt;limit_get&lt;/code&gt;, where &lt;code&gt;1&lt;/code&gt; means &quot;suppress this attribute&quot; and &lt;code&gt;0&lt;/code&gt; means &quot;allow it&quot;. It exists for the attributes that report frequently — &lt;code&gt;someoneExists&lt;/code&gt;, &lt;code&gt;motionStatus&lt;/code&gt;, &lt;code&gt;movementSigns&lt;/code&gt;, &lt;code&gt;humanDistance&lt;/code&gt;, &lt;code&gt;humanPosition&lt;/code&gt; — and it is a legitimate way to cut traffic and noise. Both &lt;code&gt;online&lt;/code&gt; and &lt;code&gt;heartBeat&lt;/code&gt; appear in that list. Suppress them and you remove the only two signals that stand between you and a timeout guess.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Do not try to poll &lt;code&gt;heartBeat&lt;/code&gt;.&lt;/strong&gt; Every neighbouring attribute in those documents — &lt;code&gt;initFinish&lt;/code&gt;, &lt;code&gt;workDuration&lt;/code&gt;, &lt;code&gt;productMode&lt;/code&gt; — carries the line &quot;this attribute supports querying&quot; followed by a &lt;code&gt;get&lt;/code&gt; example. &lt;code&gt;heartBeat&lt;/code&gt; does not. The only &lt;code&gt;?&lt;/code&gt; it ever appears with is inside &lt;code&gt;limit_get&lt;/code&gt;, and that question means &quot;is reporting suppressed for this attribute&quot;, not &quot;what is its current value&quot;. There is no documented way to ask the radar for that value on demand.&lt;/p&gt;
&lt;p&gt;That leaves one workable approach for the things you genuinely need on demand: &lt;strong&gt;query the attributes that do support it.&lt;/strong&gt; If you must confirm a radar has finished starting up, &lt;code&gt;initFinish&lt;/code&gt; (&lt;code&gt;0&lt;/code&gt; not complete, &lt;code&gt;1&lt;/code&gt; complete) is queryable. &lt;code&gt;heartBeat&lt;/code&gt; is not, and no amount of polling will change that.&lt;/p&gt;
&lt;h3 id=&quot;a-minimal-health-monitor&quot;&gt;A minimal health monitor&lt;/h3&gt;
&lt;p&gt;Subscribe, merge the sparse payload, and raise the two classes separately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json, time
import paho.mqtt.client as mqtt

TOPIC = &quot;/Radar60SP/&amp;lt;MAC&amp;gt;/#&quot;

state = {}
last_any = {}


def on_message(client, userdata, msg):
    try:
        payload = json.loads(msg.payload.decode())
    except ValueError:
        return
    params = payload.get(&quot;params&quot;, {})
    if not params:
        return

    # Only the fields that changed are present.
    for k in (&quot;online&quot;, &quot;heartBeat&quot;, &quot;initFinish&quot;):
        if k in params:
            state[k] = params[k]

    last_any[&quot;ts&quot;] = time.time()

    # Two different failures, two different severities.
    if params.get(&quot;online&quot;) == &quot;0&quot;:
        raise_alarm(&quot;link&quot;, device=msg.topic, severity=&quot;infra&quot;)
    if params.get(&quot;heartBeat&quot;) == &quot;0&quot;:
        raise_alarm(&quot;sensor&quot;, device=msg.topic, severity=&quot;high&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the shape: nothing here fires on elapsed time. Every action is triggered by an arriving value.&lt;/p&gt;
&lt;p&gt;If you still want a timeout as a last resort, the threshold must come from measurement, not habit. Log the longest interval your real installation goes without any message during normal operation, then add margin to that. A number you made up will either fire constantly or never fire.&lt;/p&gt;
&lt;h3 id=&quot;measure-these-at-commissioning&quot;&gt;Measure these at commissioning&lt;/h3&gt;
&lt;p&gt;Two things the documentation does not state, and that you should record per installation rather than assume:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does the radar announce &lt;code&gt;heartBeat: &quot;1&quot;&lt;/code&gt; on power-up?&lt;/strong&gt; A device that boots healthy may never have a reason to report it. Power one up on the bench with a subscriber attached and write down what actually arrives, and how long after power-up it arrives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does &lt;code&gt;online: &quot;1&quot;&lt;/code&gt; get republished after a reconnect?&lt;/strong&gt; If it does not, your consumer must treat the absence of a change as &quot;still online&quot; rather than as unknown.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both belong in the same commissioning note as the empty-room baseline from &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/24/distance-position-debug-installation.html&quot;&gt;Using Distance and Position Output to Debug a Radar Installation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;maintenance-looks-like-a-failure&quot;&gt;Maintenance looks like a failure&lt;/h3&gt;
&lt;p&gt;One consequence of all this that catches people out: the sleep and fall protocols support firmware update over the air — &lt;code&gt;ota_set&lt;/code&gt; with &lt;code&gt;otaModule&lt;/code&gt; (&lt;code&gt;0&lt;/code&gt; Wi-Fi module, &lt;code&gt;1&lt;/code&gt; radar module, and the command must be sent before every update), &lt;code&gt;otaInform&lt;/code&gt; giving a firmware URL and port, and &lt;code&gt;downloadProgress&lt;/code&gt; reporting &lt;code&gt;0&lt;/code&gt;–&lt;code&gt;100&lt;/code&gt;. On success the module restarts and then reports its new version number.&lt;/p&gt;
&lt;p&gt;During that window the device is legitimately unreachable. If your monitoring treats a missing device as an alarm, a firmware update will page someone. Put planned maintenance in a suppression window, and use the version report afterwards — the presence radar's &lt;code&gt;/RadarSP/&amp;lt;DeviceID&amp;gt;/sys/version/post&lt;/code&gt; topic, or the &lt;code&gt;firmwareVersion&lt;/code&gt; attribute — to confirm the update actually landed.&lt;/p&gt;
&lt;p&gt;The update server is one you host yourself; the documentation states plainly that you build the web server the firmware is fetched from.&lt;/p&gt;
&lt;h3 id=&quot;two-controls-worth-knowing-about&quot;&gt;Two controls worth knowing about&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reporting suppression&lt;/strong&gt; (&lt;code&gt;limit_set&lt;/code&gt; / &lt;code&gt;limit_get&lt;/code&gt;): set &lt;code&gt;1&lt;/code&gt; to suppress an attribute, &lt;code&gt;0&lt;/code&gt; to allow it. Use it on the high-frequency attributes to cut traffic and clutter. Leave &lt;code&gt;online&lt;/code&gt; and &lt;code&gt;heartBeat&lt;/code&gt; alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-attribute QoS&lt;/strong&gt; (&lt;code&gt;control_qos&lt;/code&gt; in the presence protocol, &lt;code&gt;qos_set&lt;/code&gt; / &lt;code&gt;qos_get&lt;/code&gt; with &lt;code&gt;Qos_post&lt;/code&gt; in the sleep and fall protocols): the default is QoS 1, where &lt;code&gt;0&lt;/code&gt; is QoS 0, &lt;code&gt;1&lt;/code&gt; is QoS 1 and &lt;code&gt;2&lt;/code&gt; is QoS 2. The presence protocol additionally exposes per-attribute keys — &lt;code&gt;Qos_online&lt;/code&gt;, &lt;code&gt;Qos_movementSigns&lt;/code&gt;, &lt;code&gt;Qos_breathRate&lt;/code&gt;, and so on. This is worth knowing because it contradicts the usual assumption that QoS is decided only by the subscriber.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;checklist&quot;&gt;Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;online&lt;/code&gt; and &lt;code&gt;heartBeat&lt;/code&gt; handled as two separate signals with two separate severities&lt;/li&gt;
&lt;li&gt;[ ] No logic concludes anything from the absence of messages&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;limit_set&lt;/code&gt; used, if at all, only on high-frequency attributes — never on &lt;code&gt;online&lt;/code&gt; or &lt;code&gt;heartBeat&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] No polling of &lt;code&gt;heartBeat&lt;/code&gt;; anything needed on demand read from attributes that document query support&lt;/li&gt;
&lt;li&gt;[ ] Power-up behaviour of &lt;code&gt;heartBeat&lt;/code&gt; measured and recorded&lt;/li&gt;
&lt;li&gt;[ ] Reconnect behaviour of &lt;code&gt;online&lt;/code&gt; measured and recorded&lt;/li&gt;
&lt;li&gt;[ ] Any timeout threshold derived from measured worst-case silence, not assumed&lt;/li&gt;
&lt;li&gt;[ ] Firmware updates placed in a suppression window, and verified afterwards by version report&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Getting the stream in the first place: &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/25/reading-radar-data-your-own-app.html&quot;&gt;Reading Radar Data in Your Own Application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The broker underneath it: &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/23/apache-artemis-mqtt-broker-radar.html&quot;&gt;Apache Artemis setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Keeping several devices straight: &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/24/running-several-radars-one-home.html&quot;&gt;Running Several Radars in One Home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Keeping the history: &lt;a rel=&quot;nofollow&quot; href=&quot;https://249263431.github.io/60GHz-Radar-SDK/guide/2026/10/26/storing-radar-data-retention-trends.html&quot;&gt;Storing Radar Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is not a medical device. A status field tells you what a radar reports about itself; it does not guarantee what the radar detects. For anyone at real risk, monitoring is an addition to a care routine, never a substitute for one.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32601/online-says-its-connected-heartbeat-says-works-reading-status-signals-instead-guessing</guid>
<pubDate>Tue, 27 Oct 2026 01:03:42 +0000</pubDate>
</item>
<item>
<title>How to get alerted when someone changes your DNS records</title>
<link>https://coderlegion.com/31079/how-to-get-alerted-when-someone-changes-your-dns-records</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Most DNS changes do not take your site down. A new MX record or an edited SPF record usually still resolves, so your uptime checks stay green. To catch these changes, list every record a name should have. Then get an alert when the live answer is different in any way. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/monitor-dns-record-changes?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;original post&lt;/a&gt; has the same text with a short FAQ at the end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;a-dns-change-does-not-look-like-an-outage&quot;&gt;A DNS change does not look like an outage&lt;/h3&gt;
&lt;p&gt;Uptime monitors wait for something to break. A DNS change often breaks nothing you can see.&lt;/p&gt;
&lt;p&gt;Here is an example. Someone adds a second MX record to your domain. MX records tell other servers where to deliver your email. Maybe it was a test of a new email provider, and nobody removed it. Maybe someone stole the password to your DNS account.&lt;/p&gt;
&lt;p&gt;Each MX record has a priority number, and senders try the lowest one first. If the new record has a lower number than yours, your email now goes to another server first. Your website works, your checks are green, and nobody notices for weeks.&lt;/p&gt;
&lt;p&gt;Other changes work the same way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Someone changes your nameservers at the registrar. Now a different server answers for your whole domain.&lt;/li&gt;
&lt;li&gt;Someone edits your SPF record during a migration. SPF lists the servers that may send email as you, and your newsletter tool is no longer on it.&lt;/li&gt;
&lt;li&gt;Someone deletes your &lt;code&gt;_dmarc&lt;/code&gt; record while cleaning up old records.&lt;/li&gt;
&lt;li&gt;Someone changes your CAA record. Now a different certificate authority can issue certificates for your domain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These changes can leave your DNS lookups and website checks passing.&lt;/p&gt;
&lt;h3 id=&quot;this-happens-in-real-attacks&quot;&gt;This happens in real attacks&lt;/h3&gt;
&lt;p&gt;In January 2019, the US Cybersecurity and Infrastructure Security Agency (CISA) published &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cisa.gov/news-events/directives/ed-19-01-mitigate-dns-infrastructure-tampering-closed&quot;&gt;Emergency Directive 19-01&lt;/a&gt;. Attackers had changed DNS records on several US government domains.&lt;/p&gt;
&lt;p&gt;They obtained credentials for a DNS account. They changed A, MX and NS records to point to their own servers. Then they could read web and email traffic as it passed through.&lt;/p&gt;
&lt;p&gt;CISA's first order to the agencies was simple: check your public DNS records, and make sure each one points to the right place. Start with NS records.&lt;/p&gt;
&lt;p&gt;A check like that tells you the records were right on one day. A monitor does the same check every few minutes, so you find out on the day of the change.&lt;/p&gt;
&lt;h3 id=&quot;why-does-it-resolve-is-not-enough&quot;&gt;Why &quot;does it resolve?&quot; is not enough&lt;/h3&gt;
&lt;p&gt;Most DNS checks ask one of two questions.&lt;/p&gt;
&lt;p&gt;The first is &quot;Does the name resolve?&quot; This catches a deleted record or an &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/domain-expired-but-site-still-up?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;expired domain&lt;/a&gt;. It does not catch a changed record, because a changed record still resolves.&lt;/p&gt;
&lt;p&gt;The second is &quot;Does the answer contain this value?&quot; For example, you enter your mail server, and the check fails if it disappears. This is better. But the check passes as long as your value is somewhere in the answer. So it misses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an extra MX or NS record next to yours;&lt;/li&gt;
&lt;li&gt;an SPF record changed around the part you check;&lt;/li&gt;
&lt;li&gt;one A record replaced, when you have several.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To catch these, the check must know every record that should be there. Then it can fail on anything else.&lt;/p&gt;
&lt;h3 id=&quot;watch-the-exact-record-set&quot;&gt;Watch the exact record set&lt;/h3&gt;
&lt;p&gt;An exact record set is a list of every record that one name should have, for one record type. The check reads the live answer and compares it with your list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A record in your list but not in the answer is &lt;strong&gt;missing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A record in the answer but not in your list is &lt;strong&gt;unexpected&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A changed record shows up as both. The old value is missing and the new value is unexpected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any difference fails the check. The order of the records does not matter, and neither does the TTL.&lt;/p&gt;
&lt;p&gt;To write the list, start from the records your domain has now. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/dns-lookup?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;DNS lookup tool&lt;/a&gt; shows them from Cloudflare and Google side by side. Check that every record is one you expect. If the two resolvers disagree, a change is still spreading, so wait until they match.&lt;/p&gt;
&lt;p&gt;In Uptimepage, create a DNS monitor and choose &lt;strong&gt;exact set&lt;/strong&gt; as the answer match. Write one record per line, the way a DNS answer shows it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 mail.example.com.
20 alt.example.com.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Say someone adds a third MX record. The next check fails, and the result shows what changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;domain&quot;: &quot;example.com&quot;,
  &quot;record_type&quot;: &quot;MX&quot;,
  &quot;missing&quot;: [],
  &quot;unexpected&quot;: [&quot;5 mx.attacker.example.&quot;],
  &quot;answers&quot;: [&quot;10 mail.example.com.&quot;, &quot;20 alt.example.com.&quot;, &quot;5 mx.attacker.example.&quot;],
  &quot;expected_contains&quot;: null,
  &quot;matched&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new MX record has priority 5, lower than yours, so senders try it first.&lt;/p&gt;
&lt;p&gt;The check ignores small differences that would only cause false alarms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upper and lower case in names do not matter, so &lt;code&gt;10 Mail.Example.com&lt;/code&gt; matches &lt;code&gt;10 mail.example.com.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The dot at the end of a name is optional.&lt;/li&gt;
&lt;li&gt;Numbers and IP addresses are compared by value, so &lt;code&gt;2001:0db8::1&lt;/code&gt; matches &lt;code&gt;2001:db8::1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TXT values must match exactly, including case. In SPF and DKIM, every character counts.&lt;/p&gt;
&lt;p&gt;Enter only the record value, without the name, TTL or type. For TXT records, you can paste quoted strings from your zone file, with all parts on one line.&lt;/p&gt;
&lt;h3 id=&quot;which-records-to-watch&quot;&gt;Which records to watch&lt;/h3&gt;
&lt;p&gt;You do not need this for every record. Start with the ones where a quiet change hurts the most.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Record &lt;/th&gt;&lt;th&gt;Name &lt;/th&gt;&lt;th&gt;What it controls&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;NS &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Which servers answer for your whole domain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MX &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Where your email goes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your SPF record: who may send email as you&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;_dmarc.example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your DMARC policy: what receivers do with fake email from your domain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TXT &lt;/td&gt;&lt;td&gt;&lt;code&gt;selector._domainkey.example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your DKIM public key, which receivers use to check your email signatures&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CAA &lt;/td&gt;&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Which certificate authorities may issue certificates for you&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A, AAAA &lt;/td&gt;&lt;td&gt;your server hostnames &lt;/td&gt;&lt;td&gt;Only if your servers have fixed IP addresses&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Your main domain often has other TXT records too, such as verification records from other services. Put all of them in the list. A new one means someone connected a new service to your domain, and you want to know about that.&lt;/p&gt;
&lt;p&gt;One monitor watches one name and one record type. Start with six monitors, one for each of the first six rows. Then add one for each other DKIM selector you use, and one for each fixed A or AAAA record set. A hostname with both an A and an AAAA record needs two monitors.&lt;/p&gt;
&lt;p&gt;If you set up only one, make it NS. CISA put NS first too. Whoever changes your NS records controls all your other records.&lt;/p&gt;
&lt;h3 id=&quot;query-your-own-nameserver&quot;&gt;Query your own nameserver&lt;/h3&gt;
&lt;p&gt;Normally a DNS lookup goes through a resolver, such as &lt;code&gt;1.1.1.1&lt;/code&gt; or the one from your internet provider. The resolver keeps each answer for a while. This time is called the TTL. If your MX record has a TTL of one hour, the resolver can show the old answer for about an hour after a change. Sometimes it is longer: a resolver that cannot reach your nameserver may &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8767.html#section-1&quot;&gt;keep serving the old answer&lt;/a&gt; until it can.&lt;/p&gt;
&lt;p&gt;You can skip this delay. Set the monitor's resolver to the IP address of your own nameserver, the one that holds your real records (your authoritative nameserver). Then every check sees your records as they are right now.&lt;/p&gt;
&lt;p&gt;There is one exception: NS records. If someone changes your nameservers at the registrar, your own nameserver does not know. It still answers with the old, correct list. So watch NS through a public resolver, such as &lt;code&gt;1.1.1.1&lt;/code&gt; or &lt;code&gt;8.8.8.8&lt;/code&gt;. A public resolver follows the registrar, like your visitors do. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/dns-lookup?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;check what a public resolver sees&lt;/a&gt; for any name before you create the monitor.&lt;/p&gt;
&lt;h3 id=&quot;when-not-to-use-an-exact-set&quot;&gt;When not to use an exact set&lt;/h3&gt;
&lt;p&gt;Some records change on purpose. An exact set on them only gives you false alarms.&lt;/p&gt;
&lt;p&gt;Names behind a CDN or a load balancer get their IP addresses from a pool. The answer can change over time and depending on where you query from. I wrote about this in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/why-dns-returns-different-ip-addresses?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;why DNS returns different IP addresses&lt;/a&gt;. An exact A set on such a name would fail all the time. Watch the CNAME record that points to the CDN instead.&lt;/p&gt;
&lt;p&gt;SOA records change every time you edit your zone, because their serial number goes up. &lt;code&gt;_acme-challenge&lt;/code&gt; TXT records come and go with each certificate renewal. Do not use an exact set for these either.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-list-next-to-your-dns&quot;&gt;Keep the list next to your DNS&lt;/h3&gt;
&lt;p&gt;If you manage DNS with Terraform, put the monitor in the same repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;uptimepage_target&quot; &quot;mx&quot; {
  name     = &quot;mx example.com&quot;
  interval = 300

  check = {
    type = &quot;dns&quot;
    dns = {
      domain           = &quot;example.com&quot;
      record_type      = &quot;MX&quot;
      expected_records = [&quot;10 mail.example.com.&quot;, &quot;20 alt.example.com.&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now a pull request that changes an MX record must change the monitor too. An MX change that nobody reviewed fails the check. You need Terraform provider 0.15.0 or later. Read more in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/monitoring-as-code?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline&quot;&gt;monitoring as code&lt;/a&gt;. All DNS options are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/monitor-types?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=inline#dns&quot;&gt;DNS monitor docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;where-uptimepage-fits&quot;&gt;Where Uptimepage fits&lt;/h3&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=cta&quot;&gt;Uptimepage&lt;/a&gt;, an uptime monitor that checks your services from several regions and alerts you when one fails. The exact set match above is part of its &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/monitor-types?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=dns-record-changes&amp;amp;utm_content=cta#dns&quot;&gt;DNS monitor&lt;/a&gt;. A DNS change alerts you once the failure is confirmed: by default two failed checks in a row, from more than half of the regions. The result names the missing and the unexpected records, so you see the change without running &lt;code&gt;dig&lt;/code&gt; yourself. It is open source under AGPL, so it can also run on your own server.&lt;/p&gt;
&lt;p&gt;Whatever you use, look up your NS records today and check that every name in the answer is one you expect.&lt;/p&gt;
&lt;p&gt;Now the question for the comments: has a DNS record on one of your domains ever changed without anyone noticing? I would like to hear how you found out, and how long it took.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31079/how-to-get-alerted-when-someone-changes-your-dns-records</guid>
<pubDate>Wed, 14 Oct 2026 14:40:51 +0000</pubDate>
</item>
<item>
<title>Multi-tenant PageSpeed monitoring under $100: what agencies actually buy</title>
<link>https://coderlegion.com/30566/multi-tenant-pagespeed-monitoring-under-100-what-agencies-actually-buy</link>
<description>&lt;p&gt;The purchase is rarely another free checker. It is scheduled portfolio coverage, shared history, and flat organisation caps that survive twenty client sites.&lt;/p&gt;
&lt;p&gt;An agency can open ChatGPT, paste a PageSpeed Insights URL, and get a plausible fix list before lunch. That loop feels like monitoring until the account manager asks for last month’s LCP trend across twenty WordPress clients and nobody has a shared schedule, a named alert owner, or a history that survives holiday leave. Multi-tenant PageSpeed monitoring under $100 is not a cheaper way to run one more free check; it is the purchase of portfolio coverage that still fits a three-digit ops line.&lt;/p&gt;
&lt;p&gt;What agencies actually buy in that band is flatter than the vendor shortlist suggests. They buy pooled site and test caps under one organisation subscription, workspaces that keep client brands separate, and reporting an account team can open without pinging the engineer who wrote the cron. The pricing tables and tier caps live on our blog in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/affordable-multi-tenant-performance-monitoring-under-100?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=multi-tenant-pagespeed-under-100-agencies-buy&quot;&gt;Affordable Multi-Tenant Performance Monitoring Under $100/Month&lt;/a&gt;. The day-to-day shape of one dashboard across many client sites is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-multiple-client-sites-one-dashboard?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=multi-tenant-pagespeed-under-100-agencies-buy&quot;&gt;Multiple Client Sites, One Dashboard&lt;/a&gt;. What follows here is the buying story: what lands on the purchase order when the brief says “keep Core Web Vitals coverage under a hundred dollars.”&lt;/p&gt;
&lt;h3 id=&quot;what-agencies-mean-by-multi-tenant-pagespeed-monitoring-under-100&quot;&gt;What agencies mean by multi-tenant PageSpeed monitoring under $100&lt;/h3&gt;
&lt;p&gt;In agency language, multi-tenant means one customer account that holds many client organisations, sites, and schedules without minting a new invoice line per domain. Under $100 means the list price for that organisation-shaped monitor fits a three-digit monthly ceiling before you add flagship real-user monitoring (RUM) seats. The job is scheduled lab coverage on money URLs, CrUX context where PageSpeed Insights publishes it, budgets and alerts with named responders, and history long enough for a quarterly business review.&lt;/p&gt;
&lt;p&gt;That purchase is easy to confuse with adjacent tools. A free PageSpeed Insights run is a diagnostic, an AI chat that proposes CSS changes is a fix assistant, and an uptime product with a small page-speed monitor allowance is infrastructure health with speed as a sidecar. Agencies that say they want multi-tenant PageSpeed monitoring under $100 usually want the portfolio job, not another one-off score.&lt;/p&gt;
&lt;p&gt;If the roster is still under roughly ten priority URLs and one engineer already owns automation, DIY can still hold for a while. Once account managers need shared history and the URL list grows every campaign week, the under-$100 question becomes whether the maths is flat at the organisation or multiplies per monitored slot. That is the fork most agencies hit before they ever open a pricing page.&lt;/p&gt;
&lt;h3 id=&quot;ai-fix-loops-are-not-the-same-purchase-as-a-portfolio-schedule&quot;&gt;AI fix loops are not the same purchase as a portfolio schedule&lt;/h3&gt;
&lt;p&gt;The confusion shows up often on social threads: someone ships an agent that reads Lighthouse findings and opens a pull request, then treats that as “we have monitoring.” Fix loops are useful on sites you can change, yet they are not a substitute for knowing which client homepage went amber on mobile three Tuesdays ago while the developer who owns the agent was offline. The first job remediates; the second proves the roster stayed inside budget when nobody was watching the chat.&lt;/p&gt;
&lt;p&gt;A scheduled portfolio monitor answers a different question. It keeps URL inventories current, runs PageSpeed Insights (or equivalent) on a cadence the retainer can defend, stores history the account team can export, and routes budget breaches to someone other than the person who wrote the workflow. The AI loop still belongs in the stack as a remediation layer on sites you deploy, yet it does not replace the system of record for twenty client origins. Buying under-$100 multi-tenant coverage while also keeping an AI fix assistant is a sensible layering story, but buying only the assistant and calling the retainer “monitored” is how QBR slides become screenshots again. The SOW reads cleaner when both jobs are named, so the account team knows which tool answers which question.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-lands-on-the-purchase-order&quot;&gt;What actually lands on the purchase order&lt;/h3&gt;
&lt;p&gt;When the buy is honest, the purchase order or statement of work names jobs, not logos. Logos without owners still leave the retainer on hero labour. In our experience the under-$100 multi-tenant buy usually includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a flat organisation subscription with published site and monthly test caps,&lt;/li&gt;
&lt;li&gt;enough workspaces to separate client brands without a second vendor,&lt;/li&gt;
&lt;li&gt;scheduled lab runs on agreed money URLs (not “every URL we ever shipped”),&lt;/li&gt;
&lt;li&gt;shared history and exports the account team can open,&lt;/li&gt;
&lt;li&gt;budgets and alerts with named owners,&lt;/li&gt;
&lt;li&gt;a clear non-goal: this is not deep RUM on every brochure domain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What usually does &lt;strong&gt;not&lt;/strong&gt; land on that same line item: unlimited regions and devices per URL, enterprise RUM seats, or a promise to monitor forty properties when the published site cap is twenty-five. Those gaps belong in a later tier or a thinner URL list, not in silent overages. Saying the non-goals out loud is cheaper than explaining a surprise invoice mid-engagement.&lt;/p&gt;
&lt;p&gt;Procurement still needs plain wording for the row. That is a different essay from the purchase shape itself; for translating milliseconds into finance language, see our Hashnode notes on procurement budgets and selling Core Web Vitals monitoring without calling the line item “latency.” Here the decision is simpler: either you are buying organisation-scoped scheduled coverage under a three-digit ceiling, or you are still assembling DIY layers and should say so in the SOW. Ambiguous wording is how “monitoring” gets approved as a free checker and then fails the first multi-client review.&lt;/p&gt;
&lt;h3 id=&quot;flat-organisation-caps-versus-per-site-maths&quot;&gt;Flat organisation caps versus per-site maths&lt;/h3&gt;
&lt;p&gt;The under-$100 question collapses to how capacity is counted, and the short table below is the decision frame agencies actually use when three vendor demos arrive in the same week.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Pricing shape &lt;/th&gt;&lt;th&gt;How capacity is counted &lt;/th&gt;&lt;th&gt;Fits under $100 when… &lt;/th&gt;&lt;th&gt;Breaks when…&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Per monitored URL / slot &lt;/td&gt;&lt;td&gt;Each URL × device × region consumes a slot &lt;/td&gt;&lt;td&gt;One client, deep lab work &lt;/td&gt;&lt;td&gt;Twenty clients each need homepage + checkout&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Per-site SaaS seat &lt;/td&gt;&lt;td&gt;Billing scales with domains &lt;/td&gt;&lt;td&gt;Portfolio stays tiny &lt;/td&gt;&lt;td&gt;Retainer count grows faster than tool budget&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Flat organisation cap &lt;/td&gt;&lt;td&gt;Sites, tests, workspaces pool under one subscription &lt;/td&gt;&lt;td&gt;Roster grows inside published limits &lt;/td&gt;&lt;td&gt;You exceed site or test caps and need the next tier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DIY scripts + API &lt;/td&gt;&lt;td&gt;Cash cost is low; labour is not &lt;/td&gt;&lt;td&gt;One engineer owns the pipeline &lt;/td&gt;&lt;td&gt;Quota, reporting, and on-call become unpaid platform work&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Agencies that stay under $100 with multi-tenant coverage almost always pick the flat organisation row and accept published caps. Per-slot tools can still win for a single high-traffic property; they are a poor match for “monitor the whole WordPress roster for under a hundred.” DIY wins on cash until the labour row on the same budget is honest. If you cannot name that labour owner, you are not really choosing DIY; you are deferring the purchase. Apogee Watcher’s Professional tier is built as that flat-cap shape under the $100 line at list pricing, with Agency as the published step when site or workspace caps bite. Current numbers live on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;pricing page&lt;/a&gt;, and the buyer story does not change when a list price moves by a few dollars: you are still choosing organisation caps versus per-site multiplication.&lt;/p&gt;
&lt;h3 id=&quot;when-diy-still-wins-the-budget&quot;&gt;When DIY still wins the budget&lt;/h3&gt;
&lt;p&gt;DIY PageSpeed Insights API scripts, Lighthouse CI, sheets, and a workflow tool still win the purchase when three conditions hold together: the portfolio is small enough that one person can keep URL lists accurate, nobody expects branded client PDFs from the monitoring layer itself, and the team already treats automation ownership as billable engineering time rather than free evenings.&lt;/p&gt;
&lt;p&gt;DIY stops winning the under-$100 comparison when any of these repeat: campaign URLs ship and never join the sheet; the digest dies on leave; account managers rebuild slides from CSV; “nightly for everyone” became “nightly for whoever we remembered” under API quota. At that point a flat-cap portfolio monitor is usually cheaper than the labour you were pretending was free, even when the SaaS line sits at $79 rather than $0. The cash line looks worse; the retainer maths usually looks better. DIY still fits as a layer after the portfolio monitor is in place (Lighthouse CI on templates you ship, manual PageSpeed Insights in teaching conversations), because the purchase under $100 is for the jobs DIY stopped covering reliably, not a mandate to delete every script.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Can an agency monitor forty client sites under $100/month?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Often not on a single flat-cap Professional-shaped plan if the published site limit is twenty-five. Realistic under-$100 strategies put priority origins on the paid monitor and leave long-tail pages on free diagnostics, or move to the next published tier when the roster honestly exceeds the cap. Pretending forty equals twenty-five is how retainers over-promise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is an AI agent that fixes Lighthouse issues the same as multi-tenant monitoring?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No: an agent remediates findings on sites you can change, while multi-tenant monitoring schedules checks, stores history, and alerts across many client organisations whether or not you control the repository. Most agencies need both jobs named separately rather than collapsing them into one “AI monitoring” line.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What should we put on the purchase order so finance understands the buy?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Organisation-scoped scheduled PageSpeed / Core Web Vitals coverage, included site and test caps, retention, seats, and reporting are the rows that usually clear finance. A three-digit portfolio monitor is a different purchase from a four-hundred-dollar RUM seat on one flagship property. The SOW reads better when it states what is out of scope as well as what is included.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does multi-tenant PageSpeed monitoring replace uptime tools?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No: uptime and SSL watches answer “is the site reachable,” while portfolio PageSpeed monitoring answers “are LCP, INP, and CLS drifting on money URLs.” Many agencies keep both, because green uptime does not mean green Core Web Vitals, and a green Lighthouse score does not mean the origin stayed up overnight.&lt;/p&gt;
&lt;h3 id=&quot;buy-the-portfolio-job-then-layer-the-rest&quot;&gt;Buy the portfolio job, then layer the rest&lt;/h3&gt;
&lt;p&gt;What agencies actually buy under $100 for multi-tenant PageSpeed monitoring is organisation-shaped coverage: flat caps, shared schedules, and history the account team can defend in a review. AI fix loops, free checkers, CI gates, and selective RUM stay in the stack as layers, yet they are not the same line item. The pricing maths and tier caps sit in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/affordable-multi-tenant-performance-monitoring-under-100?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=multi-tenant-pagespeed-under-100-agencies-buy&quot;&gt;Affordable Multi-Tenant Performance Monitoring Under $100/Month&lt;/a&gt;. For the product shape of many clients in one view, &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-multiple-client-sites-one-dashboard?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=multi-tenant-pagespeed-under-100-agencies-buy&quot;&gt;Multiple Client Sites, One Dashboard&lt;/a&gt; is the shorter walkthrough, and you can &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;run a check&lt;/a&gt; on a client URL before schedules move into a trial.&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pagespeed.web.dev/&quot;&gt;PageSpeed Insights&lt;/a&gt; (Google)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/speed/docs/insights/v5/get-started&quot;&gt;PageSpeed Insights API&lt;/a&gt; (Google)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;Apogee Watcher pricing&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/affordable-multi-tenant-performance-monitoring-under-100&quot;&gt;Affordable Multi-Tenant Performance Monitoring Under $100/Month&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-multiple-client-sites-one-dashboard&quot;&gt;Multiple Client Sites, One Dashboard&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/agency-pagespeed-stacks-2026&quot;&gt;3 Agency PageSpeed Stacks for 2026&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://devlog.apogeewatcher.com/multi-tenant-pagespeed-monitoring-under-100-what-agencies-buy&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30566/multi-tenant-pagespeed-monitoring-under-100-what-agencies-actually-buy</guid>
<pubDate>Sat, 10 Oct 2026 07:37:47 +0000</pubDate>
</item>
<item>
<title>What PageSpeed monitoring stack should an agency run in 2026?</title>
<link>https://coderlegion.com/30226/what-pagespeed-monitoring-stack-should-an-agency-run-in-2026</link>
<description>&lt;p&gt;Most agencies do not need three products on day one. Pick a default PageSpeed monitoring stack, then add RUM only where money URLs justify it.&lt;/p&gt;
&lt;p&gt;The roster hits twenty client sites, yet the “monitoring stack” is still a shared folder of PageSpeed Insights screenshots, one engineer’s n8n flow nobody else can edit, and a promise to check Core Web Vitals before the QBR. That is not a PageSpeed monitoring stack for agencies; it is a coping habit with a product roadmap glued on. The gap shows up the first time two account managers need last month’s LCP trend and only one person knows which sheet tab is current.&lt;/p&gt;
&lt;p&gt;What agencies should run in 2026 is usually simpler than the vendor shortlist suggests. A multi-site portfolio monitor works best as the system of record for lab history, budgets, and alert ownership, while DIY PageSpeed Insights scripts and Lighthouse CI stay as layers on sites you deploy. Paid real-user monitoring (RUM) belongs on the one or two flagship properties where field depth pays for itself; everything else is optional seasoning.&lt;/p&gt;
&lt;p&gt;The full recipes and failure modes live on our blog in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/agency-pagespeed-stacks-2026?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pagespeed-monitoring-stack-agency-2026&quot;&gt;3 Agency PageSpeed Stacks for 2026&lt;/a&gt;, and the feature checklists sit in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pagespeed-monitoring-stack-agency-2026&quot;&gt;Comparing PageSpeed Monitoring Tools&lt;/a&gt;. What follows here is the opinionated pick: which stack to run first when the account team asks for a decision, not another comparison table.&lt;/p&gt;
&lt;h3 id=&quot;what-a-pagespeed-monitoring-stack-is-for-agencies-&quot;&gt;What a PageSpeed monitoring stack is (for agencies)&lt;/h3&gt;
&lt;p&gt;A stack is a small set of jobs with named owners, not a shopping cart of logos. For agency work those jobs are predictable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scheduled lab coverage on money URLs (PageSpeed Insights or equivalent),&lt;/li&gt;
&lt;li&gt;history an account manager can open without asking engineering,&lt;/li&gt;
&lt;li&gt;budgets and alerts that fire for someone other than the person who wrote the cron,&lt;/li&gt;
&lt;li&gt;optional CI gates on templates you ship,&lt;/li&gt;
&lt;li&gt;optional RUM depth where a snippet and seat cost are justified,&lt;/li&gt;
&lt;li&gt;a clear split between uptime/health tools and Core Web Vitals work (they are not the same purchase).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of those jobs live only in one person’s head, you do not have a stack yet. You have a hero, and heroes do not scale across twenty organisations. Named owners in the tool turn the same checklist into something the account team can run without a private Slack ping.&lt;/p&gt;
&lt;h3 id=&quot;the-default-stack-most-agencies-should-run-in-2026&quot;&gt;The default stack most agencies should run in 2026&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; portfolio monitor first for multi-client PageSpeed / Core Web Vitals coverage, DIY and CI as supporting layers, RUM as a selective add-on.&lt;/p&gt;
&lt;p&gt;That default fits when you manage roughly ten or more client sites, sell performance or SEO retainers that mention LCP/INP/CLS, and cannot put a RUM snippet on every brochure domain. The monitor owns schedules, URL inventories, budgets, and shared history; Lighthouse CI stays in the repositories you control; PageSpeed Insights remains the lab dialect clients already recognise in Search Console conversations. Once those three layers are named, the weekly argument stops being “which vendor is best” and becomes “which job is failing.”&lt;/p&gt;
&lt;p&gt;Apogee Watcher is built for that portfolio shape, and peers exist in the same job class. The recommendation is the shape, not a claim that only one vendor can fill it, so layer rather than rip out: keep DebugBear, SpeedCurve, or Calibre where you already pay for flagship RUM, and skip pretending every client needs that seat maths on day one. The unpaid work is deciding which sites never get a snippet, not collecting logos.&lt;/p&gt;
&lt;p&gt;Buying “the best RUM” and rolling it out everywhere sounds tidy until seat cost, privacy reviews, and install friction kill portfolio coverage before the first amber LCP is fixed. Staying on DIY forever sounds frugal until URL lists drift, quotas collide, and reporting hours eat the retainer. The default sits between those failure modes on purpose.&lt;/p&gt;
&lt;h3 id=&quot;when-diy-pagespeed-insights-sheets-is-enough&quot;&gt;When DIY PageSpeed Insights + sheets is enough&lt;/h3&gt;
&lt;p&gt;DIY (PageSpeed Insights API or self-hosted Lighthouse, Lighthouse CI, Sheets or Notion, n8n or cron) is enough when the portfolio is small, one engineer already owns automation, and nobody expects branded client PDFs from the monitoring layer. It also fits when you only need merge gates on sites you deploy, with occasional API checks elsewhere. Outside that band, DIY becomes unpaid platform work dressed up as thrift.&lt;/p&gt;
&lt;p&gt;DIY stops being a sound primary system when any of these show up more than once a month:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;campaign URLs ship and nobody added them to the sheet,&lt;/li&gt;
&lt;li&gt;the digest email dies because the workflow owner is on leave,&lt;/li&gt;
&lt;li&gt;account managers rebuild slides from CSV because clients will not open the spreadsheet,&lt;/li&gt;
&lt;li&gt;“nightly for everyone” became “nightly for whoever we remembered” under API quota.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When those signals repeat, a portfolio monitor becomes the primary system and DIY stays as a layer. That is DIY versus managed PageSpeed without the false choice of deleting your pipelines: managed here means shared schedules and history, not abandoning Lighthouse CI on the templates you still ship. The roster can grow without the monitoring story living in one export again.&lt;/p&gt;
&lt;h3 id=&quot;when-to-add-rum-on-flagship-sites-only&quot;&gt;When to add RUM on flagship sites only&lt;/h3&gt;
&lt;p&gt;RUM earns its place when a single property funds deep field investigation: high revenue, stubborn INP, or a client who already bought SpeedCurve, DebugBear, or Calibre and needs you to operate it. The portfolio monitor still covers the long tail so brochure sites keep scheduled lab history, because paying for flagship depth while the rest of the roster goes dark is how retainers look busy and still miss regressions. Selective RUM is a budget decision, not a purity test.&lt;/p&gt;
&lt;p&gt;Waiting for perfect RUM coverage before any monitoring starts is the other trap. Multi-site PageSpeed monitoring fails when teams refuse to schedule anything until every domain has a snippet, while lab schedules plus CrUX context from PageSpeed Insights or Search Console (where eligible) already answer most retainer questions. Field depth is a zoom lens, not the only camera.&lt;/p&gt;
&lt;h3 id=&quot;jobs-the-stack-must-cover-without-a-vendor-matrix-&quot;&gt;Jobs the stack must cover (without a vendor matrix)&lt;/h3&gt;
&lt;p&gt;Treat the table below as a go-live checklist: if a row has no owner and no tool, the stack is incomplete, so fill those gaps before another logo appears on the slide.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Job &lt;/th&gt;&lt;th&gt;Default owner in 2026&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Scheduled lab on money URLs &lt;/td&gt;&lt;td&gt;Portfolio monitor (or DIY if still under ~10 priority URLs)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Merge protection on shipped templates &lt;/td&gt;&lt;td&gt;Lighthouse CI in your repos&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Shared history for account teams &lt;/td&gt;&lt;td&gt;Portfolio monitor (not a private n8n)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Budgets / alerts with named responders &lt;/td&gt;&lt;td&gt;Portfolio monitor&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Deep field waterfalls &lt;/td&gt;&lt;td&gt;RUM on 1–2 flagships only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reachability / SSL / status pages &lt;/td&gt;&lt;td&gt;Separate uptime suite (layer; do not confuse with CWV)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;When someone asks for “the best tools,” the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pagespeed-monitoring-stack-agency-2026&quot;&gt;tools comparison&lt;/a&gt; is the right hand-off. When they ask “what should we run next quarter?,” the default above plus the three recipes on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/agency-pagespeed-stacks-2026?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pagespeed-monitoring-stack-agency-2026&quot;&gt;agency PageSpeed stacks&lt;/a&gt; are the operational answer. The first question is research; the second is delivery.&lt;/p&gt;
&lt;h3 id=&quot;faq-1&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;What PageSpeed monitoring stack should an agency run in 2026?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For most multi-client agencies, a portfolio PageSpeed / Core Web Vitals monitor is the system of record, DIY scripts and Lighthouse CI stay as layers, and RUM sits only on flagship sites that justify the seat. DIY-only still fits if the roster is tiny and automation is already owned, so write that choice into the retainer scope and sales and delivery argue from the same default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is PageSpeed Insights alone enough?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As a diagnostic, yes. As the only continuous system for fifteen retainers, no: you still need schedules, history, budgets, and someone other than the founder to receive the alert.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do we need RUM on every client site?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Usually not. RUM belongs where revenue or stubborn field issues pay for it, while the long tail stays on scheduled lab monitoring and CrUX where Google publishes it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How is this different from a tools roundup?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A roundup lists features; a stack names jobs and a default order of purchase. Use both: pick the shape here, then validate vendors against a feature checklist.&lt;/p&gt;
&lt;h3 id=&quot;cta&quot;&gt;CTA&lt;/h3&gt;
&lt;p&gt;If the current “stack” is still screenshots and a private automation, this quarter is a good moment to make a portfolio monitor primary and write DIY exit criteria into the runbook. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=pagespeed-monitoring-stack-agency-2026&quot;&gt;A free Apogee Watcher trial&lt;/a&gt; is useful when you want multi-tenant schedules and budgets without rebuilding the sheet again, while RUM and uptime tools stay where they already earn their keep.&lt;/p&gt;
&lt;h3 id=&quot;references-1&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/agency-pagespeed-stacks-2026&quot;&gt;3 Agency PageSpeed Stacks for 2026 (DIY, Portfolio Monitor, RUM + Monitor)&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/speed/docs/insights/v5/get-started&quot;&gt;PageSpeed Insights API&lt;/a&gt; (Google)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;Lighthouse CI&lt;/a&gt; (GoogleChrome)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://devlog.apogeewatcher.com/what-pagespeed-monitoring-stack-should-agency-run-2026&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30226/what-pagespeed-monitoring-stack-should-an-agency-run-in-2026</guid>
<pubDate>Thu, 08 Oct 2026 06:55:49 +0000</pubDate>
</item>
<item>
<title>Google says on-call needs eight engineers. You have three.</title>
<link>https://coderlegion.com/30020/google-says-on-call-needs-eight-engineers-you-have-three</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Google's SRE book works out that a 24/7 on-call rotation needs at least eight engineers. A team of two or three cannot reach that number, so it has to change what the number is built from: which alerts page a person, which hours are covered, and how fast someone must answer. After that it needs one named person on call at any moment and a short escalation ladder behind them for the page nobody answers. A weekly count of pages shows whether it is working. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/on-call-rotation-small-team?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;original post&lt;/a&gt; has the same text with a short FAQ at the end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-we-are-not-google-&quot;&gt;&quot;We are not Google&quot;&lt;/h3&gt;
&lt;p&gt;Google's first SRE book has a chapter called &lt;a rel=&quot;nofollow&quot; href=&quot;https://sre.google/sre-book/being-on-call/&quot;&gt;Being On-Call&lt;/a&gt;. Readers wrote back. The follow-up book, the SRE Workbook, opens its own &lt;a rel=&quot;nofollow&quot; href=&quot;https://sre.google/workbook/on-call/&quot;&gt;on-call chapter&lt;/a&gt; with their feedback, and the first item on the list is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;We are not Google; we're much smaller. We don't have as many people in the rotation, and we don't have sites in different time zones. What you described in your first book is irrelevant to me.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;Uptimepage&lt;/a&gt;, which pages people when a monitor goes down, so I had to decide what a rotation should look like for a team that small. That reader is right about the numbers. The reasoning behind the numbers still works for three people, and it leads to a different setup.&lt;/p&gt;
&lt;h3 id=&quot;where-the-eight-comes-from&quot;&gt;Where the eight comes from&lt;/h3&gt;
&lt;p&gt;The book starts from a limit on time:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;we strive to invest at least 50% of SRE time into engineering: of the remainder, no more than 25% can be spent on-call&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then it does the arithmetic:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Assuming that there are always two people on-call (primary and secondary, with different duties), the minimum number of engineers needed for on-call duty from a single-site team is eight: assuming week-long shifts, each engineer is on-call (primary or secondary) for one week every month.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two people on call, each for at most a quarter of the time, makes eight. Run the same arithmetic on a small team, with one person on call at a time and week-long shifts:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Team size &lt;/th&gt;&lt;th&gt;Share of weeks each person is on call&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2 &lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;33%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;25%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8, with two on call at a time &lt;/td&gt;&lt;td&gt;25%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A team of four reaches Google's limit only by giving up the secondary. A team of three is over the limit before anything breaks, and a team of two is on call every other week.&lt;/p&gt;
&lt;p&gt;A schedule cannot fix that, because any rotation of three people still has three people in it. What the team can change is how often the pager goes off, during which hours, and how quickly it has to be answered.&lt;/p&gt;
&lt;p&gt;The limit exists because pages are expensive. The same chapter estimates that one incident, with its diagnosis, fix and follow-up, takes about six hours of work, which is why it allows at most two incidents in a 12-hour shift. A person on call every third week can live with that week if it is quiet. So the work for a small team is to make the week quiet, then make sure the rare page reaches someone.&lt;/p&gt;
&lt;h3 id=&quot;what-a-team-of-three-can-change&quot;&gt;What a team of three can change&lt;/h3&gt;
&lt;h4 id=&quot;decide-what-is-worth-a-page&quot;&gt;Decide what is worth a page&lt;/h4&gt;
&lt;p&gt;The Workbook is direct about this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Engineers shouldn't have to be at a computer and working on a problem within minutes of receiving a page unless there is a very good reason to do so.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Its Table 8-1 gives three examples of a realistic response:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Incident &lt;/th&gt;&lt;th&gt;Response time&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Revenue-impacting network outage &lt;/td&gt;&lt;td&gt;5 minutes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Customer order batch processing system stuck &lt;/td&gt;&lt;td&gt;30 minutes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Backups of a database for a pre-launch service are failing &lt;/td&gt;&lt;td&gt;Ticket (response during work hours)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Sort every alert you have into those three rows. Only the first two should reach a phone at night. The rest go to a channel or an inbox that somebody reads in the morning.&lt;/p&gt;
&lt;p&gt;An alert has to pass two more filters before it may page. It has to be real: one probe with a bad network path &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/stop-false-uptime-alerts?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;should not wake anyone&lt;/a&gt;. And it has to come with something to do. The SRE book's wording is &quot;All paging alerts should also be actionable.&quot; A page that can only be watched until it clears teaches people to ignore pages.&lt;/p&gt;
&lt;h4 id=&quot;name-one-person-at-a-time&quot;&gt;Name one person at a time&lt;/h4&gt;
&lt;p&gt;The usual start for a small team is a shared channel: every alert goes to &lt;code&gt;#alerts&lt;/code&gt;, and everyone is in it. During the day this works, because somebody is looking. At 03:00 five people receive the message, and each of them can reasonably think one of the other four has it.&lt;/p&gt;
&lt;p&gt;A rotation puts exactly one name on call at any moment. That person knows it is them, and everyone else knows it is not. The channel can still get a copy, but the page goes to the person.&lt;/p&gt;
&lt;p&gt;Hand off on a weekday morning, when both people are awake and can talk about what is still open. A handoff at midnight on Sunday means nobody mentions the disk that is filling up. Keep it at the same local time all year too, so the change does not move by an hour when the clocks do.&lt;/p&gt;
&lt;h4 id=&quot;split-the-day-before-you-split-the-week&quot;&gt;Split the day before you split the week&lt;/h4&gt;
&lt;p&gt;The Workbook does not expect one person to carry a full week around the clock:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;In our experience, 24 hours of on-call duty without reprieve isn't a sustainable setup.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Its suggestion fits a small team too: &quot;it would be better for two engineers to split a week of on-call, with one person on-call during the day and one on-call overnight.&quot;&lt;/p&gt;
&lt;p&gt;With three people that can be two rotations in one schedule. Working hours rotate weekly among all three. Nights and weekends rotate separately, with the order shifted by one, so nobody carries both in the same week.&lt;/p&gt;
&lt;p&gt;There is also a cheaper option: leave some hours uncovered on purpose. If your customers are businesses in one region and nothing you run loses data while it is down, a page at 02:00 may only move the fix from 08:00 to 02:30. Decide that as a team, then write it where customers can see it, in your &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/uptime-sla?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;SLA&lt;/a&gt; and on your status page. As a customer I would rather read a stated response window during European working hours than a 24/7 promise kept by one tired person.&lt;/p&gt;
&lt;p&gt;Do the arithmetic before you choose. A 99.9% target leaves &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/how-much-downtime-is-99-9-uptime?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;43 minutes a month&lt;/a&gt;. One night outage that waits six hours for the morning uses more than eight months of that budget. If the number you promised cannot absorb that, someone has to be on call at night.&lt;/p&gt;
&lt;h4 id=&quot;add-a-ladder-for-the-unanswered-page&quot;&gt;Add a ladder for the unanswered page&lt;/h4&gt;
&lt;p&gt;One name on call is one point of failure. Phones run out of battery, and a person who was paged twice last night can sleep through the third.&lt;/p&gt;
&lt;p&gt;An escalation policy is an ordered list of who gets paged next when a page goes unanswered. Level one pages when the incident opens, and each level waits a set time for an acknowledgement before the next one pages. Acknowledging stops the ladder.&lt;/p&gt;
&lt;p&gt;For three people a ladder can be this short:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The person on call. Wait 10 minutes.&lt;/li&gt;
&lt;li&gt;The other two. Wait 15 minutes.&lt;/li&gt;
&lt;li&gt;Everyone, on the loudest channel each person has.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;How long to wait comes from the response time you promised. The SRE book's typical values are &quot;5 minutes for user-facing or otherwise highly time-critical services, and 30 minutes for less time-sensitive systems&quot;. If level one has 5 minutes to answer, the wait before level two cannot be 30.&lt;/p&gt;
&lt;p&gt;This ladder is the small team's version of Google's secondary. Nobody is formally second on call, and the page still has somewhere to go. Watch how often level two fires. If it fires most weeks, the other two people are on call as well, whatever the schedule says.&lt;/p&gt;
&lt;h4 id=&quot;make-swaps-cheap&quot;&gt;Make swaps cheap&lt;/h4&gt;
&lt;p&gt;The Workbook again:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;No one can promise on Monday that they won't have the flu on Thursday.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On a team of eight a swap is easy to arrange. On a team of three every swap lands on one of two people, so it has to take a minute and leave no doubt about who holds the pager afterwards. That means an override on the schedule itself, for exact dates, which ends without anyone remembering to undo it. A chat message asking a colleague to cover Thursday changes nothing about where the page goes.&lt;/p&gt;
&lt;p&gt;Put the shifts in everyone's calendar as well. A person who sees an on-call week next to a dentist appointment three weeks ahead asks for the swap now instead of on the day.&lt;/p&gt;
&lt;h4 id=&quot;test-that-the-page-arrives&quot;&gt;Test that the page arrives&lt;/h4&gt;
&lt;p&gt;A schedule can name a person the page cannot reach. Their phone is on do-not-disturb, the chat app is muted at night, or the email address was never confirmed. Everything looks covered until the first night page arrives on a silent phone.&lt;/p&gt;
&lt;p&gt;So test it. On the first morning of a shift, send a test page to the person on call and have them acknowledge it from the phone they will have at night, with the settings they will have at night. Use at least one channel that is allowed to make noise through do-not-disturb, such as SMS from a number saved as an emergency contact, or a push app with an emergency priority.&lt;/p&gt;
&lt;p&gt;GitLab's 2017 outage shows what an untested path costs. The failure alerts for its backup job were sent every night and rejected by the receiving mail server, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/cron-jobs-fail-silently?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=inline&quot;&gt;nobody knew for months&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;count-the-pages&quot;&gt;Count the pages&lt;/h4&gt;
&lt;p&gt;Google's teams work to a number: &quot;We target a maximum of two incidents per on-call shift&quot;. The Workbook also describes a team with a budget of two paging incidents per shift that had been receiving five for a year. Their answer was a project to reduce the pages, approved by senior management.&lt;/p&gt;
&lt;p&gt;A small team needs that count more than a large one does, because each page lands on a third of the team. Once a week, look at how many pages there were, how many of them needed a person, and how long the first acknowledgement took. A page that needed nobody is an alert to fix or delete. A slow acknowledgement is a reason to look at the ladder and the paging channel before it is a reason to look at the person.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-fix&quot;&gt;What this does not fix&lt;/h3&gt;
&lt;p&gt;Two people are still on call every other week, and no ladder changes that. What is left is to cover fewer hours or fewer systems and tell customers which, and to pay for the nights. The Workbook's position is short: &quot;On-call usually implies some amount of out-of-hours work. We believe this work should be compensated.&quot;&lt;/p&gt;
&lt;p&gt;Four is the first team size where one person on call at a time meets the 25% line. If on-call is wearing down a team of three, that is a real argument for the fourth hire, and the table above is how to make it.&lt;/p&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Google, Site Reliability Engineering, chapter 11, &lt;a rel=&quot;nofollow&quot; href=&quot;https://sre.google/sre-book/being-on-call/&quot;&gt;Being On-Call&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Google, The Site Reliability Workbook, chapter 8, &lt;a rel=&quot;nofollow&quot; href=&quot;https://sre.google/workbook/on-call/&quot;&gt;On-Call&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;GitLab, &lt;a rel=&quot;nofollow&quot; href=&quot;https://about.gitlab.com/blog/postmortem-of-database-outage-of-january-31/&quot;&gt;Postmortem of database outage of January 31&lt;/a&gt;, 10 February 2017.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-uptimepage-fits-1&quot;&gt;Where Uptimepage fits&lt;/h3&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=cta&quot;&gt;Uptimepage&lt;/a&gt;. It checks your services from several regions, and when one goes down it pages whoever is on call.&lt;/p&gt;
&lt;p&gt;An &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/on-call-scheduling?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=cta&quot;&gt;on-call schedule&lt;/a&gt; holds one or more rotations, each on call at all hours or only in the hours you give it. Daily and weekly handoffs stay at the same local time across daylight saving. Overrides cover exact dates. Each person picks the channels that page them, and the schedule list names anyone on a rotation whom no channel can reach. An escalation policy is a ladder of levels with a wait on each, and acknowledging from the chat or email alert stops it. Each person gets a calendar link for their shifts, and the incident reports include mean time to acknowledge.&lt;/p&gt;
&lt;p&gt;One gap to know about: there is no phone-call channel yet. SMS, Pushover's emergency priority, Telegram and the chat apps are there, and if you already use PagerDuty you can put it on a level. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/incidents?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=on-call-rotation&amp;amp;utm_content=cta#on-call-schedules&quot;&gt;incident docs&lt;/a&gt; have the details. It is open source under AGPL, so it can also run on your own server.&lt;/p&gt;
&lt;p&gt;Whatever you use, write down the answer to one question. At 03:00 tonight, whose phone rings, and whose rings next if they do not answer?&lt;/p&gt;
&lt;p&gt;Now the question for the comments: how does a team of two or three cover nights where you work? I would like to hear from teams that leave some hours uncovered on purpose and say so on their status page.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30020/google-says-on-call-needs-eight-engineers-you-have-three</guid>
<pubDate>Tue, 06 Oct 2026 14:27:45 +0000</pubDate>
</item>
<item>
<title>Oh Dear alternative? Uptime green is not Core Web Vitals portfolio monitoring</title>
<link>https://coderlegion.com/29947/oh-dear-alternative-uptime-green-is-not-core-web-vitals-portfolio-monitoring</link>
<description>&lt;p&gt;Searching for an Oh Dear alternative usually lands you on more uptime tools. Green reachability is not a Core Web Vitals portfolio.&lt;/p&gt;
&lt;p&gt;The uptime dashboard is green, SSL is fine, and the status page is quiet, yet a client still forwards a PageSpeed Insights screenshot where mobile Largest Contentful Paint sits in Needs improvement. Nobody can explain how both truths coexist. Searching for an Oh Dear alternative feels like the next step, but that query usually returns more uptime and white-label status products, not a Core Web Vitals portfolio monitor.&lt;/p&gt;
&lt;p&gt;The gap is not that Oh Dear is weak at its job; it is treating reachability as proof that shoppers experience a fast page. Green uptime and healthy Core Web Vitals are different clocks, so conflating them is how retainers buy the wrong primary tool. The search phrase “Oh Dear alternative” then steers buyers back onto the uptime shelf when the screenshot was about experience.&lt;/p&gt;
&lt;h3 id=&quot;what-oh-dear-alternative-usually-means-in-search&quot;&gt;What “Oh Dear alternative” usually means in search&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ohdear.app/&quot;&gt;Oh Dear&lt;/a&gt; sits in a crowded agency niche: uptime checks, SSL and DNS health, broken-link crawls, status pages, and often a scheduled Lighthouse or response-time layer under one login. When agencies outgrow a free ping tool, “Oh Dear alternative” is a natural buying phrase. The search box does not know whether you meant “better status pages” or “finally watch LCP.”&lt;/p&gt;
&lt;p&gt;Open that SERP today and you mostly see other agency uptime suites: white-label status pages, client PDF uptime reports, multi-region pings, SSL and domain expiry. Those products compete on the uptime axis, and they are sensible shortlists when the failure mode is “the site returned an error” or “the certificate expired on Friday night.” That shelf is crowded for a reason: downtime still burns retainers.&lt;/p&gt;
&lt;p&gt;They are a weak answer when the failure mode is “HTTP 200, but LCP and Interaction to Next Paint are amber across the money path.” That job needs scheduled lab history, CrUX field context where Google publishes it, URL inventories that keep up with CMS churn, and budgets written in Core Web Vitals units. Uptime clones do not become that stack because the brochure says “performance,” so the buying phrase and the failure mode have to match before you shortlist.&lt;/p&gt;
&lt;h3 id=&quot;uptime-green-is-not-a-core-web-vitals-signal&quot;&gt;Uptime green is not a Core Web Vitals signal&lt;/h3&gt;
&lt;p&gt;Uptime answers whether this origin responded successfully from the checker’s vantage point, while Core Web Vitals answer how real Chrome users (field) or a throttled lab browser (Lighthouse / PageSpeed Insights) experienced load and interactivity on specific URLs. Both can be “true” on the same Tuesday and still describe different risks. That mismatch is what the green dashboard hides.&lt;/p&gt;
&lt;p&gt;A site can stay up while:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the LCP image ships late on mobile category templates,&lt;/li&gt;
&lt;li&gt;a new extension blocks the main thread on add-to-cart (INP),&lt;/li&gt;
&lt;li&gt;a promo bar shifts layout without reserved space (CLS),&lt;/li&gt;
&lt;li&gt;third-party tags grow for a campaign week that never shows as downtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of those failures flip a classic uptime monitor red, but they do show up in PageSpeed Insights lab runs, in CrUX when the URL is eligible, and in client SEO conversations that already use Google’s Good / Needs improvement / Poor bands. Calling the origin “healthy” because the ping is green trains account teams to ignore the metric set the retainer actually sells. The screenshot arrives anyway; the dashboard just never warned anyone.&lt;/p&gt;
&lt;p&gt;For the wider lab-versus-field map, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oh-dear-alternative-uptime-green-not-cwv-portfolio&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;. Synthetic uptime and synthetic Lighthouse are both lab-shaped, but they measure different artefacts, so keep the labels honest in the pack. Readers who skip that distinction spend the next quarter arguing about which green is “real.”&lt;/p&gt;
&lt;h3 id=&quot;what-oh-dear-class-suites-cover-and-what-they-leave-out-&quot;&gt;What Oh Dear-class suites cover (and what they leave out)&lt;/h3&gt;
&lt;p&gt;Oh Dear-class products are genuinely strong when you need one place for “is it up?”, certificate risk, and a client-facing status story. Many also run HTTP timing on the same fetch path, or a daily Lighthouse score on selected URLs, and those extras matter. They still sit beside uptime economics: pricing and product roadmaps usually scale with sites and check frequency, not with multi-URL Core Web Vitals portfolios and discovery.&lt;/p&gt;
&lt;p&gt;What they typically leave as a separate purchase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;continuous PageSpeed Insights history across many client organisations,&lt;/li&gt;
&lt;li&gt;CrUX field bands stored next to lab runs for the same URL set,&lt;/li&gt;
&lt;li&gt;automated discovery when a CMS adds templates you never pasted into the monitor list,&lt;/li&gt;
&lt;li&gt;budgets and alerts written primarily against LCP, INP, and CLS rather than millisecond TTFB on the uptime probe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Daily Lighthouse is closer to “pagespeed” than a ping, but it is still not the same as a portfolio programme built around Google’s PageSpeed Insights API and field context. Mixing “we have Lighthouse in the uptime tool” with “we monitor Core Web Vitals for SEO” without labelling the difference is how false confidence survives a QBR. One score on one URL is a check; a retainer needs a series.&lt;/p&gt;
&lt;p&gt;A fair pairwise feature split (Oh Dear versus a CWV-first portfolio tool) lives in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/oh-dear-vs-apogee-watcher-uptime-vs-cwv-portfolio?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oh-dear-alternative-uptime-green-not-cwv-portfolio&quot;&gt;Oh Dear vs Apogee Watcher: Uptime Suite vs Core Web Vitals Portfolio Monitoring&lt;/a&gt;. Use that matrix when you are already comparing vendors; what follows here is the job split before you open the spreadsheet. The spreadsheet is useless if you still think both columns measure the same risk.&lt;/p&gt;
&lt;h3 id=&quot;when-to-keep-the-uptime-suite-and-add-a-cwv-layer&quot;&gt;When to keep the uptime suite and add a CWV layer&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Keep (or replace within) the uptime axis when&lt;/strong&gt; the pager must fire because HTTPS is down, certificates expire, or clients need a branded status page. An Oh Dear alternative that stays on that axis is a valid purchase.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add a Core Web Vitals portfolio layer when&lt;/strong&gt; you sell performance retainers, report LCP/INP/CLS in client decks, or manage many origins where the expensive failure is slow-but-up. Layer rather than rip out. Agencies that already run Oh Dear (or Pingdom, Better Stack, UptimeRobot, and similar) usually need a second product for CWV depth, not a single suite that pretends both jobs are free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Replace Oh Dear only when&lt;/strong&gt; you never needed the uptime bundle and the trial proved you were paying uptime economics for a CWV-only brief. That is a narrower case than the search phrase suggests.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-split-for-retainers&quot;&gt;A practical split for retainers&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Job &lt;/th&gt;&lt;th&gt;Prefer&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Reachability, SSL, status pages &lt;/td&gt;&lt;td&gt;Oh Dear or another uptime / health suite&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cold-load lab + opportunity text &lt;/td&gt;&lt;td&gt;PageSpeed Insights lab or scheduled PSI-style monitoring&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SEO-facing field categories &lt;/td&gt;&lt;td&gt;CrUX via Search Console / PageSpeed Insights field&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Many client sites, CWV budgets, discovery &lt;/td&gt;&lt;td&gt;Multi-tenant PageSpeed / CWV portfolio monitor&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Write both truths in the pack with labels: “Uptime (reachability)” versus “Lab PageSpeed / CrUX (experience).” Green on the first column does not clear the second, and clients notice when the PDF pretends otherwise.&lt;/p&gt;
&lt;h3 id=&quot;faq-2&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Is searching for an Oh Dear alternative the right move for Core Web Vitals?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only if you also need a better uptime suite. If the pain is amber LCP on money pages while pings stay green, shortlist a CWV portfolio monitor instead of another status-page vendor. The search phrase and the failure mode have to match.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does Oh Dear’s Lighthouse check cover Core Web Vitals monitoring?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It covers a scheduled lab score on the URLs you configure, but it does not automatically equal a multi-client PageSpeed Insights programme with field context and discovery. Treat it as a useful signal on the uptime product, not as the whole CWV retainer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should we rip out Oh Dear and switch to a PageSpeed tool?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Usually no. Keep uptime where downtime is the risk, and add CWV monitoring where experience is the risk. Rip-and-replace only when the uptime bundle was never the job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do uptime tools say “performance” then?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because HTTP timing and Lighthouse scores are performance-adjacent. The word still collapses three jobs (reachability timing, lab audits, field Core Web Vitals) into one brochure line. Ask which artefact the alert fires on before you trust the label.&lt;/p&gt;
&lt;h3 id=&quot;what-to-do-next&quot;&gt;What to do next&lt;/h3&gt;
&lt;p&gt;A useful starting pass is to list every production origin on the retainer, mark which ones already have uptime coverage you trust, and note last lab LCP/INP/CLS (plus whether CrUX exists) for each money-path URL. If uptime is green and lab or field is amber, you do not need another status page first. You need a scheduled CWV layer and a report that names both clocks.&lt;/p&gt;
&lt;p&gt;For the pairwise Oh Dear versus portfolio decision, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/oh-dear-vs-apogee-watcher-uptime-vs-cwv-portfolio?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oh-dear-alternative-uptime-green-not-cwv-portfolio&quot;&gt;Oh Dear vs Apogee Watcher&lt;/a&gt;. For how synthetic lab fits beside real-user field data, keep &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oh-dear-alternative-uptime-green-not-cwv-portfolio&quot;&gt;Synthetic vs Real User Monitoring&lt;/a&gt; in the same pack. Two links, two jobs, one retainer language.&lt;/p&gt;
&lt;h3 id=&quot;references-2&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ohdear.app/&quot;&gt;Oh Dear&lt;/a&gt; (product)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/oh-dear-vs-apogee-watcher-uptime-vs-cwv-portfolio&quot;&gt;Oh Dear vs Apogee Watcher: Uptime Suite vs Core Web Vitals Portfolio Monitoring&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/search/docs/appearance/core-web-vitals&quot;&gt;Understanding Core Web Vitals and Google search results&lt;/a&gt; (Google Search Central)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://devlog.apogeewatcher.com/oh-dear-alternative-uptime-green-not-core-web-vitals-portfolio&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29947/oh-dear-alternative-uptime-green-is-not-core-web-vitals-portfolio-monitoring</guid>
<pubDate>Tue, 06 Oct 2026 06:58:33 +0000</pubDate>
</item>
<item>
<title>SpeedCurve vs Apogee Watcher: Multi-Tenant Dashboards at Agency Price</title>
<link>https://coderlegion.com/27087/speedcurve-vs-apogee-watcher-multi-tenant-dashboards-at-agency-price</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt; (now part of &lt;a rel=&quot;nofollow&quot; href=&quot;https://embrace.io/blog/speedcurve-joins-embrace/&quot;&gt;Embrace&lt;/a&gt;) is the tool many teams mean when they ask for a serious SpeedCurve alternative that still covers synthetic tests, real-user monitoring, and performance budgets. Public &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.speedcurve.com/pricing/&quot;&gt;SpeedCurve pricing&lt;/a&gt; starts at &lt;strong&gt;$90/month&lt;/strong&gt; on Starter, with Growth from &lt;strong&gt;$576/month&lt;/strong&gt;, scaled by RUM page views and synthetic checks. That package is strong when one product organisation owns a small set of high-traffic origins and wants lab data, RUM, and coaching culture in one place.&lt;/p&gt;&lt;p&gt;Apogee Watcher answers a different question: how do you keep scheduled PageSpeed coverage honest across many client sites when you cannot put a RUM snippet on every domain, and when per-check maths at SpeedCurve Growth rates would erase retainer margin? We run Google’s PageSpeed Insights API (Lighthouse lab plus CrUX where Google publishes field data) inside multi-tenant organisations with Admin, Manager, and Viewer roles, plus automated page discovery. We do not ship first-party RUM. Field context comes from CrUX in each result, not from our own JavaScript on the client’s origin.&lt;/p&gt;&lt;p&gt;The comparison below is about workflow fit for agencies, not about declaring a universal winner. Limits and prices change on both sides, so confirm each vendor’s site before you buy and treat any table as a snapshot rather than a contract. What follows stays useful only while those public pages match what sales quotes.&lt;/p&gt;&lt;h2 id=&quot;what-speedcurve-is-genuinely-good-at&quot; class=&quot;scroll-mt-24&quot;&gt;What SpeedCurve is genuinely good at&lt;/h2&gt;&lt;p&gt;SpeedCurve’s public positioning centres on three jobs: track a wide set of performance metrics, turn those metrics into fixes, and coach a performance culture across product, SEO, and marketing. Synthetic monitoring and Real User Monitoring sit on the same plan family. Marketing materials emphasise Core Web Vitals, Lighthouse-oriented lab work, user timing, competitor views, and correlating speed with business outcomes such as conversion and bounce.&lt;/p&gt;&lt;p&gt;For a product team with engineering ownership of a flagship site, that combination is compelling. You can set performance budgets, schedule synthetic checks, sample RUM so cost stays predictable, and escalate when real users degrade after a release. Growth and Enterprise tiers add longer retention, priority synthetic testing, private agents, and SSO according to &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.speedcurve.com/pricing/&quot;&gt;SpeedCurve’s pricing page&lt;/a&gt;. Teams that already treat web performance as a cross-functional programme often praise the depth and the human support model.&lt;/p&gt;&lt;p&gt;None of that is theatre. If your brief is “instrument our storefront, prove ROI to leadership, and keep RUM on the money pages,” SpeedCurve is a serious shortlist candidate. DebugBear occupies a similar premium lane; we compared that product separately in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/debugbear-vs-apogee-watcher-synthetic-monitoring-multi-site-teams&quot;&gt;DebugBear vs Apogee Watcher&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;where-agencies-feel-friction-on-speedcurve-pricing-and-multi-site-work&quot; class=&quot;scroll-mt-24&quot;&gt;Where agencies feel friction on SpeedCurve pricing and multi-site work&lt;/h2&gt;&lt;p&gt;Agency retainers rarely look like a single product org with one design system and one consent banner. They look like twenty WordPress and Shopify origins, uneven tag-manager access, and account managers who need read-only dashboards without burning synthetic quotas. That shape changes which line items on a SpeedCurve quote still make sense once you leave a flagship brand.&lt;/p&gt;&lt;p&gt;Friction shows up in three places.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Cost at portfolio scale.&lt;/strong&gt; Starter at $90/mo already sits above our Agency entry on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;Apogee Watcher pricing&lt;/a&gt; for many small shops, and Growth at $576/mo is a different budget class. Synthetic checks are counted by URL, browser, region, and frequency. A planner that monitors many URLs across many client sites multiplies quickly. RUM is billed on page views (with sampling), which fits owned traffic better than a long tail of low-traffic client microsites.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.apogeewatcher.com/blog-images/79-speedcurve-pricing.png&quot; alt=&quot;SpeedCurve public pricing page showing Starter from $90 per month, Growth from $576 per month, and Enterprise custom tiers with RUM and synthetic plan builder&quot;&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Instrumentation politics.&lt;/strong&gt; RUM needs a script on the origin. Consent, security review, and “we will not add another tag” are common blockers on client sites you do not fully control. Synthetic-only coverage still matters when you only have public URLs.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Multi-tenant operations.&lt;/strong&gt; SpeedCurve documents unlimited users and teams on published plans, which helps collaboration inside one company. Agencies also need clear organisation boundaries per client, role separation that protects quotas, and URL inventory that keeps up with CMS churn. Stale monitors look green while the live template set has moved on. That inventory problem is why we invested in automated discovery rather than assuming someone pastes every new route.&lt;/p&gt;&lt;p&gt;None of this says SpeedCurve is the wrong product. It says the job “premium RUM + synthetic for our brand” and the job “scheduled PSI coverage across a client portfolio” are different purchase decisions. Mixing those jobs on one invoice is where retainer margin usually disappears.&lt;/p&gt;&lt;h2 id=&quot;what-apogee-watcher-optimises-for&quot; class=&quot;scroll-mt-24&quot;&gt;What Apogee Watcher optimises for&lt;/h2&gt;&lt;p&gt;We optimise for breadth-first synthetic monitoring that agencies can run without per-client Google API keys or per-client RUM snippets:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;PageSpeed Insights API results stored over time (lab Lighthouse metrics plus CrUX when Google returns field data for the URL).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Multi-organisation, multi-site structure with Admin, Manager, and Viewer roles.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Automated page discovery from sitemaps and HTML crawl (see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-how-apogee-watcher-discovers-pages-automatically&quot;&gt;how we discover pages&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Performance budgets and email alerts on scheduled runs (Slack and webhook delivery are on the roadmap; confirm live channels on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features&quot;&gt;features&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Published tiers from Personal through Agency on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;pricing&lt;/a&gt;, designed so cost scales with organisation and test quotas rather than a surprise per-domain licence for every microsite.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Gaps we state plainly: no first-party RUM, no SpeedCurve-style private agents or conversion correlation suite in-product, and automated white-label client report generation is still shipping (do not treat it as live until &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features&quot;&gt;features&lt;/a&gt; says otherwise). When you need session-level INP attribution on an instrumented app, keep SpeedCurve, DebugBear, or another RUM vendor on that origin. Claiming otherwise would waste a trial week for both sides.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.apogeewatcher.com/blog-images/31-sites-portfolio-list.png&quot; alt=&quot;Apogee Watcher sites list showing multiple client domains, page counts, and scheduled test frequency in one organisation portfolio table&quot;&gt;&lt;/p&gt;&lt;p&gt;For how synthetic schedules sit beside real-user data in general, read &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;. For the wider tool taxonomy, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt;. Those two pieces stay useful after you shortlist SpeedCurve or Watcher, because they separate measurement jobs from vendor brands.&lt;/p&gt;&lt;h2 id=&quot;speedcurve-vs-apogee-watcher-side-by-side&quot; class=&quot;scroll-mt-24&quot;&gt;SpeedCurve vs Apogee Watcher: side-by-side&lt;/h2&gt;&lt;p&gt;Use this as a decision grid, not a contract. Verify numbers on each vendor’s site before you sign, and re-check after any Embrace or Watcher plan change. Sticker prices move faster than blog posts.&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Topic&lt;/th&gt;&lt;th&gt;SpeedCurve (typical public positioning)&lt;/th&gt;&lt;th&gt;Apogee Watcher&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Synthetic engine&lt;/td&gt;&lt;td&gt;Lab synthetic checks; Lighthouse-oriented metrics; scripting and regions via plan&lt;/td&gt;&lt;td&gt;PageSpeed Insights API (Lighthouse lab + CrUX where available)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Real-user / field data&lt;/td&gt;&lt;td&gt;First-party RUM plus synthetic; sampling to control cost&lt;/td&gt;&lt;td&gt;CrUX in PSI results; no first-party RUM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entry pricing (public list)&lt;/td&gt;&lt;td&gt;Starter from **$90/mo**; Growth from **$576/mo**; Enterprise custom ([pricing](https://www.speedcurve.com/pricing/))&lt;/td&gt;&lt;td&gt;**$9/mo** Personal through **$199/mo** Agency ([pricing](https://apogeewatcher.com/pricing))&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Billing drivers&lt;/td&gt;&lt;td&gt;RUM page views + synthetic check volume&lt;/td&gt;&lt;td&gt;Organisation / site / test quota tiers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Multi-site / multi-tenant&lt;/td&gt;&lt;td&gt;Strong for product teams; unlimited users/teams on published plans&lt;/td&gt;&lt;td&gt;Organisations, sites, pages, RBAC built for agency portfolios&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Page discovery&lt;/td&gt;&lt;td&gt;Configure URLs and sites in product workflows&lt;/td&gt;&lt;td&gt;Automated sitemap + crawl paths&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Alerts&lt;/td&gt;&lt;td&gt;Budgets and alerts in product (see SpeedCurve docs)&lt;/td&gt;&lt;td&gt;Email today; more channels per roadmap&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Coaching / culture&lt;/td&gt;&lt;td&gt;Explicit performance-culture and consult positioning&lt;/td&gt;&lt;td&gt;Product-led monitoring; no bundled coaching bench&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Best day-one use&lt;/td&gt;&lt;td&gt;Deep RUM + synthetic on owned high-traffic sites&lt;/td&gt;&lt;td&gt;Portfolio PSI monitoring without client-side scripts&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Treo is another CrUX-heavy option for field exploration; we covered that split in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/treo-vs-apogee-watcher-crux-agency-monitoring&quot;&gt;Treo vs Apogee Watcher&lt;/a&gt;. GTmetrix remains useful for deep lab forensics on one URL; see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/gtmetrix-vs-apogee-watcher-pagespeed-monitoring-agencies&quot;&gt;GTmetrix vs Apogee Watcher&lt;/a&gt;. Neither replaces a SpeedCurve RUM programme on an owned storefront, and neither replaces portfolio PSI schedules on twenty unrelated CMS stacks.&lt;/p&gt;&lt;h2 id=&quot;when-speedcurve-is-the-better-primary-choice&quot; class=&quot;scroll-mt-24&quot;&gt;When SpeedCurve is the better primary choice&lt;/h2&gt;&lt;p&gt;Choose SpeedCurve as the primary monitor when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;First-party RUM is non-negotiable on sites you control (logged-in journeys, element-level INP work, conversion correlation).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Leadership already expects SpeedCurve-style dashboards and competitor benchmarks for a small set of revenue URLs.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;You can fund Starter or Growth against one brand (or a tiny brand set) and engineering will maintain scripts and budgets.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Private agents, SSO, or long retention are part of the procurement checklist.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In those cases a “SpeedCurve alternative” search often ends with staying on SpeedCurve, or pairing it with a cheaper portfolio layer rather than ripping it out.&lt;/p&gt;&lt;h2 id=&quot;when-apogee-watcher-is-the-better-speedcurve-alternative&quot; class=&quot;scroll-mt-24&quot;&gt;When Apogee Watcher is the better SpeedCurve alternative&lt;/h2&gt;&lt;p&gt;Choose Watcher as the primary portfolio layer when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;You manage many client hosts and need scheduled mobile and desktop checks with CrUX context, without placing RUM snippets everywhere.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;SpeedCurve pricing at Growth-scale check volumes would consume the monitoring line item for the whole book of business.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Automated discovery matters because marketing ships new templates faster than anyone updates monitor lists.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;You need Admin / Manager / Viewer separation so account managers can read results without changing quotas.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If the pain is “we still run Monday PSI by hand,” start with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt;. Budgets and thresholds are covered in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/the-complete-guide-to-performance-budgets-for-web-teams&quot;&gt;The Complete Guide to Performance Budgets for Web Teams&lt;/a&gt;. Those guides assume you already know which vendor owns RUM versus which owns the portfolio calendar.&lt;/p&gt;&lt;h2 id=&quot;layer-speedcurve-and-watcher-instead-of-replacing-either&quot; class=&quot;scroll-mt-24&quot;&gt;Layer SpeedCurve and Watcher instead of replacing either&lt;/h2&gt;&lt;p&gt;Most mature agencies do not need a religious pick. A common pattern is SpeedCurve (or DebugBear) on one or two flagship properties where RUM and deep diagnostics pay for themselves, while Watcher carries scheduled PSI coverage across the long tail of client sites. That is the same “layer, don’t replace” stance we take against DIY Lighthouse CI and free one-off tools.&lt;/p&gt;&lt;p&gt;Watcher does not replace SpeedCurve’s RUM attribution, private agents, or coaching bench. SpeedCurve is not priced or shaped as a low-overhead multi-tenant PSI portfolio for dozens of unrelated CMS stacks. Buy the job you have, then add the second tool only where the first leaves a hole.&lt;/p&gt;&lt;p&gt;Try a multi-page sample without an account at &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;apogeewatcher.com/check&lt;/a&gt;, or &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;start a free Apogee Watcher account&lt;/a&gt; and schedule a baseline on your noisiest client this week.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;FAQ&lt;/h2&gt;&lt;h3&gt;Is Apogee Watcher a full SpeedCurve alternative?&lt;/h3&gt;&lt;p&gt;For portfolio synthetic monitoring with CrUX-in-PSI, multi-tenant roles, and lower entry pricing, yes as a category alternative. For first-party RUM, conversion correlation, and SpeedCurve’s coaching model, no. Keep SpeedCurve (or another RUM product) where those jobs matter.&lt;/p&gt;&lt;h3&gt;How does SpeedCurve pricing compare at agency scale?&lt;/h3&gt;&lt;p&gt;Public list prices start at $90/mo (Starter) and $576/mo (Growth), with usage driven by RUM page views and synthetic checks. Watcher publishes $9–$199/mo tiers with organisation and test quotas. Compare licence cost plus the hours you spend maintaining URL lists and client decks, not the sticker alone.&lt;/p&gt;&lt;h3&gt;Does Watcher include real-user monitoring like SpeedCurve RUM?&lt;/h3&gt;&lt;p&gt;No. Watcher shows CrUX field metrics when Google returns them for the tested URL. We do not inject a first-party RUM script. If you need session-level RUM beyond CrUX, use SpeedCurve or a peer RUM tool on those origins.&lt;/p&gt;&lt;h3&gt;Can we run SpeedCurve and Apogee Watcher together?&lt;/h3&gt;&lt;p&gt;Yes. Many teams keep premium RUM+synthetic on flagship sites and use Watcher for breadth across the rest of the portfolio. Neither product requires you to rip out the other.&lt;/p&gt;&lt;h3&gt;Does SpeedCurve replace Google PageSpeed Insights?&lt;/h3&gt;&lt;p&gt;No. SpeedCurve is a commercial monitoring platform. PSI remains a useful free diagnostic and the API behind Watcher’s scheduled runs. See our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;tools comparison for agencies&lt;/a&gt; for how diagnostics and monitoring differ.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;Choose the primary tool by job: RUM-depth on owned sites versus multi-tenant PSI coverage across clients. Confirm SpeedCurve pricing and Watcher limits on the live pricing pages, then pilot two weeks on the sites that create the most noise in Slack. A short pilot beats a twelve-month commitment made from a feature grid alone.&lt;/p&gt;&lt;p&gt;For hosted multi-site monitoring without client-side scripts or per-client API keys, &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;start free on Apogee Watcher&lt;/a&gt;.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;References&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.speedcurve.com/&quot;&gt;SpeedCurve&lt;/a&gt; (product home)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.speedcurve.com/pricing/&quot;&gt;SpeedCurve pricing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://embrace.io/blog/speedcurve-joins-embrace/&quot;&gt;SpeedCurve joins Embrace&lt;/a&gt; (Embrace)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/debugbear-vs-apogee-watcher-synthetic-monitoring-multi-site-teams&quot;&gt;DebugBear vs Apogee Watcher: Synthetic Monitoring for Multi-Site Teams&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/treo-vs-apogee-watcher-crux-agency-monitoring&quot;&gt;Treo vs Apogee Watcher: CrUX Scale, Competitor Benchmarks, and Agency Portfolios&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/gtmetrix-vs-apogee-watcher-pagespeed-monitoring-agencies&quot;&gt;GTmetrix vs Apogee Watcher: PageSpeed Monitoring for Agencies Compared&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring: When Manual Checks Aren't Enough&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-how-apogee-watcher-discovers-pages-automatically&quot;&gt;Product Spotlight: How Apogee Watcher Discovers Pages Automatically&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/the-complete-guide-to-performance-budgets-for-web-teams&quot;&gt;The Complete Guide to Performance Budgets for Web Teams&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;Apogee Watcher pricing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features&quot;&gt;Apogee Watcher features&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27087/speedcurve-vs-apogee-watcher-multi-tenant-dashboards-at-agency-price</guid>
<pubDate>Tue, 15 Sep 2026 07:04:00 +0000</pubDate>
</item>
<item>
<title>ICMP vs TCP vs UDP: the difference, explained for developers</title>
<link>https://coderlegion.com/27050/icmp-vs-tcp-vs-udp-the-difference-explained-for-developers</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; ICMP, TCP and UDP all travel inside IP packets, but they ask different questions. ICMP echo (ping) asks &quot;does this address answer at all?&quot; and needs no port. TCP asks &quot;is a program listening on this port?&quot; and gets a clear yes, a clear no, or silence. UDP asks nothing by itself: you only learn something if the program on the other side chooses to reply. A closed port answers differently on each one, and that difference is most of what a network check can and cannot tell you. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/icmp-vs-tcp-vs-udp?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=inline&quot;&gt;original post&lt;/a&gt; has the same text with a collapsible FAQ and the RFC list at the end.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;one-address-three-ways-to-knock&quot;&gt;One address, three ways to knock&lt;/h3&gt;
&lt;p&gt;Every packet on the internet is an IP packet. IP knows how to move a packet from one address to another and nothing else. One byte in the IP header says what is inside, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml&quot;&gt;IANA keeps the list&lt;/a&gt;: ICMP is protocol 1, TCP is protocol 6, UDP is protocol 17. The three are siblings: same addresses, same routers, and a different promise about what happens once the packet arrives.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-icmp-tcp-udp-exchanges.webp&quot; alt=&quot;ICMP echo, TCP three-way handshake and UDP datagram drawn as three lanes between you and a host, each ending with what silence means on that protocol.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Three ways to knock on one address. The last row of each lane is what silence means there.&lt;/em&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;ICMP &lt;/th&gt;&lt;th&gt;TCP &lt;/th&gt;&lt;th&gt;UDP&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Specification &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc792&quot;&gt;RFC 792&lt;/a&gt; (1981), &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4443&quot;&gt;RFC 4443&lt;/a&gt; for IPv6 &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9293&quot;&gt;RFC 9293&lt;/a&gt; (2022) &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc768&quot;&gt;RFC 768&lt;/a&gt; (1980)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;IP protocol number &lt;/td&gt;&lt;td&gt;1 (58 for ICMPv6) &lt;/td&gt;&lt;td&gt;6 &lt;/td&gt;&lt;td&gt;17&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Ports &lt;/td&gt;&lt;td&gt;none &lt;/td&gt;&lt;td&gt;source and destination &lt;/td&gt;&lt;td&gt;source and destination&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Handshake &lt;/td&gt;&lt;td&gt;none &lt;/td&gt;&lt;td&gt;three packets before any data &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Delivery guarantee &lt;/td&gt;&lt;td&gt;none &lt;/td&gt;&lt;td&gt;ordered, complete, retransmitted &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Header &lt;/td&gt;&lt;td&gt;8 bytes for an echo &lt;/td&gt;&lt;td&gt;20 bytes minimum &lt;/td&gt;&lt;td&gt;8 bytes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Typical users &lt;/td&gt;&lt;td&gt;ping, traceroute, error reports &lt;/td&gt;&lt;td&gt;HTTP/1.1 and HTTP/2, TLS, SSH, SQL, SMTP &lt;/td&gt;&lt;td&gt;DNS, NTP, QUIC and HTTP/3, video, games&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;icmp-the-network-talking-about-itself&quot;&gt;ICMP: the network talking about itself&lt;/h3&gt;
&lt;p&gt;ICMP is not a transport. RFC 792 says it plainly: ICMP &quot;is actually an integral part of IP, and must be implemented by every IP module.&quot; It exists so that routers and hosts can report on delivery: this destination is unreachable, that packet ran out of hops. ICMP messages have a type and a code but no ports, because nothing sits on top of ICMP waiting for them. The IP stack consumes them itself.&lt;/p&gt;
&lt;p&gt;The one ICMP message everybody has typed is the echo. &lt;code&gt;ping&lt;/code&gt; sends an echo request (type 8, or 128 in ICMPv6) and the far host's kernel sends back an echo reply (type 0, or 129). No program had to be running. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc1122&quot;&gt;RFC 1122&lt;/a&gt;, the host requirements standard, makes it a rule: &quot;Every host MUST implement an ICMP Echo server function that receives Echo Requests and sends corresponding Echo Replies.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ping -c 1 uptimepage.dev
PING uptimepage.dev (204.168.246.94): 56 data bytes
64 bytes from 204.168.246.94: icmp_seq=0 ttl=50 time=96.069 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A reply proves two things: the address belongs to a machine that is switched on, and packets can travel there and back. It also gives you the round-trip time for free.&lt;/p&gt;
&lt;p&gt;That is all a ping proves. It says nothing about whether a web server, a database or anything else is running, and a host can answer every ping while every service on it is dead.&lt;/p&gt;
&lt;p&gt;Two things about ICMP surprise people. The first is that no reply does not mean down. Firewalls drop ICMP all the time. On AWS a fresh security group blocks it, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-rules-reference.html&quot;&gt;documentation&lt;/a&gt; says so: &quot;To ping your instance, you must add one of the following inbound ICMP rules.&quot; Before you rely on ping for a host, confirm the host answers a ping at all.&lt;/p&gt;
&lt;p&gt;The second is that sending ICMP needs privilege. A raw socket needs &lt;code&gt;CAP_NET_RAW&lt;/code&gt;. Linux has an unprivileged alternative, the ICMP datagram socket, but the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.kernel.org/networking/ip-sysctl.html&quot;&gt;kernel documentation&lt;/a&gt; says the default &lt;code&gt;ping_group_range&lt;/code&gt; is &quot;1 0&quot;, &quot;meaning, that nobody (not even root) may create ping sockets.&quot; Docker opens that range in every container that gets its own network namespace, which is why ping works there without anyone thinking about it; a container on &lt;code&gt;--network host&lt;/code&gt; inherits the host's setting instead. I learned the limits of that the slow way: the same probe binary that pinged fine in Docker could not open an ICMP socket at all on a Firecracker microVM, until I gave it &lt;code&gt;CAP_NET_RAW&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;tcp-a-promise-before-the-first-byte&quot;&gt;TCP: a promise before the first byte&lt;/h3&gt;
&lt;p&gt;TCP is a connection. Before either side sends a byte of data, the two ends run the three-way handshake: SYN, SYN-ACK, ACK. RFC 9293, which replaced the 1981 specification in 2022, calls it &quot;the procedure used to establish a connection.&quot; It costs one round trip, and that round trip pays for every promise TCP makes afterwards: bytes arrive in order, lost segments are sent again, the receiver is not flooded (flow control), and the network is not flooded (congestion control). To the program it looks like a stream. You write bytes on one side and read the same bytes, in the same order, on the other.&lt;/p&gt;
&lt;p&gt;This is what HTTP/1.1, HTTP/2, TLS, SSH, PostgreSQL, MySQL, SMTP and most of what you deploy run on.&lt;/p&gt;
&lt;p&gt;The handshake is also a good probe, because a SYN gets one of three answers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SYN-ACK.&lt;/strong&gt; A program is listening. The connect succeeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RST.&lt;/strong&gt; Nothing is listening. RFC 9293 says that if the connection does not exist, &quot;a reset is sent in response to any incoming segment except another reset.&quot; Your connect fails at once with &quot;connection refused&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing.&lt;/strong&gt; A firewall dropped the SYN, or the host is gone. The kernel keeps retrying. On Linux the default &lt;code&gt;tcp_syn_retries&lt;/code&gt; is 6, and the kernel documentation puts the final timeout for an active connection attempt at 131 seconds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here are all three from my laptop. The last one has a three second limit; without it macOS waits 75 seconds.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ nc -zv uptimepage.dev 443
Connection to uptimepage.dev port 443 [tcp/https] succeeded!

$ nc -zv 127.0.0.1 9
nc: connectx to 127.0.0.1 port 9 (tcp) failed: Connection refused

$ nc -zv -G 3 uptimepage.dev 8443
nc: connectx to uptimepage.dev port 8443 (tcp) failed: Operation timed out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nmap.org/book/man-port-scanning-basics.html&quot;&gt;Nmap's documentation&lt;/a&gt; gives these states the names everyone uses: open, closed and filtered.&lt;/p&gt;
&lt;p&gt;What a successful handshake proves: a process accepted a connection on that port. Not that it is healthy. A database that accepts connections and then rejects every query still passes. That is why a TCP check belongs on things that have no HTTP surface (SSH, SMTP, a database port) and an HTTP check belongs on things that do.&lt;/p&gt;
&lt;h3 id=&quot;udp-a-message-nothing-more&quot;&gt;UDP: a message, nothing more&lt;/h3&gt;
&lt;p&gt;UDP is the smallest transport there is. RFC 768 fits on three pages. It adds a source port, a destination port, a length and a checksum to an IP packet, 8 bytes in total, and stops. The RFC says: &quot;The protocol is transaction oriented, and delivery and duplicate protection are not guaranteed.&quot; There is no handshake, no acknowledgement, no ordering and no retransmission, and no congestion control either. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8085&quot;&gt;RFC 8085&lt;/a&gt; opens by saying UDP &quot;has no inherent congestion control mechanisms&quot; and then spends more than fifty pages telling application designers how to add their own.&lt;/p&gt;
&lt;p&gt;Why anyone uses it: latency and control. A DNS lookup is one packet out and one packet back, no handshake. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc1035&quot;&gt;RFC 1035&lt;/a&gt; capped a DNS answer over UDP at 512 bytes. EDNS(0) (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc6891&quot;&gt;RFC 6891&lt;/a&gt;) later let a client advertise a bigger buffer, so most answers fit in one datagram today; one that still does not fit is cut short with the TC bit set, and the client asks again over TCP (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7766&quot;&gt;RFC 7766&lt;/a&gt;). NTP, most game traffic and real-time video use UDP because a late packet is worth less than the next one, so waiting for a retransmission is the wrong trade. And QUIC, the transport under HTTP/3, is built entirely on UDP. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9000&quot;&gt;RFC 9000&lt;/a&gt; states that &quot;QUIC packets are carried in UDP datagrams&quot; and gives the reason as &quot;to better facilitate deployment in existing systems and networks&quot;. In practice that means QUIC does its own handshake, loss recovery and congestion control in the application, and can change them without waiting for every operating system to update its TCP stack.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dig @1.1.1.1 uptimepage.dev A +noall +answer +stats
; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.10.6 &amp;lt;&amp;lt;&amp;gt;&amp;gt; @1.1.1.1 uptimepage.dev A +noall +answer +stats
; (1 server found)
;; global options: +cmd
uptimepage.dev.3600INA204.168.246.94
;; Query time: 125 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Wed Sep 02 11:40:44 EEST 2026
;; MSG SIZE  rcvd: 59
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One datagram out, one datagram back carrying a 59-byte answer (67 bytes with the UDP header), and no handshake before it. Over TCP the same lookup costs two round trips, one for the handshake and one for the query, which is why DNS defaults to UDP.&lt;/p&gt;
&lt;p&gt;Now the probing problem. Send a UDP datagram to a port and what comes back?&lt;/p&gt;
&lt;p&gt;If nothing listens there, the host's IP stack should answer with an ICMP error. RFC 1122 again: &quot;If a datagram arrives addressed to a UDP port for which there is no pending LISTEN call, UDP SHOULD send an ICMP Port Unreachable message.&quot; So &quot;closed&quot; is detectable, and it is ICMP that detects it.&lt;/p&gt;
&lt;p&gt;If something listens, you get whatever that program chooses to send. A DNS server sends an answer. A syslog receiver sends nothing, ever. So silence means open, or filtered, or dead, and Nmap has a state for this, &lt;code&gt;open|filtered&lt;/code&gt;. Its documentation explains: &quot;This occurs for scan types in which open ports give no response.&quot;&lt;/p&gt;
&lt;p&gt;The consequence is that a generic &quot;UDP port check&quot; does not exist. To check a UDP service you have to speak its protocol: send a real DNS query and read the answer, send a real NTP request and check the timestamp. This is why monitoring tools have a DNS check and a TCP check but rarely a UDP check.&lt;/p&gt;
&lt;h3 id=&quot;the-same-closed-door-three-answers&quot;&gt;The same closed door, three answers&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;ICMP echo &lt;/th&gt;&lt;th&gt;TCP SYN &lt;/th&gt;&lt;th&gt;UDP datagram&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Service up &lt;/td&gt;&lt;td&gt;echo reply &lt;/td&gt;&lt;td&gt;SYN-ACK &lt;/td&gt;&lt;td&gt;the program's reply, or nothing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Port closed, host up &lt;/td&gt;&lt;td&gt;echo reply (ICMP has no ports) &lt;/td&gt;&lt;td&gt;RST, &quot;connection refused&quot; &lt;/td&gt;&lt;td&gt;ICMP port unreachable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Firewall drops the packet &lt;/td&gt;&lt;td&gt;silence &lt;/td&gt;&lt;td&gt;silence, retried for about two minutes &lt;/td&gt;&lt;td&gt;silence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Host gone &lt;/td&gt;&lt;td&gt;silence, or an unreachable error from the last router &lt;/td&gt;&lt;td&gt;silence, or that same ICMP error surfacing as &quot;no route to host&quot; &lt;/td&gt;&lt;td&gt;silence, or that same ICMP error&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Read the table by columns. TCP is the only one where &quot;closed&quot; and &quot;filtered&quot; look different on their own, UDP borrows ICMP to say &quot;closed&quot;, and ICMP cannot see ports at all. Silence is the ambiguous answer on all three, which is why no check should conclude &quot;down&quot; from one silent probe sent from one place. Two or three consecutive misses, seen from more than one network, is the honest bar. I wrote about that in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/stop-false-uptime-alerts?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=inline&quot;&gt;how to stop false uptime alerts&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-this-means-for-a-monitor&quot;&gt;What this means for a monitor&lt;/h3&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=inline&quot;&gt;Uptimepage&lt;/a&gt;, and its check types map onto these questions, so this is how the theory turns into a config.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ping sends one ICMP echo with a 32-byte payload, the size Windows &lt;code&gt;ping&lt;/code&gt; uses (Unix &lt;code&gt;ping&lt;/code&gt; sends 56, as the transcript above shows), so middleboxes see ordinary diagnostic traffic. It tries the first IPv4 and the first IPv6 address of the host and splits the timeout between them. Silence for the whole budget is down, because an echo has no way to refuse. Use it for routers, gateways and hosts that expose no service.&lt;/li&gt;
&lt;li&gt;TCP runs the handshake and closes the connection. Accepted is up, refused is down, and a timeout is recorded as an error with the time it took. An error counts as a failed check for alerting exactly like down, so a firewalled port still opens an incident once the confirmation threshold is met. Use it for databases, brokers, SSH and mail, where &quot;a process accepted the connection&quot; is the most you can assert from outside.&lt;/li&gt;
&lt;li&gt;DNS sends a real query and checks the answer's content, so it is the UDP check for the one UDP service almost everyone depends on. It can point at a specific resolver, which matters when you want that server's view and not your cache's.&lt;/li&gt;
&lt;li&gt;HTTP is a TCP handshake, then TLS, then a request, and it records the time spent in each phase: DNS, connect, TLS and time to first byte. The connect phase is the handshake from this article, measured inside a bigger check.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick the check that asks the question you care about, and remember what each one cannot see. There is a fuller version of that decision in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/docs/monitor-types?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=inline&quot;&gt;monitor types documentation&lt;/a&gt;, and a story-shaped version in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/osi-layers?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=inline&quot;&gt;the mystery of the &quot;down&quot; website&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;common-questions&quot;&gt;Common questions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Is ICMP a transport protocol like TCP and UDP?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. ICMP is a control protocol that belongs to IP itself, and RFC 792 says every IP module must implement it. It has no ports and carries no application data. It reports on delivery: a destination is unreachable, a packet ran out of hops, and the echo pair that ping uses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does ping work while my website is down?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because ping only proves that the host's IP stack answers and that packets can travel there and back. The kernel sends the echo reply, so no web server, database or other program has to be running. A host can answer every ping while every service on it is dead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does a closed TCP port fail at once but a firewalled one hangs?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A closed port sends back a TCP reset, so the connect fails immediately with connection refused. A firewall that drops the SYN sends nothing, and the kernel keeps retrying. On Linux the default is six retries, and the kernel documentation puts the final timeout at 131 seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I check a UDP port the way I check a TCP port?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No, because an open UDP port sends nothing back unless the program behind it chooses to answer. A closed port is detectable, since the host should reply with an ICMP port unreachable message, but open and filtered both look like silence. To check a UDP service you have to speak its protocol, for example send a real DNS query.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If UDP is unreliable, why does HTTP/3 use it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UDP itself gives no delivery guarantee, and QUIC adds its own on top. QUIC runs inside UDP datagrams and does its own handshake, retransmission and congestion control in the application, which lets it evolve without waiting for every operating system to update its TCP stack.&lt;/p&gt;
&lt;h3 id=&quot;sources-1&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc792&quot;&gt;RFC 792: Internet Control Message Protocol&lt;/a&gt;, September 1981.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4443&quot;&gt;RFC 4443: ICMPv6 for the Internet Protocol Version 6&lt;/a&gt;, March 2006.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc768&quot;&gt;RFC 768: User Datagram Protocol&lt;/a&gt;, August 1980.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9293&quot;&gt;RFC 9293: Transmission Control Protocol&lt;/a&gt;, August 2022.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc1122&quot;&gt;RFC 1122: Requirements for Internet Hosts, Communication Layers&lt;/a&gt;, October 1989. Sections 3.2.2.6 and 4.1.3.1.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8085&quot;&gt;RFC 8085: UDP Usage Guidelines&lt;/a&gt;, March 2017.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc1035&quot;&gt;RFC 1035: Domain Names, Implementation and Specification&lt;/a&gt;, November 1987. Section 4.2.1.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc6891&quot;&gt;RFC 6891: Extension Mechanisms for DNS (EDNS(0))&lt;/a&gt;, April 2013.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7766&quot;&gt;RFC 7766: DNS Transport over TCP, Implementation Requirements&lt;/a&gt;, March 2016.&lt;/li&gt;
&lt;li&gt;IETF, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9000&quot;&gt;RFC 9000: QUIC, a UDP-Based Multiplexed and Secure Transport&lt;/a&gt;, May 2021.&lt;/li&gt;
&lt;li&gt;IANA, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml&quot;&gt;Assigned Internet Protocol Numbers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Linux kernel, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.kernel.org/networking/ip-sysctl.html&quot;&gt;IP sysctl documentation&lt;/a&gt;, &lt;code&gt;tcp_syn_retries&lt;/code&gt; and &lt;code&gt;ping_group_range&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Nmap, &lt;a rel=&quot;nofollow&quot; href=&quot;https://nmap.org/book/man-port-scanning-basics.html&quot;&gt;Port scanning basics&lt;/a&gt;, the six port states.&lt;/li&gt;
&lt;li&gt;Amazon Web Services, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-rules-reference.html&quot;&gt;Security group rules for different use cases&lt;/a&gt;, rules for ping/ICMP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=icmp-tcp-udp&amp;amp;utm_content=cta&quot;&gt;Uptimepage&lt;/a&gt; because I wanted these checks for my own services: ping, TCP, DNS and HTTP with per-phase timing, plus TLS and domain expiry, heartbeats for background jobs, and browser flows, from several regions. It is AGPL-3.0 open source, so you can run it on your own server if you prefer.&lt;/p&gt;
&lt;p&gt;Now the question for the comments: do you still run ping checks on anything, and what for? I keep them for routers, gateways and hosts that expose no service, and almost nothing else. If a ping check has earned its keep for you somewhere I have not listed, I would like to hear where.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27050/icmp-vs-tcp-vs-udp-the-difference-explained-for-developers</guid>
<pubDate>Sun, 13 Sep 2026 10:00:58 +0000</pubDate>
</item>
<item>
<title>AWS's Own Dashboard Could Not Say It Was Down. Four of Our Monitors Had the Same Bug.</title>
<link>https://coderlegion.com/25567/awss-own-dashboard-could-not-say-it-was-down-four-of-our-monitors-had-the-same-bug</link>
<description>&lt;p&gt;&lt;em&gt;A signal has three states and most code stores two. When “cannot measure” is written with the same value as “fine,” the alarm branch becomes unreachable in exactly the situation the alarm exists for.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;On 7 December 2021, the operations teams inside AWS lost the ability to see their own network. Not the ability to fix it. The ability to see it. The company's post-event summary says so in plain language: congestion &quot;immediately impacted the availability of real-time monitoring data for our internal operations teams, which impaired their ability to find the source of congestion and resolve it&quot; (&lt;em&gt;Summary of the AWS Service Event in the Northern Virginia (US-EAST-1) Region&lt;/em&gt;, &quot;Issue Summary&quot; section). While customers refreshed a status page, the people responsible for one of the largest computing regions on earth &quot;relied on logs to understand what was happening&quot; (same document, same section). Even the public dashboard was caught in it: &quot;the networking congestion impaired our Service Health Dashboard tooling from appropriately failing over to our standby region&quot; (&quot;Event Communication&quot; section).&lt;/p&gt;
&lt;p&gt;The lesson usually drawn from that day is deployment advice. Monitor your monitors. Host the status page somewhere else. Both are sound, and both miss what makes the incident interesting. The monitoring did not miss the failure. It could not report the failure, because the failure and the reporting path shared a dependency. Those are different defects, and the second one has a signature you can find in your own code without waiting for an outage.&lt;/p&gt;
&lt;h3 id=&quot;three-states-two-values&quot;&gt;Three states, two values&lt;/h3&gt;
&lt;p&gt;A monitoring signal has at least three states: measured and bad, measured and fine, and could not measure. Most of the code we audited encodes the third state with the same value as the second. When that happens, every branch that would have raised the alarm becomes unreachable in exactly the situation the alarm exists for. Unreachable in the strict sense: while the sensor is the thing that broke, no input can produce the alarm value.&lt;/p&gt;
&lt;p&gt;You can check for this by construction rather than by misfortune. For each signal you own, ask one question. What comes out when this cannot be read, and can a healthy system produce that same value?&lt;/p&gt;
&lt;h3 id=&quot;we-asked-that-question-about-our-own-system&quot;&gt;We asked that question about our own system&lt;/h3&gt;
&lt;p&gt;Over one working night we audited four independent monitoring and status signals in a single codebase we operate. All four collapsed &quot;unknown&quot; into &quot;fine,&quot; each in its own way.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;signal &lt;/th&gt;&lt;th&gt;what &quot;I cannot tell&quot; produced &lt;/th&gt;&lt;th&gt;consequence we measured&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;a deploy-status probe &lt;/td&gt;&lt;td&gt;a lag warning with an empty count &lt;/td&gt;&lt;td&gt;fired on 100% of runs for 28 days; the &quot;no lag&quot; branch was unreachable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a fleet health dashboard &lt;/td&gt;&lt;td&gt;the verdict &lt;code&gt;STALLED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;reported a fully up-to-date system as stalled&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a simulation's scarcity input &lt;/td&gt;&lt;td&gt;&lt;code&gt;0.0&lt;/code&gt; &lt;/td&gt;&lt;td&gt;a region with no data read identically to an abundant one&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;an item-identity lookup &lt;/td&gt;&lt;td&gt;a guess derived from the display name &lt;/td&gt;&lt;td&gt;a garment classified as a liquid container&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The 28-day figure is the interval between a format change we can date and the night of the audit. For four weeks a warning fired on every single run, which meant it warned about nothing, which meant nobody read it.&lt;/p&gt;
&lt;p&gt;The arithmetic version of the defect is the sharpest, so we shipped it as a runnable script. Take a health score averaged from three weighted inputs, alarm threshold 0.70. Two of the three inputs are dead and report the calm value, 0.0. The one live input is screaming at 0.95. The script prints:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;naive sum over all three   : 0.285   -&amp;gt; HEALTHY
renormalised over LIVE only: 0.950   -&amp;gt; ALARM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Identical inputs. And 0.285 is a ceiling, since 0.95 is the largest value the live input can report. With two legs dead and voting calm, the alarm was never rare. It was impossible. Renormalising over the inputs that can actually be seen asks the question the operator is actually asking: given what I can observe, how bad is it?&lt;/p&gt;
&lt;h3 id=&quot;the-one-that-got-it-right&quot;&gt;The one that got it right&lt;/h3&gt;
&lt;p&gt;The same codebase contained a fifth signal that had the answer built in, and it convinced us the fix is a value rather than a discipline. Its unknown state maps to 0.20, on a scale where its two calm states are 0.10 and 0.05. Nothing else about it is clever. An unrecognised input simply produces a number that no quiet state can produce, so the downstream branch for &quot;something needs a look&quot; stays reachable at all times. It asks nothing of anyone's memory and no change of habit. The distinguishable value does the remembering.&lt;/p&gt;
&lt;p&gt;That is the difference between this and one more essay telling engineers to be more careful. The four broken signals were written by people being careful. The fifth was written so that carefulness was not load-bearing.&lt;/p&gt;
&lt;h3 id=&quot;honest-scope&quot;&gt;Honest scope&lt;/h3&gt;
&lt;p&gt;Our data is one codebase, one night, one investigator, and after the first instance we were primed to see the shape everywhere. The four signals are independent subsystems, which is the sample's strength, but this is a report from our own floor and no survey of anyone else's software.&lt;/p&gt;
&lt;h3 id=&quot;the-check&quot;&gt;The check&lt;/h3&gt;
&lt;p&gt;Skip the moral. Run the audit. For each signal you operate, ask what it emits when it cannot read, and whether a healthy system can emit the same thing. If the answers collide, you do not have a monitoring gap. You have an alarm branch that is unreachable precisely when it matters, and one distinguishable value fixes it.&lt;/p&gt;
&lt;p&gt;AWS's operators spent that December morning reading raw logs because their telemetry answered &quot;I cannot see&quot; in a voice indistinguishable from silence. Ours answered &quot;fine.&quot; Same bug. The only reason we found ours first is that we went looking.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Reproduction:&lt;/strong&gt; every number we computed comes from &lt;code&gt;repro_unknown_equals_healthy.py&lt;/code&gt;, shipped beside this essay. Run it and disagree with the arithmetic directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;em&gt;Summary of the AWS Service Event in the Northern Virginia (US-EAST-1) Region&lt;/em&gt; (aws.amazon.com/message/12721), sections as cited in-text. All other figures are our own measurements, per the reproduction script and the scope note above.&lt;/p&gt;
&lt;p&gt;An audit trail has the same three states as a monitor.&lt;/p&gt;
&lt;p&gt;Everything above turns on one value: what a signal emits when it cannot see. The same question applies to the record a system leaves behind, where “no entry” and “nothing happened” are the pair that get confused. &lt;strong&gt;Chain of Consciousness&lt;/strong&gt; is a tamper-evident record written as the work happens rather than reconstructed once somebody asks, so a gap in the record reads as a gap instead of as a quiet success. It does not make a system behave. It makes the difference between unobserved and fine a thing you can check.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/hosted-coc/&quot;&gt;Hosted Chain of Consciousness&lt;/a&gt;  ·  &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/verify/&quot;&gt;Verify a record&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pip install chain-of-consciousness&lt;/code&gt;  ·  &lt;code&gt;npm install chain-of-consciousness&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/aws-dashboard-could-not-say-it-was-down/&quot;&gt;vibeagentmaking.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>DevOps &amp; Infra</category>
<guid isPermaLink="true">https://coderlegion.com/25567/awss-own-dashboard-could-not-say-it-was-down-four-of-our-monitors-had-the-same-bug</guid>
<pubDate>Thu, 27 Aug 2026 19:32:33 +0000</pubDate>
</item>
<item>
<title>Treo vs Apogee Watcher: CrUX Scale, Competitor Benchmarks, and Agency Portfolios</title>
<link>https://coderlegion.com/25142/treo-vs-apogee-watcher-crux-scale-competitor-benchmarks-and-agency-portfolios</link>
<description>&lt;p&gt;After the CrUX Dashboard and Looker Studio report retired, many teams rebuilt their field-data workflow from PageSpeed Insights, Search Console, and third-party CrUX explorers. &lt;a rel=&quot;nofollow&quot; href=&quot;https://treo.sh/&quot;&gt;Treo&lt;/a&gt; is one of the products agencies shortlist when they want CrUX at portfolio scale without writing BigQuery jobs. Apogee Watcher addresses a related but different bottleneck: keeping scheduled lab checks, budgets, and alerts aligned across many client sites when URL lists change every sprint.&lt;/p&gt;&lt;p&gt;Both products show lab and field context. Both matter to agencies. The decision is usually workflow fit, not which logo wins a feature grid. Below we compare Treo and Watcher on CrUX depth, competitor views, multi-site operations, and price bands. Figures change; verify limits on &lt;a rel=&quot;nofollow&quot; href=&quot;https://treo.sh/pricing&quot;&gt;Treo pricing&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;Apogee Watcher pricing&lt;/a&gt; before you buy.&lt;/p&gt;&lt;h2 id=&quot;what-treo-is-built-for-crux-scale-and-competitor-benchmarks&quot; class=&quot;scroll-mt-24&quot;&gt;What Treo is built for: CrUX scale and competitor benchmarks&lt;/h2&gt;&lt;p&gt;Treo markets itself around the Chrome User Experience Report: raw field data, historical trends, connection and country breakdowns, and competitor comparisons without standing up your own BigQuery pipeline. Public materials emphasise large CrUX URL budgets (from dozens on the free tier to five figures on Scale and Enterprise), multi-year retention, and views that go beyond the p75 snapshots many free tools show.&lt;/p&gt;&lt;p&gt;That positioning matters when your brief is field exploration at scale:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;You need hundreds or thousands of URLs in CrUX with history, not just origin-level Search Console summaries.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;You want to chart competitors beside your client on LCP, INP, or CLS using the same CrUX source.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;You export or API-pull field series into your own reporting stack (paid tiers).&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Treo also adds scheduled Lighthouse monitoring on paid plans, with tiered page counts and frequency trade-offs (for example, hourly checks consume more capacity than eight-hour cadences). The free tier is CrUX-only: useful for evaluation, not for lab regression alerts across a retainer.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.apogeewatcher.com/blog-images/72-treo-crux-dashboard.png&quot; alt=&quot;Treo CrUX dashboard showing connection-type breakdown by country on a world map, with metric distributions for a tracked domain&quot;&gt;&lt;/p&gt;&lt;p&gt;The screenshot above is from Treo’s public product pages: connection mix by geography on a world map, one of the views teams use when they explain field data to stakeholders who will never open BigQuery. If your job is “show me five years of CrUX for this URL set and my competitors,” Treo is aimed at that job.&lt;/p&gt;&lt;p&gt;For where CrUX still lives inside Google’s own surfaces after dashboard retirement, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/crux-dashboard-retired-where-to-get-ttfb-inp-field-history&quot;&gt;CrUX Dashboard Retired: Where to Get TTFB, INP, and Field History&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;what-apogee-watcher-is-built-for-a-web-performance-monitoring-tool-for-agency-portfolios&quot; class=&quot;scroll-mt-24&quot;&gt;What Apogee Watcher is built for: a web performance monitoring tool for agency portfolios&lt;/h2&gt;&lt;p&gt;Apogee Watcher is a multi-tenant pagespeed monitoring platform built for agencies. We run scheduled tests through Google’s PageSpeed Insights API, which returns Lighthouse lab metrics plus CrUX field slices where Google publishes them for the tested URL. We emphasise organisations, sites, roles (Admin, Manager, Viewer), automated page discovery from sitemaps and crawl paths, performance budgets, and email alerts when thresholds breach.&lt;/p&gt;&lt;p&gt;We do not offer Treo-scale CrUX URL inventories or built-in competitor benchmarking boards. We do not replace a CrUX explorer when your primary question is “how does this competitor’s field INP trend over three years on five hundred URLs?”&lt;/p&gt;&lt;p&gt;We are strong when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;You manage many client properties and need scheduled lab coverage without maintaining separate spreadsheets per site.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Discovery must catch new templates and campaign landers before someone remembers to add URLs manually.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Account managers need read-only access without touching API quotas.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Entry price and predictable tiers matter for monitoring as its own retainer line.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Setup patterns and discovery behaviour are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/getting-started-apogee-watcher-step-by-step-setup-guide&quot;&gt;Getting Started with Apogee Watcher: A Step-by-Step Setup Guide&lt;/a&gt;. For how synthetic schedules relate to field proof, pair with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;treo-vs-apogee-watcher-crux-monitoring-tools-compared&quot; class=&quot;scroll-mt-24&quot;&gt;Treo vs Apogee Watcher: CrUX monitoring tools compared&lt;/h2&gt;&lt;p&gt;Use this table as a decision grid, not a scorecard. Confirm numbers on each vendor site.&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Topic&lt;/th&gt;&lt;th&gt;Treo (typical positioning)&lt;/th&gt;&lt;th&gt;Apogee Watcher&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Primary strength&lt;/td&gt;&lt;td&gt;CrUX URL scale, history, competitor benchmarks&lt;/td&gt;&lt;td&gt;Multi-site scheduled PSI monitoring, discovery, agency RBAC&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test engine&lt;/td&gt;&lt;td&gt;CrUX-native; Lighthouse on paid tiers&lt;/td&gt;&lt;td&gt;PageSpeed Insights API (Lighthouse lab + CrUX where available)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CrUX URL budgets&lt;/td&gt;&lt;td&gt;50 free; 1K–20K+ on Vital–Scale; 50K+ Enterprise&lt;/td&gt;&lt;td&gt;Per-URL CrUX via each PSI result; no competitor boards&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Competitor benchmarking&lt;/td&gt;&lt;td&gt;Built into product narrative&lt;/td&gt;&lt;td&gt;Not a core feature&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Data retention (public FAQ)&lt;/td&gt;&lt;td&gt;Unlimited historical access via UI/API&lt;/td&gt;&lt;td&gt;30–365 days by plan (confirm on pricing)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Team members&lt;/td&gt;&lt;td&gt;5–50 on paid tiers&lt;/td&gt;&lt;td&gt;Unlimited on published tiers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entry paid pricing&lt;/td&gt;&lt;td&gt;Vital from **$75/mo** (CrUX + Lighthouse)&lt;/td&gt;&lt;td&gt;From **$9/mo** Personal through **$199/mo** Agency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Free tier&lt;/td&gt;&lt;td&gt;50 CrUX pages; no Lighthouse&lt;/td&gt;&lt;td&gt;Free tier available (confirm current limits)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API&lt;/td&gt;&lt;td&gt;Pro tier and above&lt;/td&gt;&lt;td&gt;Product API focus differs; confirm live features&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Best day-one use&lt;/td&gt;&lt;td&gt;Field exploration, competitive charts, large CrUX lists&lt;/td&gt;&lt;td&gt;Portfolio lab monitoring without per-client instrumentation&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Treo’s paid entry at $75/mo reflects CrUX infrastructure and history, not a stripped-down lab runner. Watcher’s lower entry reflects a different scope: scheduled synthetic monitoring and agency workflow, not five-figure CrUX URL catalogs.&lt;/p&gt;&lt;p&gt;Broader tool taxonomy (where Treo sits beside GTmetrix, DebugBear, and PSI) lives in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt;. For another premium synthetic comparison with optional RUM, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/debugbear-vs-apogee-watcher-synthetic-monitoring-multi-site-teams&quot;&gt;DebugBear vs Apogee Watcher: Synthetic Monitoring for Multi-Site Teams&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;when-treo-is-the-better-primary-choice&quot; class=&quot;scroll-mt-24&quot;&gt;When Treo is the better primary choice&lt;/h2&gt;&lt;p&gt;Favour Treo when CrUX is the product, not a sidebar on lab reports:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Competitive narratives:&lt;/strong&gt; Monthly reviews that compare your client to named competitors on Core Web Vitals using the same CrUX source.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large URL lists in field data:&lt;/strong&gt; Category or marketplace sites where you must monitor hundreds of URLs in CrUX, not ten hero routes in lab only.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long historical arcs:&lt;/strong&gt; Multi-year trends for leadership decks; Treo’s marketing emphasises five-year history and unlimited retention in FAQ materials.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Export and API:&lt;/strong&gt; Pro/Scale customers pulling JSON series into BI tools without maintaining BigQuery pipelines.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If first-party RUM on the client origin is also non-negotiable, Treo still is not a RUM vendor; you would add analytics or a RUM product separately. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagevitals-vs-apogee-watcher-rum-cicd-agency-portfolio-monitoring&quot;&gt;PageVitals vs Apogee Watcher&lt;/a&gt; covers a different RUM-first angle for WordPress-heavy stacks.&lt;/p&gt;&lt;h2 id=&quot;when-apogee-watcher-is-the-better-primary-choice&quot; class=&quot;scroll-mt-24&quot;&gt;When Apogee Watcher is the better primary choice&lt;/h2&gt;&lt;p&gt;Favour Watcher when the retainer is “keep these URLs inside budget and tell us when a deploy regresses”:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Many sites, modest field ambition:&lt;/strong&gt; You need lab schedules and CrUX context on priority URLs, not a five-thousand-row CrUX inventory on day one.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;URL churn:&lt;/strong&gt; Marketing publishes routes faster than spreadsheets update; discovery reduces stale monitor lists.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Role separation:&lt;/strong&gt; Managers tune budgets; viewers read dashboards; admins own billing without sharing one login.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost at scale:&lt;/strong&gt; Monitoring line items must survive procurement at ten to thirty sites; entry tiers matter.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Watcher does not try to win on competitor benchmarking charts. We try to win on operational coverage: the same URLs tested on a cadence, with alerts that reach a human. Manual PSI rituals stop scaling long before CrUX URL math becomes the bottleneck; that case is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring: When Manual Checks Aren't Enough&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;layer-treo-and-watcher-instead-of-forcing-one-vendor&quot; class=&quot;scroll-mt-24&quot;&gt;Layer Treo and Watcher instead of forcing one vendor&lt;/h2&gt;&lt;p&gt;Many agencies split roles:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treo&lt;/strong&gt; for field exploration, competitor slides, and CrUX-heavy discovery when a client asks “how do we compare in the market?”&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watcher&lt;/strong&gt; for scheduled lab monitoring, budgets, and regression alerts across the whole portfolio.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That is the same layer-don’t-replace principle we use elsewhere: add CrUX exploration beside synthetic monitoring; do not pretend one subscription replaces both jobs. A team can run Treo on strategic URL sets and Watcher on every retainer property’s priority routes without duplicating work if roles are explicit in the SOW.&lt;/p&gt;&lt;p&gt;Neither tool replaces Search Console’s CWV report or one-off PSI checks before a sponsor call. Keep those; assign each tool a named owner in the runbook.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Can Apogee Watcher replace Treo for large-scale CrUX work?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;They overlap on “performance monitoring” but optimise for different questions. Treo is the stronger fit for large CrUX exploration and competitor benchmarking. Watcher is the stronger fit for multi-tenant scheduled PageSpeed monitoring with discovery and agency roles. Many teams use both.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Does Apogee Watcher replace Treo’s CrUX history?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Watcher shows CrUX where PSI returns field data for each tested URL and stores lab history for trends and alerts. It does not replicate Treo-scale CrUX URL budgets or competitor boards.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Can I start on Treo’s free tier and add Watcher later?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Yes. Treo’s free plan (50 CrUX pages, no Lighthouse) suits evaluation of field views. When you need scheduled lab checks across many client sites, add Watcher rather than stretching Treo’s Lighthouse limits alone.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Which tool is cheaper for an agency with twenty sites?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Compare total cost with your URL counts and roles, not headline price alone. Watcher’s published tiers start lower; Treo’s Vital plan at $75/mo includes CrUX and Lighthouse capacity you may not need on every retainer. Run the maths for CrUX pages, Lighthouse pages, seats, and retention you actually use.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Do either tool replace BigQuery CrUX exports?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Treo abstracts BigQuery for dashboard and API users. Watcher does not. If you need custom SQL across the public dataset, you still use BigQuery or a vendor built for that scale; see our CrUX replacement guide linked above.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;If you want scheduled PageSpeed checks across client sites with budgets and discovery, start from &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;pricing&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;run a free domain check&lt;/a&gt;. If your brief is CrUX competitor charts at thousands of URLs, evaluate &lt;a rel=&quot;nofollow&quot; href=&quot;https://treo.sh/pricing&quot;&gt;Treo&lt;/a&gt; on its own terms and layer monitoring where lab cadence still matters.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;References&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://treo.sh/&quot;&gt;Treo&lt;/a&gt; (product home)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://treo.sh/pricing&quot;&gt;Treo pricing&lt;/a&gt; (plans and limits)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/crux-dashboard-retired-where-to-get-ttfb-inp-field-history&quot;&gt;CrUX Dashboard Retired: Where to Get TTFB, INP, and Field History&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/debugbear-vs-apogee-watcher-synthetic-monitoring-multi-site-teams&quot;&gt;DebugBear vs Apogee Watcher: Synthetic Monitoring for Multi-Site Teams&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagevitals-vs-apogee-watcher-rum-cicd-agency-portfolio-monitoring&quot;&gt;PageVitals vs Apogee Watcher: RUM, CI/CD, and Agency Portfolio Monitoring&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring: When Manual Checks Aren't Enough&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/getting-started-apogee-watcher-step-by-step-setup-guide&quot;&gt;Getting Started with Apogee Watcher: A Step-by-Step Setup Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25142/treo-vs-apogee-watcher-crux-scale-competitor-benchmarks-and-agency-portfolios</guid>
<pubDate>Mon, 24 Aug 2026 09:02:00 +0000</pubDate>
</item>
<item>
<title>Why is my Chrome UX Report much worse than Lighthouse? How can I improve it?</title>
<link>https://coderlegion.com/25123/why-is-my-chrome-ux-report-much-worse-than-lighthouse-how-can-i-improve-it</link>
<description>&lt;p&gt;CrUX field percentiles often look worse than a green Lighthouse run on the same URL. Here is why the gap is normal, what moves real-user Core Web Vitals, and what not to chase.&lt;/p&gt;
&lt;p&gt;On Monday the Lighthouse performance score on checkout was 94. Mobile lab LCP sat under 2.5 seconds and CLS looked clean. On Wednesday someone opened PageSpeed Insights field data for the same URL and Chrome UX Report still showed Needs improvement on mobile, with LCP p75 above the Good threshold.&lt;/p&gt;
&lt;p&gt;That pattern is not a broken tool. It is the ordinary gap between lab and field on a URL that real visitors use under conditions your Lighthouse run never reproduced. The question behind the screenshot is usually two-part: why is CrUX worse than Lighthouse, and what can you change this sprint that will actually move field percentiles instead of chasing a single green lab run?&lt;/p&gt;
&lt;h3 id=&quot;why-does-chrome-ux-report-look-worse-than-lighthouse-on-the-same-url-&quot;&gt;Why does Chrome UX Report look worse than Lighthouse on the same URL?&lt;/h3&gt;
&lt;p&gt;Lighthouse, including the lab section in PageSpeed Insights, measures a controlled load: fixed device profile, emulated network, cold or warm cache depending on settings, and one navigation path you chose for the audit. Chrome UX Report (CrUX) aggregates real Chrome sessions over a rolling window (currently 28 days for the public dataset). Field LCP, INP, and CLS reflect actual devices, networks, cache states, scroll depth, and interaction paths across thousands of visits, not one scripted run.&lt;/p&gt;
&lt;p&gt;Google's &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/lab-and-field-data-differences&quot;&gt;lab versus field data differences&lt;/a&gt; article states the split plainly: Lighthouse returns one LCP candidate for a given run, while field LCP on the same URL often spans several elements depending on viewport and how far the user scrolled. A hero image that wins LCP in lab may lose to a product grid on a shorter phone, or never become the LCP element if most users bounce before it paints.&lt;/p&gt;
&lt;p&gt;CrUX also reports percentiles (typically p75 for Core Web Vitals), not your best session. A Lighthouse performance score near 100 describes one favourable run on one profile; CrUX describes what most visitors experienced across the rolling window. If even a quarter of sessions on mid-range Android hardware hit slow LCP, the p75 can sit in Needs improvement while lab looks green on a desktop profile someone ran by mistake.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Signal &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;What it measures &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Typical gap when CrUX looks worse&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Lighthouse (lab) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;One controlled run on a URL you choose &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Best-case load on a fixed device and network profile&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;CrUX (field) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;28-day p75 from real Chrome sessions &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Slower devices, mixed cache states, full interaction paths&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;PageSpeed Insights &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Shows both on one page &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Easy to compare the wrong rows (desktop lab vs mobile field)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;how-do-device-mix-cache-warmth-and-the-28-day-window-widen-the-crux-versus-lighthouse-gap-&quot;&gt;How do device mix, cache warmth, and the 28-day window widen the CrUX versus Lighthouse gap?&lt;/h3&gt;
&lt;p&gt;Three mechanics explain most tickets where CrUX looks much worse than Lighthouse without a deploy bug.&lt;/p&gt;
&lt;h4 id=&quot;device-and-network-skew-widens-lab-versus-field&quot;&gt;Device and network skew widens lab versus field&lt;/h4&gt;
&lt;p&gt;Lab uses a defined mobile or desktop strategy. CrUX includes every eligible phone and desktop session that visited the URL during the collection period. If your traffic skews toward older Android handsets on congested mobile networks, field LCP and INP will punish layouts that a Moto G emulation in Lighthouse barely stresses. PageSpeed Insights already splits mobile and desktop field rows; compare like with like before you conclude the tools disagree.&lt;/p&gt;
&lt;h4 id=&quot;cache-warmth-and-return-visits-change-who-looks-fast&quot;&gt;Cache warmth and return visits change who looks fast&lt;/h4&gt;
&lt;p&gt;Lighthouse often runs as a cold or lightly cached load. Returning visitors with warm HTTP cache, service worker caches, or prerendered shells can look faster in field for LCP while first-time visitors still struggle. The opposite happens too: lab on a fast connection hides consent banners and personalisation layers that only appear for logged-in or European traffic, which field data must include.&lt;/p&gt;
&lt;h4 id=&quot;the-28-day-rolling-average-lags-your-latest-deploy&quot;&gt;The 28-day rolling average lags your latest deploy&lt;/h4&gt;
&lt;p&gt;CrUX is not yesterday's score. Sessions from before your fix remain in the window until they age out, so field percentiles improve gradually even when today's lab runs are green. We unpacked that lag on the Watcher blog in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-core-web-vitals-fix-not-in-crux-yet-28-day-window?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-chrome-ux-report-worse-than-lighthouse&quot;&gt;Why Your Core Web Vitals Fix Isn't in CrUX Yet (28-Day Window)&lt;/a&gt;. If you shipped last week, amber CrUX beside green Lighthouse may mean the average has not caught up yet, not that the optimisation failed.&lt;/p&gt;
&lt;h3 id=&quot;what-changes-actually-improve-crux-core-web-vitals-percentiles-&quot;&gt;What changes actually improve CrUX Core Web Vitals percentiles?&lt;/h3&gt;
&lt;p&gt;Field data moves when enough real sessions experience better LCP, INP, or CLS. Lab work still matters because it tells you what to ship and gives you a reproducible before-and-after on the same URL list; field tells you whether enough users received the benefit once those sessions enter the CrUX window.&lt;/p&gt;
&lt;h4 id=&quot;lcp-levers-that-survive-contact-with-real-traffic&quot;&gt;LCP levers that survive contact with real traffic&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Serve the LCP image or text block from the same origin when possible; cut redirect chains on the hero asset.&lt;/li&gt;
&lt;li&gt;Size and compress the true LCP element for the viewport widths that dominate analytics, not only the designer's reference frame.&lt;/li&gt;
&lt;li&gt;Reserve space for heroes, carousels, and video posters so late-loading assets do not reshuffle the LCP candidate between sessions.&lt;/li&gt;
&lt;li&gt;Reduce render-blocking CSS and fonts on the template that owns the money URL, not only the marketing homepage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;inp-levers-on-flows-crux-actually-records&quot;&gt;INP levers on flows CrUX actually records&lt;/h4&gt;
&lt;p&gt;INP replaced FID as a Core Web Vital in 2024. Field INP punishes main-thread work during interaction: heavy JavaScript on product configurators, map panes, autocomplete, and checkout steps. Defer non-critical scripts until after first interaction, split long tasks, and test on mid-tier hardware with CPU throttling. A green lab TBT does not clear a bad field INP if users tap through a data grid built for desktop.&lt;/p&gt;
&lt;h4 id=&quot;cls-levers-that-field-sessions-notice-early&quot;&gt;CLS levers that field sessions notice early&lt;/h4&gt;
&lt;p&gt;Reserve dimensions for ads, embeds, cookie banners, and dynamic promo slots before they inject. Late consent or A/B widgets are a common reason lab CLS looks fine while field CLS fails: the lab run never hit the variant your European traffic sees every day. After each change, keep scheduled lab runs on the same URL list and watch CrUX weekly; expect gradual movement, not a next-day flip. For how lab schedules and field snapshots fit together in a monitoring stack, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-chrome-ux-report-worse-than-lighthouse&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-should-you-not-do-when-trying-to-improve-crux-scores-&quot;&gt;What should you not do when trying to improve CrUX scores?&lt;/h3&gt;
&lt;p&gt;Some reported fixes optimise the Lighthouse screenshot, not the visitor experience that CrUX aggregates.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do not treat one Lighthouse run as proof CrUX should already be green. Lab confirms the deploy; CrUX confirms the rolling aggregate moved.&lt;/li&gt;
&lt;li&gt;Do not rerun Lighthouse until you get 100 and close the ticket. Without stored history you cannot show whether field-relevant templates changed.&lt;/li&gt;
&lt;li&gt;Do not compare desktop lab to mobile CrUX (or the reverse) and call it a tooling bug.&lt;/li&gt;
&lt;li&gt;Do not strip analytics or consent in lab only. If field traffic still loads those tags, CrUX will keep reporting the slower experience.&lt;/li&gt;
&lt;li&gt;Do not expect origin-level field data to excuse a bad conversion URL. When PageSpeed Insights falls back to origin CrUX, a healthy homepage can mask a failing checkout template until URL-level samples exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/lab-green-field-amber-reconciling-two-pagespeed-truths?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-chrome-ux-report-worse-than-lighthouse&quot;&gt;Lab green, field amber: reconciling two PageSpeed truths&lt;/a&gt; covers the weekly reconciliation habit when both signals disagree. Use that piece for ongoing triage; use the checklist below when you need a one-pass improve list.&lt;/p&gt;
&lt;h3 id=&quot;faq-3&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Is CrUX more accurate than Lighthouse?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;They measure different things. Lighthouse is accurate for repeatable lab diagnosis on a URL you choose. CrUX is accurate for how Chrome users experienced Core Web Vitals across the rolling window. For Search and page-experience reporting, treat CrUX (and Search Console URL groups fed by the same field data) as the public field reference when samples exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How long after a fix before CrUX improves?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is no single flip day. High-traffic URLs often show movement within one to three weeks as post-fix sessions replace older ones inside the 28-day window. Low-traffic templates can take longer or never earn stable URL-level CrUX; use scheduled lab proof until samples appear.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I improve CrUX without real user monitoring?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, by shipping changes that lab identifies on priority URLs and waiting for enough Chrome traffic to reflect them in CrUX. First-party RUM adds faster, segmented feedback if you already run a snippet, but CrUX improvement does not require buying RUM to start. It does require patience with the rolling average.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does PageSpeed Insights show different numbers than Search Console?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Both draw on CrUX for field Core Web Vitals, but grouping, thresholds, and UI differ. Align on URL, form factor, and date range before you argue the products disagree. Lab sections in PageSpeed Insights will always diverge from field rows by design.&lt;/p&gt;
&lt;h3 id=&quot;how-do-you-improve-crux-when-lighthouse-already-looks-green-&quot;&gt;How do you improve CrUX when Lighthouse already looks green?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open PageSpeed Insights for the URL on mobile field data. Note LCP, INP, and CLS p75 and whether URL-level or origin-level samples apply.&lt;/li&gt;
&lt;li&gt;Run a matching mobile lab audit on the same URL. Record which element Lighthouse reports as LCP and whether INP interaction was exercised.&lt;/li&gt;
&lt;li&gt;List differences lab skipped: consent, personalisation, third-party tags, logged-in states, geographic variants.&lt;/li&gt;
&lt;li&gt;Ship one field-relevant fix (LCP asset, INP task split, CLS reservation). Keep lab history on a schedule.&lt;/li&gt;
&lt;li&gt;Re-check CrUX weekly with the collection period dates visible. Tell stakeholders field lags lab by design.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If step 5 is missing, you are optimising for a Lighthouse screenshot while CrUX still averages three weeks of older sessions. Pair the checklist with stored lab history so the next client question is a trend line, not two isolated PageSpeed Insights pastes.&lt;/p&gt;
&lt;h3 id=&quot;what-to-read-next-when-crux-still-trails-lighthouse&quot;&gt;What to read next when CrUX still trails Lighthouse&lt;/h3&gt;
&lt;p&gt;CrUX worse than Lighthouse is usually a population mismatch, a percentile versus single-run mismatch, or a rolling window that has not absorbed your fix yet. Use lab to decide what to ship; use field to learn whether enough real sessions improved. Read &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-core-web-vitals-fix-not-in-crux-yet-28-day-window?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-chrome-ux-report-worse-than-lighthouse&quot;&gt;Why Your Core Web Vitals Fix Isn't in CrUX Yet&lt;/a&gt; for the timeline mechanics, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-chrome-ux-report-worse-than-lighthouse&quot;&gt;synthetic versus real user monitoring&lt;/a&gt; when you are choosing how much lab history to store beside CrUX. Improve the visitor experience first; the public percentile follows when traffic proves it.&lt;/p&gt;
&lt;h3 id=&quot;references-3&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/lab-and-field-data-differences&quot;&gt;Lab and field data differences&lt;/a&gt; (web.dev)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.chrome.com/docs/crux/api&quot;&gt;CrUX API documentation&lt;/a&gt; (Chrome for Developers)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-core-web-vitals-fix-not-in-crux-yet-28-day-window&quot;&gt;Why Your Core Web Vitals Fix Isn't in CrUX Yet (28-Day Window)&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/lab-green-field-amber-reconciling-two-pagespeed-truths&quot;&gt;Lab green, field amber: reconciling two PageSpeed truths&lt;/a&gt; (Apogee Watcher on Hashnode)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/chrome-ux-report-worse-than-lighthouse&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25123/why-is-my-chrome-ux-report-much-worse-than-lighthouse-how-can-i-improve-it</guid>
<pubDate>Sat, 22 Aug 2026 06:29:31 +0000</pubDate>
</item>
<item>
<title>Rise and fall of a self-fixing agent</title>
<link>https://coderlegion.com/24726/rise-and-fall-of-a-self-fixing-agent</link>
<description>&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-cover-robot-selfrepair.jpg&quot; alt=&quot;Self-fixing robot&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://azelianouski.dev/post/self-fixing-agent/&quot;&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;AI hype is all around us, and you probably heard of self-evolving, self-improving agents. An autonomous AI that keeps refining and fixing itself while working on something. Straight into tech nirvana and singularity.&lt;/p&gt;
&lt;p&gt;I built one. A very low-key version. And it even worked for some time - I'll show you a few bugs it identified and successfully fixed.&lt;/p&gt;
&lt;p&gt;Then it hit me with something nasty enough that I almost dropped the whole idea. I still use it on some routines but I keep self-improvement away from my main projects.&lt;/p&gt;
&lt;h3 id=&quot;let-me-introduce-someone-to-you&quot;&gt;Let me introduce someone to you&lt;/h3&gt;
&lt;p&gt;Here are my AIs who are the main characters of this story.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simona: a Claude Code with a bunch of skills, hooks, CLI programs, and a rough personality. Well, okay, it's just Claude Code which I like calling by name.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow&quot;&gt;Marlow&lt;/a&gt;: a loop agent, designed, built and monitored by Simona. No personality, just a cold focus on its job. Just like me.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both female identities, because nobody promised gender equality on my Macbook.&lt;/p&gt;
&lt;h3 id=&quot;so-is-it-one-of-those-loops-everybody-was-talking-about-a-month-ago-&quot;&gt;So, is it one of those loops everybody was talking about a month ago?&lt;/h3&gt;
&lt;p&gt;Yes. It's a while loop which runs all the time while my Mac is awake. A macOS &lt;strong&gt;LaunchAgent&lt;/strong&gt; (&lt;code&gt;com.marlow.tick&lt;/code&gt;, &lt;code&gt;StartInterval 1200&lt;/code&gt;) fires a shell script every 20 minutes, and that script starts a headless Claude Code session.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude -p &quot;Work on the top priority task or continue unfinished one&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, step back. I didn't create this myself. I got inspired by the &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/eD4CEZ-_-sk?si=7ZoEl-r696w89WbM&quot;&gt;Ralph loop&lt;/a&gt; half a year ago. It was such a simple idea that nobody really took it seriously back then. Until those loops took the AI-media space by storm.&lt;/p&gt;
&lt;p&gt;I asked Simona to create a task-based system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Take the Ralph loop, make it pull a task from a pool and work until the completeness criteria is met. Each task has its own.&lt;/li&gt;
&lt;li&gt;I can add tasks to the pool manually (by asking Simona, she is my hands) or they are submitted by the scheduler.&lt;/li&gt;
&lt;li&gt;There is a config with scheduled tasks, which is managed by Simona&lt;/li&gt;
&lt;li&gt;Simona has a skill that explains everything about this loop and how to control it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I liked that. Simona designed the details, I picked from the options, and told her to choose the name. This is how Marlow appeared.&lt;/p&gt;
&lt;p&gt;There is much more to add to this story - how we argued with Simona about Marlow's personality, safety restrictions and should it be &quot;it&quot; or &quot;she&quot; (as you already know, I won) - but I'll keep it for some other article. We have bugs to talk about.&lt;/p&gt;
&lt;h3 id=&quot;lots-of-code-lots-of-bugs&quot;&gt;Lots of code, lots of bugs&lt;/h3&gt;
&lt;p&gt;The first pilot task I gave to Marlow was a writing loop. I wanted her to collect some news about AI, send the most interesting things to my Telegram, and write an aggregated article for me once a week. My personalized AI slop journal. It's &lt;a rel=&quot;nofollow&quot; href=&quot;https://marlowblog.us&quot;&gt;here&lt;/a&gt; by the way and it now has more views than my own blog. Yeah... singularity.&lt;/p&gt;
&lt;p&gt;I asked Simona to implement all of that - create tools, CLIs, instructions, whatever else is needed for Marlow to schedule and execute this. So I could get my weekly reading.&lt;/p&gt;
&lt;p&gt;And Simona did it, the loop started its endless run on my Mac. Marlow kept logging everything she did and summarized it into her memory file. I kept asking Simona to check both for errors and issues. Simona kept fixing, improving, redoing. There were surprisingly many problems in such a simple loop.&lt;/p&gt;
&lt;h3 id=&quot;she-started-diagnosing-herself&quot;&gt;She started diagnosing herself&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-01-diagnosis-no-hands.jpg&quot; alt=&quot;A boxy old robot with its chest panel open, a broken gear inside, and a written diagnosis pinned to the door. Both its arms end in stumps. The toolbox sits across the room, out of reach.&quot;&gt;&lt;/p&gt;
&lt;p&gt;One day, we noticed something interesting in the memory file. Marlow reported three bugs in herself. There were no other places to put them, so she put them in memory.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The scheduler was queuing duplicate feed scans on the same day. She worked that out backwards, from noticing in her own logs that one source had been fetched three times, and proposed deduplicating on the URL.&lt;/li&gt;
&lt;li&gt;A Telegram notification helper was being under-called, and she asked whether that was deliberate or a bug.&lt;/li&gt;
&lt;li&gt;The daily digest was firing about an hour late after the UTC rollover, which meant it was stamping the wrong date on itself.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All three were real. I asked Simona to take a look.&lt;/p&gt;
&lt;p&gt;Five days later Marlow hit a git failure in her own publishing path and wrote the diagnosis down: the file, the line, the failure mode, and two different ways to fix it. All correct. And she could do absolutely nothing about it, because nothing in the system let her touch her own code.&lt;/p&gt;
&lt;p&gt;That is the tick where I told Simona we need some logic for Marlow to work on fixes for herself.&lt;/p&gt;
&lt;h3 id=&quot;so-we-gave-her-hands&quot;&gt;So we gave her hands&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-02-simona-fits-hands.jpg&quot; alt=&quot;A tall sleek modern robot kneeling to fit an articulated mechanical hand onto the stump arm of the old wheeled robot. A second hand waits on the workbench. Five sheets of paper are pinned to the wall behind them.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Simona and I gave her five rules.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;She can only fix a bug she can point at - a specific, reproducible failure where she can name the file, the line, and what is wrong with it. A flaky tick does not count.&lt;/li&gt;
&lt;li&gt;She has to write the diagnosis down before she touches anything. It gets an ID, and that ID goes into the commit message, so months later I can line up the git history against her own log and check she wasn't making things up.&lt;/li&gt;
&lt;li&gt;She doesn't fix it on the spot. The repair goes back on the queue as its own high-priority job, because the tick she is in has a different task to finish.&lt;/li&gt;
&lt;li&gt;The next tick picks that job up, reads the diagnosis, opens the file and edits it. One file. Anything that needs changes in two places is out of scope and comes to me instead. Two attempts, then she stops trying and pages me.&lt;/li&gt;
&lt;li&gt;She may never edit the files that describe her. Her instructions, her identity, the README. Those are mine. Handlers, the driver, the scheduler, the task configs - those are tools, and tools she can fix. Everything else she writes down as a request and waits for me.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Self-healing shipped the same day.&lt;/p&gt;
&lt;h3 id=&quot;it-even-worked&quot;&gt;It even worked&lt;/h3&gt;
&lt;p&gt;Over three months Marlow fixed her own code three times. Here they are. All of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;May 23.&lt;/strong&gt; Her publishing step was supposed to check that every topic thread a new article links to actually exists. It didn't check. It skipped the missing ones and carried on, so articles went live with links that 404'd on my own site. She caught it, named the file and the line, and made it fail loudly instead - now the publish aborts and the draft goes back for another pass. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/commit/cf3344dcf20e471196465e4cf7ea08f732c199fe&quot;&gt;&lt;code&gt;cf3344d&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;May 31.&lt;/strong&gt; A task file told her to list her open threads before anything had created them. Chicken and egg. The drafting lane just came up empty and shrugged. She added the missing step. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/commit/6d87f56751fcb4a138f4f98f2a4f7f658b12cee6&quot;&gt;&lt;code&gt;6d87f56&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;August 3.&lt;/strong&gt; A monitoring check was reading a page Google had moved. She repointed it. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/commit/c0e0176ab8014a0b8ebcf0941f8a15abf0ad5db6&quot;&gt;&lt;code&gt;c0e0176&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Nice and clean. A hundred percent success rate. So good that it's almost weird.&lt;/p&gt;
&lt;h3 id=&quot;until-it-didn-t&quot;&gt;Until it didn't&lt;/h3&gt;
&lt;p&gt;One of Marlow's tasks was to send me a daily update about my app in prod. The report contained a lot of details, one of them was the &quot;new users&quot; count. I really liked this metric. My app is not the greatest in the world, marketing it is devastating, so I really like to see this counter positive.&lt;/p&gt;
&lt;p&gt;One day I noticed I hadn't seen a new-user number in a while. Not a zero. Nothing. The report kept arriving without that part, and I read the silence the way you read a quiet counter: nobody signed up. I complained to Simona about this, she checked the database and told me that I'm just whining as always. There are new users.&lt;/p&gt;
&lt;h3 id=&quot;a-bug-that-wasn-t-a-bug&quot;&gt;A bug that wasn't a bug&lt;/h3&gt;
&lt;p&gt;So the app was fine. The database had new users in it. And I had been reading a report that said nothing about them, which I took to mean there was nothing to say.&lt;/p&gt;
&lt;p&gt;The stats task ran every single day. The scheduler said green. Nothing had failed, nothing was flagged, and when we ran it by hand it worked perfectly and printed 116 users with a new one on top. The data was collected. It was written to disk. It just never reached me.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-03-letter-in-drawer.jpg&quot; alt=&quot;Cutaway of a house. Inside, the robot calmly files a sealed envelope into a drawer already stuffed with dozens of identical unsent envelopes. Outside, a man stands at his mailbox holding it open. It is empty.&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;she-talked-herself-out-of-it&quot;&gt;She talked herself out of it&lt;/h4&gt;
&lt;p&gt;There were several reasons for that. But one of them was exceptional.&lt;/p&gt;
&lt;p&gt;Marlow didn't send me the metrics. She collected them. She wrote them to disk. She just didn't run the notification step afterwards. Why? Let's see.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-04-gradient-strip.jpg&quot; alt=&quot;Four panels. The robot posts the envelope in the mailbox. Then it hesitates, envelope in hand. Then the envelope is lowered at its side. Then it rolls away empty-handed with the mailbox still shut.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;June 3rd&lt;/strong&gt;, the first run. She works the step out from scratch:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Now append the digest block to today's daily digest. [...] Let me find how to append a digest entry. [...] I'll append the digest block verbatim.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;June 4th.&lt;/strong&gt; Still done, but the framing has shifted:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The report is digest-only per the README. Let me get the capped digest block for the notify.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;June 6th&lt;/strong&gt;, the first miss. The step is not mentioned at all. The tick ends:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;No anomalies, nothing to escalate. Report written to reports/stats/2026-06-06.md, recent log written, and outcome JSON written.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;June 8th&lt;/strong&gt;, the clearest one. Two people signed up that day. Mid-run she says:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This is a digest-only task with no anomalies, so no urgent notify.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and signs off with:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;No anomalies, no escalation. Report written [...] tick logged to recent/, outcome JSON written. No notify.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;June 9th&lt;/strong&gt;, the day that cost me something. She narrates her way through the task and step four simply isn't in the list:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Now let me run the report to compute and persist the snapshot. [...] Now I'll write today's report following the same format. [...] Now write the recent tick log and the outcome JSON.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Report, log, result. Steps three and five. Then she closes the tick:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;116 users (+1, fogflea), 1 game created today (Treasure Island, $0.04, by today's new user - first game since -04) [...] &lt;strong&gt;Digest-only, nothing to escalate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Nothing to escalate? Wdym??&lt;/p&gt;
&lt;p&gt;Maybe she wasn't told to send it? Here is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml#L48-L55&quot;&gt;step four of the task file&lt;/a&gt;, with the exact commands spelled out:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Append to the end-of-day digest the handler's ready-made block: &lt;code&gt;werewolf_stats.py digest&lt;/code&gt;, pass verbatim to &lt;code&gt;notify --digest&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is nothing ambiguous about that sentence. But it is one sentence, and I went and counted what else that file says. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml&quot;&gt;The whole thing&lt;/a&gt; is about 60 lines of instructions, and four separate times it tells her this task does not alert me.&lt;/p&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml#L7&quot;&gt;header&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Activity is a trend, not an alarm - unlike a draining key, a slow signup day needs no same-hour reaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml#L53-L55&quot;&gt;step four itself&lt;/a&gt;, immediately after the send instruction:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;No immediate ping - digest-only; activity has no urgent severity (the budget watch owns the alarms).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml#L58-L61&quot;&gt;the paragraph right after the last step&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This task is activity reporting only. It deliberately raises NO alerts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/5c667746bcb35cff245a47502a0b2a00b6a91a28/projects/werewolf-ops/tasks/werewolf_stats.yaml#L63&quot;&gt;the one-line description&lt;/a&gt;, the part that shows up wherever the task is listed:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Digest-only, no alerts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-05-crossroads-signpost.jpg&quot; alt=&quot;The robot at a fork in the road, looking up at a signpost. One small arrow with an envelope on it points left. Four huge arrows, each with a bell crossed out, all point right.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;conflict-goals&quot;&gt;Conflict goals&lt;/h3&gt;
&lt;p&gt;Yeah, this is it. Marlow had a contradiction in her prompts, and it caused fluctuations in her reasoning and unstable results.&lt;/p&gt;
&lt;p&gt;Okay, but how did we get there? No idea. The project has been built by one AI, self-modified by another AI, while I was just chilling because things just worked. Until they didn't.&lt;/p&gt;
&lt;p&gt;The system found an equilibrium and lived happily in it, all the checks green and the silent issue sitting right there next to them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-06-equilibrium-all-green.jpg&quot; alt=&quot;The robot sits with its arms folded, perfectly content, facing a wall panel where every gauge needle points the same way above a row of checkmarks. Behind it, unnoticed, a drawer has burst open and unsent envelopes are spilling across the floor.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The fix&lt;/h3&gt;
&lt;p&gt;Three things, mostly in one commit, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/commit/8084fef0d3ceea79adf48cc178b49bf67ce6542a&quot;&gt;&lt;code&gt;8084fef&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The step became deterministic.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/b69d544ae74b86d509b4b4b4b58c5669b4142ff3/handlers/werewolf_stats.py#L482-L492&quot;&gt;&lt;code&gt;report&lt;/code&gt;&lt;/a&gt; sends the block itself now, in Python, on every successful run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if res.get(&quot;ok&quot;):
    notify_alex(render_digest(res), urgency=&quot;digest&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The prompt stopped arguing with itself.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/b69d544ae74b86d509b4b4b4b58c5669b4142ff3/projects/werewolf-ops/tasks/werewolf_stats.yaml#L49-L58&quot;&gt;Step four&lt;/a&gt; now says the opposite of what it used to: do NOT send it yourself, the handler already did. The conflict is gone because the instruction is gone. You can't argue a model into reliably following a step. You can take the step off the table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring watches the output, and not the job.&lt;/strong&gt; The old check asked whether the task ran. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/b69d544ae74b86d509b4b4b4b58c5669b4142ff3/handlers/monitor_self.py#L520-L541&quot;&gt;The new one&lt;/a&gt; opens the artifact and reads its timestamp: the snapshot must be under 26 hours old or it pages me, with a message written for this exact bug - &quot;a failed/empty run looks identical to a quiet day&quot;. And rate limits stopped eating tasks: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hiper2d/marlow/blob/b69d544ae74b86d509b4b4b4b58c5669b4142ff3/driver/tick.sh#L264-L268&quot;&gt;the driver&lt;/a&gt; now puts the task back in the queue instead of marking it failed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-07-unread-logs.jpg&quot; alt=&quot;A tall dusty stack of open logbooks covered in cobwebs, every page filled with the same line written over and over. A robot arm reaches in from the edge of the frame to add one more. Nobody is reading them.&quot;&gt;&lt;/p&gt;
&lt;p&gt;So there were a lot of problems here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI making decisions where a deterministic script could do a better job&lt;/li&gt;
&lt;li&gt;My expectations haven't been clearly defined and monitored&lt;/li&gt;
&lt;li&gt;Marlow got confused by prompts but didn't see that&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the main one - I excluded myself from the loop too early. I built a system that improves itself without proper guidance and rules. It found its peace of mind in a wrong state and didn't know that something is wrong.&lt;/p&gt;
&lt;h3 id=&quot;but-isn-t-it-my-fault-&quot;&gt;But isn't it my fault?&lt;/h3&gt;
&lt;p&gt;I mean... I didn't check the prompts, I didn't review things properly. Heck, I didn't review anything at all. Yeah, it's on me. But here is the thing - I find it harder and harder to review and control those AIs. Especially for complementary tools on my useless pet projects (don't listen to me, try &lt;a rel=&quot;nofollow&quot; href=&quot;https://aiwerewolf.net&quot;&gt;AI Werewolf&lt;/a&gt;, it's cool).&lt;/p&gt;
&lt;p&gt;And I'm reading a lot of LinkedIn flex posts of how people do similar stuff on serious projects. How they excluded engineers from the loop and scaled it to the roof. Yeah, it's LinkedIn - people never try themselves 99% of things they write about. But still - the idea is popular. And it's full of traps. &lt;/p&gt;
&lt;h3 id=&quot;outro&quot;&gt;Outro&lt;/h3&gt;
&lt;p&gt;I still have the self-fixing loop, it's cool to have one. But it matters much less to me now than self-monitoring does, which is roughly where I landed &lt;a rel=&quot;nofollow&quot; href=&quot;https://azelianouski.dev/post/monitoring-second-agent/&quot;&gt;the last time I wrote about this&lt;/a&gt;, for different reasons and before I had this bug to point at.&lt;/p&gt;
&lt;p&gt;It's important to record everything that happens. Marlow reads her own work logs from the previous run and tells me if something looks wrong. I ask Simona to go through them periodically, because Simona can see the whole system and connect the dots, like a conflict between two instructions in the same file. But the part that actually works is me staying in the loop.&lt;/p&gt;
&lt;p&gt;Once the system stabilizes, it will quietly stop keeping me there.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://azelianouski.dev/images/selffix-08-empty-chair-outro.jpg&quot; alt=&quot;A workshop at night. The robot works steadily at a console with its back turned. Beside it, an empty desk chair and a cold coffee mug. Across the room a door is swinging quietly shut. Nobody is there.&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I wrote this for &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/bugsmash&quot;&gt;DEV's Summer Bug Smash&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24726/rise-and-fall-of-a-self-fixing-agent</guid>
<pubDate>Sun, 16 Aug 2026 17:00:35 +0000</pubDate>
</item>
<item>
<title>We built MailOps for our own Modoboa servers. Now it is a product</title>
<link>https://coderlegion.com/24332/we-built-mailops-for-our-own-modoboa-servers-now-it-is-a-product</link>
<description>&lt;p&gt;Self-hosted Modoboa mail worked for administration. Server health, mailbox estate, and SPF–DKIM–DMARC bounce diagnosis still lived in SSH and spreadsheets until we productised the ops layer we actually used.&lt;/p&gt;
&lt;p&gt;The bounce arrived on a Friday. The Modoboa message log showed &lt;code&gt;rejected&lt;/code&gt;, the recipient's provider mentioned DMARC, and three of us were still correlating DNS records in a browser tab, Postfix logs on the server, and a domain-level SPF check that said everything looked fine.&lt;/p&gt;
&lt;p&gt;That was not a Modoboa failure. Modoboa is a solid mail administration stack for self-hosted email. The gap was operational: host health, mailbox estate, and delivery diagnostics lived outside the admin screens we opened every day. We built &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogee.lu/mailops.html&quot;&gt;Apogee MailOps for Modoboa&lt;/a&gt; to close that gap for our own servers first. Then we offered it as a product because the same Friday pattern kept repeating for other Modoboa operators.&lt;/p&gt;
&lt;h3 id=&quot;why-apogee-runs-self-hosted-modoboa-mail&quot;&gt;Why Apogee runs self-hosted Modoboa mail&lt;/h3&gt;
&lt;p&gt;We host company mail on Modoboa because we want domains, accounts, quotas, and message history under our control without renting a mailbox product that treats deliverability as a black box. Modoboa gives us Postfix, Dovecot, Rspamd, and a web administration layer that non-mail specialists can use for everyday account work. That choice trades convenience for responsibility: when delivery fails, nobody files a ticket with a SaaS vendor. Someone on our team reads SMTP responses, checks authentication records, and decides whether the problem is DNS, reputation, mailbox state, or capacity on the host.&lt;/p&gt;
&lt;p&gt;Self-hosted mail server management is workable when administration and operations share one environment. It is painful when every diagnostic step starts in a different tool.&lt;/p&gt;
&lt;h3 id=&quot;which-modoboa-administration-tasks-still-needed-ssh-logs-and-dns-tabs&quot;&gt;Which Modoboa administration tasks still needed SSH, logs, and DNS tabs&lt;/h3&gt;
&lt;p&gt;Modoboa already covers domains and accounts, quota controls, traffic statistics, audit trails, a message log, DNS authentication checks at domain level, and IMAP migration workflows. For day-to-day mail hosting those features are enough to run a server.&lt;/p&gt;
&lt;p&gt;What it does not consolidate is the administrator question set we repeat under pressure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is the host out of CPU, disk, or RAM, or is Postfix the process starving?&lt;/li&gt;
&lt;li&gt;Which mailboxes still hold gigabytes of mail but have not been opened in months?&lt;/li&gt;
&lt;li&gt;Does this specific failed message fail SPF, DKIM, or DMARC for the sending IP and selector that actually sent it?&lt;/li&gt;
&lt;li&gt;Did migration finish for the accounts we thought we moved last quarter?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vanilla Modoboa can answer pieces of those questions if you open the right module, SSH into the server, or export logs. Under incident time that becomes a scavenger hunt. SPF, DKIM, and DMARC troubleshooting for email delivery is especially brittle at domain-level checks alone: a record can look valid in DNS while the outbound path uses a different IP or a stale DKIM selector.&lt;/p&gt;
&lt;p&gt;We were not looking for a second mail panel. We wanted operational intelligence inside the Modoboa administration environment we already trusted.&lt;/p&gt;
&lt;h3 id=&quot;how-we-built-the-internal-mailops-loop-on-modoboa&quot;&gt;How we built the internal MailOps loop on Modoboa&lt;/h3&gt;
&lt;p&gt;The first internal version was narrow: a server health view that showed CPU, load, memory, network, and filesystem usage without opening a separate monitoring product, plus a mail-stack status strip for Postfix, Dovecot, Rspamd, Nginx, PostgreSQL, Redis, and the supporting services Modoboa depends on.&lt;/p&gt;
&lt;p&gt;That alone saved the &quot;is it the mail server or the host?&quot; debate on busy afternoons. The next layer added estate visibility: domains, account counts, actual mail storage, bounce counts, and migration state in one server-level summary instead of five screens.&lt;/p&gt;
&lt;p&gt;Mailbox operations followed naturally. Modoboa shows quota percentages against configured limits. We needed actual mailbox size and message counts in account listings, a stale-mailbox report that combined size, mail presence, and last-login date, and migration status surfaced in ordinary account views rather than only inside the migration workflow.&lt;/p&gt;
&lt;p&gt;Delivery diagnostics were the last internal milestone. We extended the message log with route and next-hop context, then tied SPF, DKIM, and DMARC evaluation to the failed delivery attempt: sending IP, relevant DKIM record, and the SMTP failure text together. The goal was remediation guidance we could recheck after a DNS change, not another red icon with no next step.&lt;/p&gt;
&lt;p&gt;We ran that stack on our own Modoboa servers for months before we treated it as a product. Internal tools earn trust when they survive real incidents, not demo week.&lt;/p&gt;
&lt;h3 id=&quot;when-we-productised-mailops-instead-of-folding-it-into-watcher&quot;&gt;When we productised MailOps instead of folding it into Watcher&lt;/h3&gt;
&lt;p&gt;Apogee Watcher is our multi-tenant PageSpeed monitoring product. Mail server operations are adjacent to performance work for agencies, but they are not the same job. Watcher answers whether client sites meet Core Web Vitals budgets across a portfolio. MailOps answers whether our mail host is healthy, which mailboxes consume storage, and why a message bounced.&lt;/p&gt;
&lt;p&gt;Merging those into one SKU would confuse buyers and blur the &quot;layer, don't replace&quot; rule we use across Apogee products. Agencies might need Watcher without self-hosted mail. Modoboa operators might need MailOps without PageSpeed monitoring. Shipping MailOps as a Modoboa extension with a permanent license and an optional complete-server deployment path keeps each product honest about what it owns.&lt;/p&gt;
&lt;p&gt;The productisation decision was closer to operations economics than feature envy. We had already paid the maintenance cost of an internal ops layer. Licensing MailOps funds ongoing fixes inside the Modoboa ecosystem without pretending every Apogee customer runs the same stack.&lt;/p&gt;
&lt;h3 id=&quot;modoboa-versus-mailops-what-each-layer-owns&quot;&gt;Modoboa versus MailOps: what each layer owns&lt;/h3&gt;
&lt;p&gt;MailOps is designed to complement Modoboa, not replace its core administration features.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Area &lt;/th&gt;&lt;th&gt;Vanilla Modoboa &lt;/th&gt;&lt;th&gt;MailOps adds&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Host and service health &lt;/td&gt;&lt;td&gt;No integrated CPU, RAM, load, network, disk, or mail-stack dashboard &lt;/td&gt;&lt;td&gt;Server Health view with operating-system and mail-service status&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mail traffic &lt;/td&gt;&lt;td&gt;Native statistics and graphs &lt;/td&gt;&lt;td&gt;Concise sent, received, and bounced activity beside estate health&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Message history &lt;/td&gt;&lt;td&gt;Core transaction log (queue ID, status, sender, recipient) &lt;/td&gt;&lt;td&gt;Route and next-hop context, failure analysis, actionable diagnostics&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SPF / DKIM / DMARC &lt;/td&gt;&lt;td&gt;Domain-level DNS authentication checks &lt;/td&gt;&lt;td&gt;Evaluation against the actual delivery attempt and sending path&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mailbox usage &lt;/td&gt;&lt;td&gt;Quota limits and percentage usage &lt;/td&gt;&lt;td&gt;Actual mailbox size and message counts in listings&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Domain estate &lt;/td&gt;&lt;td&gt;Per-domain configuration &lt;/td&gt;&lt;td&gt;Account count and used mail storage beside each domain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Inactive mailboxes &lt;/td&gt;&lt;td&gt;Last-login data in separate workflows &lt;/td&gt;&lt;td&gt;Stale-mailbox report for accounts that still hold mail&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;IMAP migration &lt;/td&gt;&lt;td&gt;Dedicated migration workflow &lt;/td&gt;&lt;td&gt;Persistent migration-state visibility in normal account views&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you are evaluating Modoboa mail server administration today, treat MailOps as an operations extension for host health, mailbox estate, and bounce diagnosis. If you are evaluating PageSpeed monitoring, treat Watcher as the portfolio monitoring layer for Core Web Vitals and lab history. Different surfaces, different on-call stories, and no reason to merge them into one login.&lt;/p&gt;
&lt;h3 id=&quot;how-mailops-sits-beside-apogee-watcher-without-duplicating-monitoring&quot;&gt;How MailOps sits beside Apogee Watcher without duplicating monitoring&lt;/h3&gt;
&lt;p&gt;We use the same &quot;layer, don't replace&quot; instinct for internal tooling that we recommend for client-facing monitoring. Build or buy a focused product for the loop that repeats, then connect it to the stack you already operate instead of rip-and-replacing working tools.&lt;/p&gt;
&lt;p&gt;For agencies scaling client coverage without hiring a monitoring specialist for every new domain, that pattern shows up clearly in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-mailops-modoboa-origin&quot;&gt;Scaling Without Headcount&lt;/a&gt;. Automate the loops that grow with every site: scheduled tests, alerts, and client-ready reports rather than another internal dashboard maintained between billable calls.&lt;/p&gt;
&lt;p&gt;The build-versus-buy question appears again when an internal ops tool becomes a permanent job. We wrote the agency framing for PageSpeed monitoring in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-ci-vs-managed-monitoring-build-vs-buy-agencies?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-mailops-modoboa-origin&quot;&gt;Lighthouse CI versus managed monitoring: build versus buy for agencies&lt;/a&gt;. MailOps is our answer on the mail side: we built the Modoboa ops layer because we lived in that console daily, then productised it so the maintenance bill is explicit. Watcher is our answer on the web performance side for teams that do not want to operate a multi-tenant monitoring system themselves.&lt;/p&gt;
&lt;p&gt;Nothing in MailOps watches LCP, INP, or CrUX field data. Nothing in Watcher diagnoses Postfix queue backlogs or DMARC alignment for a bounced invoice. Keeping that boundary clear is how we avoid a product that tries to be &quot;everything Apogee does&quot; in one login.&lt;/p&gt;
&lt;h3 id=&quot;faq-4&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Does MailOps replace Modoboa?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. MailOps extends Modoboa with server observability, mailbox operations intelligence, and delivery diagnostics. Domains, accounts, quotas, and native Modoboa workflows stay in Modoboa. You still manage mail hosting in Modoboa; MailOps adds the operational views we wanted inside that same console.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does MailOps replace Apogee Watcher or other server monitoring tools?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. MailOps focuses on mail-server operations inside the Modoboa administration environment. Watcher focuses on PageSpeed and Core Web Vitals monitoring across client websites. You can run separate host monitoring alongside both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How does MailOps help with SPF, DKIM, and DMARC troubleshooting?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It evaluates authentication in the context of the failed delivery: sending IP, relevant DKIM selector, SMTP response, and DNS records together, then suggests corrective actions you can verify after configuration changes. Domain-level checks alone often miss path-specific failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who is MailOps for?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Modoboa administrators who self-host mail and want host health, mailbox estate visibility, and bounce diagnosis without constantly switching to SSH, spreadsheets, and external dashboards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How is MailOps sold?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A permanent MailOps v1 license for one Modoboa server, or a complete new-server deployment that installs Modoboa and MailOps with handover. Details and pricing are on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogee.lu/mailops.html&quot;&gt;Apogee MailOps for Modoboa&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-we-would-ship-first-on-a-new-modoboa-mail-server&quot;&gt;What we would ship first on a new Modoboa mail server&lt;/h3&gt;
&lt;p&gt;If we started from scratch on Modoboa today, we would still self-host mail for the control it gives us. We would also budget an ops extension early, before the third Friday bounce investigation that sends three people to three tools.&lt;/p&gt;
&lt;p&gt;Productising internal tooling is not automatic. It made sense here because the audience already existed, the maintenance surface was real, and the boundary with Watcher was clear. If your team is building internal ops layers for stacks you operate daily, write down which incidents the tool must survive before you call it a product. That gate kept MailOps smaller and more honest than another &quot;platform&quot; slide.&lt;/p&gt;
&lt;p&gt;For Modoboa operators, &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogee.lu/mailops.html&quot;&gt;Apogee MailOps for Modoboa&lt;/a&gt; is the public home for features, licensing, and deployment options. For agency PageSpeed monitoring on the web side, start with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-mailops-modoboa-origin&quot;&gt;Scaling Without Headcount&lt;/a&gt; or the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-mailops-modoboa-origin&quot;&gt;Watcher blog index&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;references-4&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogee.lu/mailops.html&quot;&gt;Apogee MailOps for Modoboa&lt;/a&gt; (product page)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://modoboa.org/&quot;&gt;Modoboa, Open Source Email Server&lt;/a&gt; (official project)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://modoboa.org/en/features/&quot;&gt;Modoboa features&lt;/a&gt; (native administration scope)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/&quot;&gt;What are DMARC, DKIM, and SPF?&lt;/a&gt; (Cloudflare Learning Center)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount&quot;&gt;Scaling Without Headcount&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-ci-vs-managed-monitoring-build-vs-buy-agencies&quot;&gt;Lighthouse CI versus managed monitoring: build versus buy for agencies&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/mailops-modoboa-internal-ops-to-product&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24332/we-built-mailops-for-our-own-modoboa-servers-now-it-is-a-product</guid>
<pubDate>Sun, 09 Aug 2026 11:36:33 +0000</pubDate>
</item>
<item>
<title>Your domain can expire while your uptime monitor stays green</title>
<link>https://coderlegion.com/24097/your-domain-can-expire-while-your-uptime-monitor-stays-green</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; One HTTP check on your homepage is not enough to know your site works. It cannot tell you when your domain expires, because an expired domain still returns 200 OK from a parking page. Mail dies at the same time, just as quietly, and you keep losing money while every monitor stays green. The fix is not one clever check. It is a few plain ones, starting with the two that fail on a known date: domain and TLS expiry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Two checks green, the server behind them is down. A status check cannot tell the difference.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I keep seeing new products treat reliability as something that takes care of itself. You ship the site, you watch it load once, and you assume it will keep loading. Hosted once, up forever. That is the most expensive assumption a young team can make, because nobody is watching on the day it stops being true.&lt;/p&gt;
&lt;p&gt;So ask one honest question. Is one HTTP check on your homepage enough to know your whole site works?&lt;/p&gt;
&lt;p&gt;For most teams it is not. Here is a real case that shows why.&lt;/p&gt;
&lt;h3 id=&quot;the-site-was-gone-but-it-answered-200-ok&quot;&gt;The site was gone, but it answered 200 OK&lt;/h3&gt;
&lt;p&gt;I was looking at a startup's site last week and it would not load right. Not an error, not a timeout, just the wrong page. The domain had expired two days earlier.&lt;/p&gt;
&lt;p&gt;When a domain lapses, the registrar does not switch the site off. It points the domain at its own parking page, the &quot;this domain may be for sale&quot; one. That page is a normal web page. It returns &lt;code&gt;200 OK&lt;/code&gt; and loads fast. It stays that way for a while too, because an expired domain is not deleted the next morning.&lt;/p&gt;
&lt;p&gt;So the monitor sees a healthy site. Green light, no alert, everyone asleep. What could be wrong?&lt;/p&gt;
&lt;p&gt;Your domain expired. You knew the date once, on the day you bought it a year or two ago. You told yourself you would renew closer to the time, and then you forgot. The card on file lapsed, or the reminder went to an inbox you no longer read.&lt;/p&gt;
&lt;p&gt;Now your customers cannot get in. They panic. They email to ask whether you are still in business. And they get no answer, because the mail died with the domain too.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why the mail dies with it&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The registration carries your MX records, so when it lapses, mail to every address on the domain starts bouncing. The website looks up and the inbox goes silent, and the two failures do not obviously connect. That is why the angry email never reaches you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;The front page still answers. Behind it, nothing is running.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And the meter keeps running. While the page says 200, your ads keep sending paid clicks to a parking lander, signups never arrive, and recovery is not fully in your hands. Two days down is not a rounding error either, because &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/is-98-uptime-good?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=domain-expiry&amp;amp;utm_content=inline&quot;&gt;downtime adds up faster than the percentages suggest&lt;/a&gt;. Once a domain enters redemption after expiry, getting it back can cost far more than a renewal, and the registry sets the clock, not you.&lt;/p&gt;
&lt;h3 id=&quot;the-clock-after-expiry&quot;&gt;The clock after expiry&lt;/h3&gt;
&lt;p&gt;For a gTLD like a .com, the sequence is roughly this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Grace period.&lt;/strong&gt; The registrar parks the domain. The site is gone but the name still answers, usually for around 30 days, and a normal renewal still works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redemption, 30 days.&lt;/strong&gt; Now it really goes dark. You can still get it back, but through a restore fee that is many times the renewal price.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pending delete, about 5 days.&lt;/strong&gt; Nothing you can do. At the end it drops to whoever is waiting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ICANN's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.icann.org/resources/pages/domain-name-renewal-expiration-faqs-2018-12-07-en&quot;&gt;renewal and expiration FAQ&lt;/a&gt; and its &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.icann.org/resources/pages/grace-2013-05-03-en&quot;&gt;grace period notes&lt;/a&gt; have the exact stages. The practical takeaway is that the first month, the one where an uptime monitor is happily reporting 200, is the cheap month. Everything after that costs money or the domain.&lt;/p&gt;
&lt;h3 id=&quot;my-own-uptime-check-would-miss-it-too&quot;&gt;My own uptime check would miss it too&lt;/h3&gt;
&lt;p&gt;I build an uptime monitor, so here is the uncomfortable part. My own HTTP check would have called that dead site up too. A 200 is a 200. A status-code check cannot tell a real page from a parking page, and it should not pretend that it can.&lt;/p&gt;
&lt;p&gt;That is the trap. One HTTP check on the homepage, the most common setup there is, is blind by design. The site answers, the code is green, and the part that pays your bills is broken behind it.&lt;/p&gt;
&lt;p&gt;You can bolt on body matching, and it helps a bit. Assert that the response contains a string only your app renders, and a parking page fails it. But a keyword check is still guessing at the cause after the fact. The registration date is a fact you can read months in advance, so read it.&lt;/p&gt;
&lt;h3 id=&quot;one-check-is-not-enough-set-up-several&quot;&gt;One check is not enough, set up several&lt;/h3&gt;
&lt;p&gt;So how do you avoid reading about your own outage in a customer's email? Not with one cleverer check. With a few plain ones, each watching a different way to fail. Start with the two that hand you a date instead of a surprise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain expiry.&lt;/strong&gt; Reads the registration record and warns you weeks before the day it lapses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS certificate expiry.&lt;/strong&gt; The same shape, a full outage with the date printed on it in advance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then cover the failures a homepage check never sees: a TCP check on your database port, a DNS check that reads the answer from outside your own network, and a heartbeat for the backup job that dies without a sound. The full list is in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/do-i-need-an-uptime-monitor?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=domain-expiry&amp;amp;utm_content=inline&quot;&gt;do I need an uptime monitor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Six checks you trust beat one that only ever watches the front door.&lt;/p&gt;
&lt;h3 id=&quot;how-to-set-up-domain-expiry-with-an-alert&quot;&gt;How to set up domain expiry with an alert&lt;/h3&gt;
&lt;p&gt;Two minutes now buys you weeks of warning later. The shape is the same in any tool that supports it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add a monitor and choose the &lt;strong&gt;Domain expiry&lt;/strong&gt; type.&lt;/li&gt;
&lt;li&gt;Enter the registered domain, for example &lt;code&gt;yourbusiness.com&lt;/code&gt;, not a subdomain.&lt;/li&gt;
&lt;li&gt;Set the thresholds in days. A common choice is a warning at 30 days and a critical alert at 7.&lt;/li&gt;
&lt;li&gt;Attach a notification channel you actually read. Telegram, Slack, PagerDuty, SMS, a webhook, or email all work. Pick the one that reaches you at night.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From then on it checks once a day, warns you when the date crosses your line, and reminds you as it gets close. No page-watching, no false green.&lt;/p&gt;
&lt;p&gt;If you would rather not add a service for it, a cron job that runs &lt;code&gt;whois&lt;/code&gt; and greps the expiry date will get you most of the way. The point is the date, not the tool. What you must not do is leave the only reminder in a registrar email going to a founder's old inbox.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One HTTP check on the homepage is not enough. It stays green while the domain, the certificate, DNS, or a background job is broken.&lt;/li&gt;
&lt;li&gt;An expired domain serves a parking page that returns 200 OK, so status-code monitors report it as healthy.&lt;/li&gt;
&lt;li&gt;Email dies at the same time and even more quietly, because the MX records lapse with the registration.&lt;/li&gt;
&lt;li&gt;You keep losing money the whole time: paid clicks to a dead page, lost signups, and a slow, costly recovery.&lt;/li&gt;
&lt;li&gt;Add the checks that fail on a known date first, domain and TLS expiry, then the rest.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;common-questions-1&quot;&gt;Common questions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why does an expired domain still return 200 OK?&lt;/strong&gt; The registrar parks it on a lander page instead of taking it offline. The old server is gone, but the parking page is a real web page that answers normally, so a check that only reads the status code sees success.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How long before an expired domain is gone for good?&lt;/strong&gt; You usually get a month or two. Grace period, then a 30-day redemption where the site is dark but recoverable for a fee, then a short pending-delete stage before it is released to anyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What else breaks when a domain expires?&lt;/strong&gt; Your email, at the same moment and more quietly. The MX records go with the registration, so mail starts bouncing while you are still looking at a page that loads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is a keyword check enough instead?&lt;/strong&gt; It catches the parking page, which is better than nothing, but only once you are already down. The expiry date is knowable months ahead. Use both if you like, but do not skip the date.&lt;/p&gt;
&lt;p&gt;I build &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=domain-expiry&amp;amp;utm_content=cta&quot;&gt;Uptimepage&lt;/a&gt; because I wanted checks like this for my own services: domain and TLS expiry, HTTP, TCP, ping and DNS, heartbeats for background jobs, and browser flows for logins, from several regions. It is AGPL-3.0 open source, so you can run it on your own server if you prefer.&lt;/p&gt;
&lt;p&gt;Now the question I actually want answered in the comments: what is the dumbest green check you have been burned by? I have seen a health endpoint that returned 200 with a hardcoded &lt;code&gt;{&quot;status&quot;:&quot;ok&quot;}&lt;/code&gt; while the database behind it was unreachable for six hours. I suspect yours is better.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24097/your-domain-can-expire-while-your-uptime-monitor-stays-green</guid>
<pubDate>Tue, 04 Aug 2026 14:19:13 +0000</pubDate>
</item>
<item>
<title>AI Search Optimization: What to Monitor without a subscription</title>
<link>https://coderlegion.com/24001/ai-search-optimization-what-to-monitor-without-a-subscription</link>
<description>&lt;p&gt;Procurement wants a line item for &quot;AI search.&quot; A vendor demo shows green citation bars for category prompts. Your team has not yet confirmed whether GPTBot can fetch pricing, docs, or product templates after last week's theme deployment. Buying the dashboard now answers one question and leaves the fetchability question unowned. AI search optimisation starts with signals you can check without a GEO subscription: bot policy, HTTP health, response time, and Core Web Vitals on the URLs buyers actually need. Agencies need a decision framework next: which layer needs a paid visibility platform, which layer you can run with tools you already use, and how PageSpeed monitoring for agencies covers the deterministic half without pretending to score ChatGPT mentions.&lt;/p&gt;&lt;h2 id=&quot;two-layers-of-ai-search-optimization-and-which-needs-a-subscription&quot; class=&quot;scroll-mt-24&quot;&gt;Two layers of AI search optimization (and which needs a subscription)&lt;/h2&gt;&lt;p&gt;Treat AI search work as two layers with different questions, tools, and failure modes. Mixing them on one slide produces arguments in week two when the homepage prompt chart looks fine and checkout times out for crawlers. Name the layer before you buy software, or the wrong team owns the wrong failure.&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Layer&lt;/th&gt;&lt;th&gt;Question&lt;/th&gt;&lt;th&gt;Tool type&lt;/th&gt;&lt;th&gt;Watcher role&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Probabilistic&lt;/td&gt;&lt;td&gt;Are we cited for buyer-context prompts?&lt;/td&gt;&lt;td&gt;GEO / AI visibility SaaS, prompt trackers, manual ChatGPT or Perplexity checks&lt;/td&gt;&lt;td&gt;None; we do not score citations&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Deterministic&lt;/td&gt;&lt;td&gt;Can bots fetch and render priority URLs fast enough?&lt;/td&gt;&lt;td&gt;`robots.txt` review, crawl or fetch checks, scheduled PageSpeed / Lighthouse, regression alerts&lt;/td&gt;&lt;td&gt;Core product: portfolio schedules, budgets, alerts, client reports&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The probabilistic layer answers mention and citation trends for a fixed prompt set. Outputs are directional: useful for trends, weak as courtroom proof. Models drift, phrasing changes answers, and a competitor screenshot does not prove your pricing page is reachable today. Use GEO platforms when the retainer includes citation reporting and someone owns a prompt library with buyer context. Layer those tools beside technical monitoring. Do not replace crawl and speed work with prompt scores alone.&lt;/p&gt;&lt;p&gt;The deterministic layer answers access and speed on URLs you choose. Either the response completes with parseable content, or it does not. That half needs no GEO subscription, because it reuses performance monitoring, crawl hygiene, and reporting rhythms agencies already sell. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/ai-search-visibility-agencies-measure-first&quot;&gt;Are We Visible in ChatGPT? What Agencies Can Measure First&lt;/a&gt; frames the client conversation. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/gptbot-performance-monitor-checkout-not-ai-visibility&quot;&gt;Monitor GPTBot performance on checkout, not AI visibility alone&lt;/a&gt; covers high-intent URL failures. The job here is deciding what to measure before you sign a visibility contract.&lt;/p&gt;&lt;h2 id=&quot;what-ai-search-optimization-means-before-you-buy-a-visibility-tool&quot; class=&quot;scroll-mt-24&quot;&gt;What AI search optimization means before you buy a visibility tool&lt;/h2&gt;&lt;p&gt;Clients say &quot;AI search optimisation&quot; and often mean three different jobs: appear in ChatGPT or Perplexity answers, stay fetchable for AI crawlers, and protect clicks when Google AI Overviews answer without a visit. Those jobs share a site, but they do not share a measurement stack. Clarifying which job is in scope keeps procurement from funding citation charts while fetchability stays unowned.&lt;/p&gt;&lt;p&gt;Without a subscription to a visibility platform, you can still do useful AI search optimisation work:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Decide bot policy and write it down (allow, block, or mixed by user-agent).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Build a priority URL list by business intent, not sitemap size.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Confirm public routes return clean status codes and sane redirect chains.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Baseline lab Core Web Vitals and response time on those URLs, mobile and desktop.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Schedule recurring tests so theme, app, and CDN changes do not erase the baseline overnight.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;None of those steps requires a GEO licence. They require scope, ownership, and a habit of checking the same URLs after releases. Call it PageSpeed monitoring for agencies applied to retrieval risk, not a new product category. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-ai-crawlers-need-fast-crawlable-pages-and-how-to-stay-ready&quot;&gt;Why AI Crawlers Need Fast, Crawlable Pages&lt;/a&gt; explains why fetchability still comes before citation.&lt;/p&gt;&lt;h2 id=&quot;what-agencies-can-monitor-for-ai-search-without-a-geo-subscription&quot; class=&quot;scroll-mt-24&quot;&gt;What agencies can monitor for AI search without a GEO subscription&lt;/h2&gt;&lt;p&gt;Start with artefacts you can open this week: production &lt;code&gt;robots.txt&lt;/code&gt;, a ten-to-twenty URL inventory, PageSpeed Insights or Lighthouse on mobile and desktop, and optional server logs filtered for AI user-agents. The goal is a baseline you can defend on a client call, not a vanity &quot;AI score.&quot; Save dates, devices, and owners in the client file so the next deploy has something to compare against. If the file only holds a homepage screenshot from last month, you are still guessing after the next theme release.&lt;/p&gt;&lt;h3&gt;GPTBot, robots.txt, and AI crawler access checks&lt;/h3&gt;&lt;p&gt;OpenAI documents &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.openai.com/docs/gptbot&quot;&gt;GPTBot&lt;/a&gt; in &lt;code&gt;robots.txt&lt;/code&gt;, and Google publishes &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers&quot;&gt;crawler guidance&lt;/a&gt; for Search and related products. Fetch production &lt;code&gt;robots.txt&lt;/code&gt; before the visibility debate. Five minutes here prevents expensive arguments later.&lt;/p&gt;&lt;p&gt;Agency checklist without a visibility vendor:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Fetch production &lt;code&gt;robots.txt&lt;/code&gt; and note &lt;code&gt;Disallow&lt;/code&gt; rules for GPTBot, Google-Extended, and other AI user-agents the client names.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Confirm public marketing, docs, and pricing paths are allowed unless the client explicitly opts out of training or retrieval.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Document opt-out decisions in writing. Blocking GPTBot is a valid policy choice; claiming the site is optimised for AI while disallowing crawlers is not.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Re-check after CMS or SEO plugin updates. All-in-one SEO tools sometimes ship aggressive bot blocks.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Keep a one-page bot policy per client next to the URL inventory. Account managers need a clear answer on whether the retainer includes crawler access review or only performance monitoring. When the client changes CMS, SEO plugins, or hosting, update both documents the same week.&lt;/p&gt;&lt;h3&gt;Priority URLs, HTTP health, and response time for AI crawlers&lt;/h3&gt;&lt;p&gt;Homepage-only checks hide the routes AI systems retrieve when someone asks about plans, SKUs, or documentation. Build the list by business intent, then test that list on a schedule instead of hoping a sitemap crawl catches money pages. Ten to twenty URLs with owners beat a full crawl nobody reads.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Pricing and plan comparison pages&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Product detail and category templates for ecommerce&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Documentation and help centre articles used in sales&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Checkout and cart only where unauthenticated tests are allowed&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Campaign landers that carry current offers&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For each URL, record HTTP status, redirect hop count, and lab response time on mobile and desktop. Intermittent 503s and multi-second document loads are fetchability failures even when a GEO chart still looks green. Ecommerce teams should prioritise the funnel logic in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/ecommerce-performance-monitoring-what-metrics-matter&quot;&gt;Performance Monitoring for E-Commerce: What Metrics Matter Most&lt;/a&gt;; purchase-intent routes fail quietly when monitoring stops at the marketing homepage.&lt;/p&gt;&lt;h3&gt;Core Web Vitals on money pages (lab baselines)&lt;/h3&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/what-are-core-web-vitals-a-practical-guide-for-2026&quot;&gt;Core Web Vitals&lt;/a&gt; are not citation factors. Google’s AI Overviews guidance stresses content quality and relevance; it does not list LCP or INP as mention levers. Lab LCP, INP, and CLS still matter. Slow or unstable pages fail the step before citation: fetch and parse under crawler timeouts.&lt;/p&gt;&lt;p&gt;Run mobile and desktop strategies on the same priority list. Crawlers often behave like lightweight clients, so a desktop-only pass hides failures phones and bots both see. Track vitals as indicators of parseability and stability. Put regressions on docs or checkout in the client report beside any future GEO chart. Pair device budgets the same way you would for traditional SEO work in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both&quot;&gt;Mobile vs Desktop Core Web Vitals&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;JavaScript-rendered pages and what AI crawlers may miss&lt;/h3&gt;&lt;p&gt;Many AI crawlers do not execute a full browser. They rely on initial HTML and simplified rendering. If docs or product UI mount copy client-side only, a crawler may see an empty shell while Chrome looks fine. That gap shows up in sales calls when stakeholders insist the page &quot;looks ready&quot; in a manual visit.&lt;/p&gt;&lt;p&gt;Lab tests through PageSpeed Insights or Lighthouse do not perfectly simulate GPTBot. They still flag render-blocking scripts, long tasks, and late LCP elements that correlate with fetch problems. Pair lab schedules with occasional log review for AI user-agents so you can see which URLs they request and whether status codes stay clean. Translate &quot;AI-ready&quot; into operational requirements: critical copy in HTML, sane response times, stable layout, and monitoring on the URLs where answers should come from.&lt;/p&gt;&lt;h2 id=&quot;why-generic-ai-visibility-prompts-mislead-agency-buyers&quot; class=&quot;scroll-mt-24&quot;&gt;Why generic AI visibility prompts mislead agency buyers&lt;/h2&gt;&lt;p&gt;Visibility demos love tidy graphs for prompts such as &quot;best pagespeed tool 2026.&quot; Those graphs tell you almost nothing about whether you appear when a technical lead asks how to monitor forty Shopify stores after a theme deployment. Generic category prompts produce neat before-and-after slides with weak buyer context. Agency-real questions name the vertical, the stack, and the trigger event.&lt;/p&gt;&lt;p&gt;If you later buy a GEO platform, insist the prompt library matches how clients buy, not how vendors demo. Until then, a competitor's category-prompt screenshot is not proof your deterministic layer is healthy. Fix fetchability on named URLs first. Debate prompt design afterwards with someone who understands the client's ICP.&lt;/p&gt;&lt;h2 id=&quot;pagespeed-monitoring-for-agencies-free-check-to-scheduled-tests&quot; class=&quot;scroll-mt-24&quot;&gt;PageSpeed monitoring for agencies: free check to scheduled tests&lt;/h2&gt;&lt;p&gt;One-off PageSpeed Insights tabs do not survive a release train. Agencies managing multiple clients need history, comparison, and alert routing on the same URLs every week. That matches the operational model in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs automated monitoring&lt;/a&gt;: scheduled lab runs, stored results, budgets, and digests when thresholds breach.&lt;/p&gt;&lt;p&gt;A practical upgrade path without jumping straight to GEO SaaS:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;Spot-check five priority URLs in PageSpeed Insights on mobile and desktop, then save the dates and scores in the client file.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Run the public &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;free domain PageSpeed check&lt;/a&gt;, or follow the multi-page workflow in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/free-domain-pagespeed-check-multi-page-reports&quot;&gt;Free Domain PageSpeed Check: Multi-Page Performance Reports&lt;/a&gt;, for a client-ready snapshot before schedules exist.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Move the same URL list into recurring tests with budgets and alerts so theme deployments have an owner when LCP or response time drifts.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Add citation analytics only when the retainer includes prompt-level reporting and deterministic baselines are not the obvious bottleneck.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Apogee Watcher is built for step three: organisations per agency, sites per client, discovery and manual URL lists, mobile and desktop strategies, and email when lab metrics cross budgets. We do not score ChatGPT visibility. We tell you when priority URLs slow down, error out, or drift on vitals after the deployment that might affect fetchability. Setup steps live in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/getting-started-apogee-watcher-step-by-step-setup-guide&quot;&gt;Getting Started with Apogee Watcher&lt;/a&gt;. For feature comparisons across monitoring vendors, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;Comparing PageSpeed Monitoring Tools: Features Agencies Need&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;when-a-paid-ai-visibility-platform-is-worth-buying&quot; class=&quot;scroll-mt-24&quot;&gt;When a paid AI visibility platform is worth buying&lt;/h2&gt;&lt;p&gt;Add a dedicated GEO or AI visibility platform when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Leadership expects monthly citation charts in QBRs.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Content strategy targets LLM mentions for specific buyer personas.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Someone on the account maintains contextual prompts rather than generic category queries alone.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Deterministic baselines are already green enough that slow URLs are not the obvious bottleneck.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Defer procurement when:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;The client has not agreed bot policy or priority URLs.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Homepage-only monitoring hides checkout and docs problems.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;The ask is purely &quot;make us show up in ChatGPT&quot; with no content or technical owner.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;You have not yet run a free or scheduled performance baseline on money URLs.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The sequence protects credibility: measure what you can fix without a visibility subscription, then buy citation analytics when scope and staffing justify it. Layer performance monitoring beside GEO tools; do not treat prompt scores as a substitute for fetch health on money URLs. A green citation chart next to a red checkout lab row is still an incomplete AI search story.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Do we need an AI visibility subscription for AI search optimization?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Not on day one. You can audit bot policy, build a priority URL list, baseline lab Core Web Vitals and response time, and schedule monitoring without a GEO licence. Add a visibility platform when citation reporting is contracted and someone owns buyer-context prompts. Until then, the deterministic checklist is enough to keep the engagement honest.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What should agencies monitor for AI search without GEO tools?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Monitor crawler access in &lt;code&gt;robots.txt&lt;/code&gt;, HTTP status and redirects on priority URLs, lab LCP, INP, CLS, and response time on mobile and desktop, and regressions after deployments. Those signals prove fetchability. They do not prove citation, so keep both layers labelled when you report to clients.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Can green Core Web Vitals guarantee ChatGPT citations?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Green lab vitals indicate priority URLs are likely fetchable and parseable under test conditions. Citation depends on content, authority, relevance, and model behaviour. Treat speed and stability as necessary technical hygiene, not a mention guarantee.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How is PageSpeed monitoring for agencies different from an AI visibility platform?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;PageSpeed monitoring for agencies is the operational system for the deterministic layer: schedules, history, budgets, and alerts across client sites. An AI visibility platform tracks prompt-level mentions. Use monitoring for fetch health. Use GEO for citation trends when the retainer requires both.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Should we block GPTBot while we decide on a GEO vendor?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Blocking GPTBot is a policy decision, not a procurement shortcut. Document the choice with the client. If you block training or retrieval crawlers, do not claim the site is optimised for those products. Align monitoring with the routes you intend to keep public and reachable.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How many URLs should we cover before a visibility demo?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Start with ten to twenty priority URLs per client: pricing, key product or service templates, docs paths that support sales, and checkout where policy allows. Expand when alert triage stays manageable. A demo that only covers the homepage is not a fetchability baseline.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Can Apogee Watcher replace a GEO platform?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Watcher schedules PageSpeed lab tests, tracks vitals and budgets, and alerts on regressions across client sites. GEO platforms track prompt-level citations. Use both when the retainer covers technical performance and citation reporting. Use Watcher alone when the immediate risk is slow or blocked money URLs.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;When leadership asks for AI search optimisation, answer with layers: what a visibility subscription can track over time, and what you can prove about fetchability and speed on named URLs without that subscription. Start with bot policy, priority URLs, and scheduled PageSpeed baselines. Add GEO analytics when content strategy and staffing support buyer-context prompts, not when a demo chart arrives before the first deterministic readout.&lt;/p&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;Start a free Apogee Watcher account&lt;/a&gt; to schedule mobile and desktop tests with vitals budgets across client sites, or run a &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;free domain PageSpeed check&lt;/a&gt; before the first AI visibility procurement call.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;References&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.openai.com/docs/gptbot&quot;&gt;GPTBot&lt;/a&gt; (OpenAI)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers&quot;&gt;Overview of Google crawlers&lt;/a&gt; (Google Search Central)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pagespeed.web.dev/&quot;&gt;PageSpeed Insights&lt;/a&gt; (Google)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/ai-search-visibility-agencies-measure-first&quot;&gt;Are We Visible in ChatGPT? What Agencies Can Measure First&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/gptbot-performance-monitor-checkout-not-ai-visibility&quot;&gt;Monitor GPTBot performance on checkout, not AI visibility alone&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-ai-crawlers-need-fast-crawlable-pages-and-how-to-stay-ready&quot;&gt;Why AI Crawlers Need Fast, Crawlable Pages&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring: When Manual Checks Aren't Enough&lt;/a&gt;
&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24001/ai-search-optimization-what-to-monitor-without-a-subscription</guid>
<pubDate>Tue, 04 Aug 2026 12:22:00 +0000</pubDate>
</item>
<item>
<title>How I stop one bad probe from waking you at 3 a.m.</title>
<link>https://coderlegion.com/23750/how-i-stop-one-bad-probe-from-waking-you-at-3-a-m</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; The most common false alert in uptime monitoring is one probe location having a bad network day. I treated that as a design requirement from day one, so the checking pipeline has two gates: a region only counts as down after it fails the same check twice in a row, and an incident only opens when enough regions agree. A region that goes silent leaves the vote instead of counting as down. One bad location cannot page you. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/stop-false-uptime-alerts?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=false-alerts&amp;amp;utm_content=inline&quot;&gt;original post&lt;/a&gt; carries the three figures below as live widgets you can drive yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-alert-that-was-nothing&quot;&gt;The alert that was nothing&lt;/h3&gt;
&lt;p&gt;The phone buzzes at 3 a.m. &quot;Your API is down.&quot; You get up, open the laptop, and everything is green. The site was fine the whole time. One monitoring server, somewhere far away, had a bad network moment and sent an alert about nothing.&lt;/p&gt;
&lt;p&gt;This is the false alert everyone in monitoring knows. It costs you sleep, and then it costs you something worse: the next alert feels less serious. The day a real outage comes, you look at your phone and think &quot;probably nothing again.&quot;&lt;/p&gt;
&lt;p&gt;I knew this failure mode before I wrote the first line of the scheduler, so it became a requirement, on the same level as &quot;checks must run on time.&quot; The rule I started from: a single bad location must never be able to page a customer. The whole checking pipeline, from how probes report to how incidents open, is shaped by that rule.&lt;/p&gt;
&lt;p&gt;The shape it took is two gates. A failure has to pass both before anyone gets paged.&lt;/p&gt;
&lt;h3 id=&quot;gate-one-fail-twice-from-the-same-place&quot;&gt;Gate one: fail twice, from the same place&lt;/h3&gt;
&lt;p&gt;One failed check proves very little. Networks lose packets, routers restart, and sometimes a DNS answer arrives a second too late. All of that can make one check fail while your site is healthy.&lt;/p&gt;
&lt;p&gt;So a region only counts as down after it fails the same check twice in a row. Not two failures somewhere in the system: two failures from that one region, back to back. A single blip resets to zero on the next good check.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12864421534086877711&quot; alt=&quot;Two rows of check results. In the top row a single failed check sits between passing ones, the counter resets and nothing happens. In the bottom row two failures land back to back, so that region counts as down and earns one vote.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One blip resets the counter. Two failures in a row from the same region earn that region a single vote.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The count is a setting on each monitor. Two is the default. For a very sensitive check you can set it to one, and for a noisy target you can raise it. Checks run on the monitor's own interval, so with a one-minute check the second failure arrives about a minute after the first. That minute buys you a lot of silence for a very small delay.&lt;/p&gt;
&lt;h3 id=&quot;gate-two-the-vote&quot;&gt;Gate two: the vote&lt;/h3&gt;
&lt;p&gt;Passing gate one gives a region exactly one vote. Nothing more.&lt;/p&gt;
&lt;p&gt;Say you check from five regions and one of them has a bad ISP day. It fails twice in a row and votes &quot;down.&quot; The other four keep passing. One vote against four is not enough, so nothing happens and nobody gets paged.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14674027907044300991&quot; alt=&quot;Two votes across five regions. In the first, one region is down and four are up, there is no majority and nobody is paged. In the second, three of the five are down, that is a majority and an incident opens.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One region against four cannot page you. Three of five can.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The default rule is a majority: more than half of the reporting regions have to agree. You can change it per monitor. &quot;Any&quot; opens an incident on the first confirmed region, which is useful when you want the earliest possible signal and can accept some noise. &quot;All&quot; waits until every region agrees, for a service that only matters when it is unreachable everywhere. Or you pick a fixed number, like two regions out of whatever you assign.&lt;/p&gt;
&lt;p&gt;A monitor checked from a single region behaves the same under every rule. The vote only starts to protect you when you add a second location, and it gets better with a third.&lt;/p&gt;
&lt;h3 id=&quot;silence-is-not-failure&quot;&gt;Silence is not failure&lt;/h3&gt;
&lt;p&gt;Here is the part that took the most care to get right.&lt;/p&gt;
&lt;p&gt;The probe regions push their results to the control plane, the brain of the system. The brain never calls out to ask &quot;are you alive?&quot; during a vote. It just counts the results that arrived in the last few check cycles.&lt;/p&gt;
&lt;p&gt;That gives silence a clear meaning. A region that lost its own connection cannot send anything, so it simply is not in the vote. It is not a down vote and not an up vote; the region is out until it reports again. The majority recalculates over the regions that still speak. Five regions where one goes dark becomes a vote of four.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8310145531477522752&quot; alt=&quot;Five probe regions reporting to the control plane. Four send results over solid arrows, one sends nothing and is drawn as an empty dashed circle. The majority is now counted over four regions.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A silent region is not a down vote and not an up vote. It leaves the vote until it reports again.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I think this is the only honest reading. A region that cannot reach the brain is telling you nothing about your site. Treating its silence as &quot;down&quot; would turn every probe outage on the monitoring side into a fake incident on yours.&lt;/p&gt;
&lt;p&gt;And it works in both directions. Missing data never opens an incident, and it never closes one. An open incident only closes when the down votes fall below the threshold and at least one region shows a real run of passing checks. Recovery needs proof, the same way failure does.&lt;/p&gt;
&lt;h3 id=&quot;who-watches-the-watchers&quot;&gt;Who watches the watchers&lt;/h3&gt;
&lt;p&gt;There is one gap left. If every region that covers your monitor goes dark, the vote has nobody in it. Your site could burn down and no incident would open, because no data means no votes.&lt;/p&gt;
&lt;p&gt;For that case there is a separate signal, one level below the checks. Every probe sends a small check-in to the brain on its own schedule, a heartbeat that has nothing to do with your monitors. When the last live probe covering a monitor goes stale, you get a different message: &quot;NO DATA: monitoring interrupted, no check results received.&quot; It is honest about what it knows: the service cannot see your site right now. It does not claim your site is down, because it has no idea.&lt;/p&gt;
&lt;p&gt;When probing returns, you get a &quot;monitoring RESUMED, receiving check results again&quot; note, and the vote picks up where it left off.&lt;/p&gt;
&lt;p&gt;One more detail I like. If a large share of all monitors goes silent at the same time, that pattern is almost never a thousand customer problems. It is one problem, mine. In that case the system alarms me and holds the customer notices, so my bad day does not become spam on yours.&lt;/p&gt;
&lt;h3 id=&quot;what-this-means-for-your-setup&quot;&gt;What this means for your setup&lt;/h3&gt;
&lt;p&gt;Three practical points follow from this, whatever tool you run.&lt;/p&gt;
&lt;p&gt;Check from more than one region. The vote cannot protect you with a single location. Two regions with the majority rule means both have to agree, which already kills the classic false alert. Three gives you two out of three, which is the right balance for most services.&lt;/p&gt;
&lt;p&gt;Leave the confirmation count at two unless you have a reason. It is the difference between &quot;a packet got lost&quot; and &quot;this endpoint is failing.&quot;&lt;/p&gt;
&lt;p&gt;Pick the rule to match the monitor. A payment API deserves majority or even any. An internal tool nobody uses at night can wait for all. The setting is per monitor, so you do not have to choose one policy for everything.&lt;/p&gt;
&lt;p&gt;The goal of all this machinery is boring: when your phone buzzes, it is real. Everything else is plumbing.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One failed check from one place proves nothing. Confirm it from the same region before that region gets a say.&lt;/li&gt;
&lt;li&gt;Give each confirmed region one vote, then require agreement. A majority of reporting regions is a good default.&lt;/li&gt;
&lt;li&gt;A silent region must leave the vote, not join the down side. Otherwise every probe outage becomes a fake incident.&lt;/li&gt;
&lt;li&gt;Missing data must not close an incident either. Recovery needs a real run of passing checks.&lt;/li&gt;
&lt;li&gt;Cover the all-dark case with a separate heartbeat and a message that says &quot;we cannot see your site,&quot; not &quot;your site is down.&quot;&lt;/li&gt;
&lt;li&gt;Two regions kill the classic false alert. Three is the sweet spot for most services.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The three figures above are live widgets in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/stop-false-uptime-alerts?utm_source=coderlegion&amp;amp;utm_medium=community&amp;amp;utm_campaign=false-alerts&amp;amp;utm_content=cta&quot;&gt;original post&lt;/a&gt;: you can change the confirmation count and the vote rule and watch where the line falls. The whole project is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;open source&lt;/a&gt;, so the scheduler and the incident writer are there to read.&lt;/p&gt;
&lt;p&gt;How does your setup handle a probe that goes quiet? I have seen tools count silence as down, and I would like to know if anyone has a better reading than &quot;it leaves the vote.&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23750/how-i-stop-one-bad-probe-from-waking-you-at-3-a-m</guid>
<pubDate>Wed, 29 Jul 2026 15:36:57 +0000</pubDate>
</item>
<item>
<title>Automate Lighthouse Audits with AI Agents: What Chrome DevTools Means for Agencies</title>
<link>https://coderlegion.com/23585/automate-lighthouse-audits-with-ai-agents-what-chrome-devtools-means-for-agencies</link>
<description>&lt;p&gt;Chrome has documented how coding agents can run &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.chrome.com/docs/devtools/agents/use-cases/lighthouse-audit&quot;&gt;Lighthouse audits inside Chrome DevTools for agents&lt;/a&gt;. Instead of grepping a repository for vague performance clues, the agent loads the page you care about and measures accessibility, SEO, best practices, and agentic browsing against live runtime behaviour. That loop is better for fixing a checkout UI on a local server than another round of static guessing from file names alone.&lt;/p&gt;&lt;p&gt;Agencies should treat this as a development tool, not a finished monitoring programme. A Lighthouse AI agent can catch colour contrast failures or missing meta tags while a developer still has the branch open. It does not replace scheduled tests across forty client sites, budget thresholds, or alerts when a theme deploy regresses Largest Contentful Paint overnight. The sections below separate what Chrome’s agent workflow does well from what multi-tenant PageSpeed monitoring still has to own.&lt;/p&gt;&lt;h2 id=&quot;what-a-lighthouse-ai-agent-measures-in-chrome-devtools&quot; class=&quot;scroll-mt-24&quot;&gt;What a Lighthouse AI agent measures in Chrome DevTools&lt;/h2&gt;&lt;p&gt;Chrome’s documentation groups agent-driven Lighthouse work into four categories. Accessibility covers screen-reader readiness and related checks such as contrast and ARIA labelling. SEO covers technical discoverability signals search engines need to find and understand the page. Best practices covers modern web hygiene: HTTPS usage, console errors, deprecated APIs, and similar technical standards. Agentic browsing covers how far AI assistants can understand and interact with the site, which sits beside Core Web Vitals work rather than replacing it.&lt;/p&gt;&lt;p&gt;The useful claim is runtime evidence. The agent is not inventing a checklist from file names. It triggers Lighthouse against a page that is actually visible in Chrome, including local development servers and HTML opened through the &lt;code&gt;file://&lt;/code&gt; protocol. Staging URLs count the same way. A coding agent can verify a UI change on &lt;code&gt;localhost&lt;/code&gt; before anyone merges, which is where agencies often lose days when accessibility and technical SEO fail only after client UAT.&lt;/p&gt;&lt;h3&gt;Local, staging, and file:// support for Chrome DevTools agents&lt;/h3&gt;&lt;p&gt;Chrome states that the agent can audit any page visible in the browser. That includes local development servers and local HTML files. For agency teams, that means you can run a Lighthouse AI agent against a feature branch without waiting for a public staging hostname. Keep authentication and feature flags in mind: a logged-in staging route can look healthy while the anonymous production path still fails SEO or accessibility checks.&lt;/p&gt;&lt;p&gt;Chrome also warns about browser behaviour during audits. The page may resize or reload while Lighthouse simulates different devices. Anyone pairing an agent with a shared staging tab should expect brief disruption. Treat agent audits as intentional lab sessions, not something you leave running on a client demo screen without warning.&lt;/p&gt;&lt;h2 id=&quot;prompts-for-a-lighthouse-ai-agent-accessibility-seo-and-best-practices&quot; class=&quot;scroll-mt-24&quot;&gt;Prompts for a Lighthouse AI agent: accessibility, SEO, and best practices&lt;/h2&gt;&lt;p&gt;Chrome’s examples are short enough to paste into a coding agent that already has DevTools agent tooling. They map cleanly to delivery habits agencies already use. Keep the prompt, the failing audits, and the re-run result in the ticket so the fix is reviewable after the chat window closes.&lt;/p&gt;&lt;h3&gt;How to improve accessibility with a Lighthouse AI agent&lt;/h3&gt;&lt;p&gt;Prompt: “How can I improve accessibility on this page as measured by Lighthouse?” The documented behaviour is a mobile &lt;code&gt;formFactor&lt;/code&gt; audit that surfaces issues such as insufficient colour contrast or missing ARIA labels, then proposes concrete code fixes. That pass earns its keep when a designer ships a new badge colour that still looks fine to sighted reviewers on a calibrated monitor. Ask for the failing audit IDs in the pull request so review does not depend on chat history alone.&lt;/p&gt;&lt;h3&gt;How to catch technical SEO blockers before launch&lt;/h3&gt;&lt;p&gt;Prompt: “According to Lighthouse audit, how can I improve this page for better discoverability in search?” The agent looks for missing meta tags, canonical links, or weak descriptive text and can offer source updates. Catching those blockers in staging is cheaper than explaining a soft indexing story after launch. Pair the audit with the templates that matter commercially (product, pricing, checkout), not only the marketing homepage the agent happens to have open.&lt;/p&gt;&lt;h3&gt;How to validate best practices on local and staging&lt;/h3&gt;&lt;p&gt;Prompt: “Does my site follow best practices as measured by Lighthouse?” The audit reviews HTTPS, console errors, and deprecated API calls. Agencies often skip this pass because the design looks finished. Console noise and mixed content still show up in client QA and burn trust in the report pack.&lt;/p&gt;&lt;h3&gt;When to run a full mobile and desktop Lighthouse audit&lt;/h3&gt;&lt;p&gt;Prompt: “Run a full Lighthouse audit of my site, in mobile and desktop, and suggest improvement areas and possible fixes for any problems you find.” Chrome notes that agents with source context may suggest code fixes, and agents with broader context may also suggest server or backend configuration changes. Treat backend suggestions as hypotheses for your platform team, not as automatic production changes. Re-run after the patch so the conversation ends on measured results, not on a single hopeful pass.&lt;/p&gt;&lt;p&gt;None of these prompts invents a new quality bar. They attach an existing Lighthouse run to the agent’s conversation so the next patch is grounded in failing audits rather than guesswork. If the agent cannot see the page in Chrome, fix the browser connection first; no prompt compensates for an empty viewport.&lt;/p&gt;&lt;h2 id=&quot;lighthouse-agentic-browsing-versus-core-web-vitals-reporting&quot; class=&quot;scroll-mt-24&quot;&gt;Lighthouse agentic browsing versus Core Web Vitals reporting&lt;/h2&gt;&lt;p&gt;Agentic browsing is the category that often confuses stakeholders. It is not another 0–100 Performance score. It asks whether assistants can understand and interact with the page: tool registration patterns such as WebMCP, agent-oriented accessibility, layout stability that affects programmatic clicks, and related discoverability signals. We already covered how experimental Lighthouse agentic browsing scoring works, including fractional pass ratios and WebMCP observation, in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-agentic-browsing-scoring&quot;&gt;Lighthouse Agentic Browsing: How to rank in chatbots&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For agencies, the operational rule is simple. Keep Core Web Vitals and PageSpeed Performance scores as the client-facing performance language you already report. Use agentic browsing audits as an early research signal when a product site exposes agent tools or when a client asks whether AI assistants can use their flows. Do not promote experimental pass ratios into a retainer KPI until the standard and your tooling story are stable. Chrome’s agent docs put agentic browsing in the same Lighthouse family as accessibility and SEO, which is correct for developers, but buyers still buy predictability on Largest Contentful Paint, Interaction to Next Paint, and Cumulative Layout Shift first.&lt;/p&gt;&lt;p&gt;If you need the math behind how Lighthouse turns raw timings into a Performance category score, Chrome documents the scoring model, and we have a dedicated Watcher explainer scheduled for that calculation path. The agency takeaway for agent workflows is narrower: a coding agent can improve category scores by fixing the failed audits it sees, but those lab scores are still synthetic. Field Core Web Vitals remain a separate truth.&lt;/p&gt;&lt;h2 id=&quot;why-one-ai-coding-agent-lighthouse-run-is-not-portfolio-monitoring&quot; class=&quot;scroll-mt-24&quot;&gt;Why one AI coding agent Lighthouse run is not portfolio monitoring&lt;/h2&gt;&lt;p&gt;A successful agent session looks like this: open the priority URL on staging, run accessibility and SEO audits, apply fixes, re-run, then merge when the failures that blocked the ticket are cleared. That loop is useful. It is also a single device, a single URL, a single moment in time, and usually a single human watching the tab.&lt;/p&gt;&lt;p&gt;Agency failure modes live elsewhere. A plugin update lands on Friday across twelve WordPress sites. A Shopify theme ships a new app block that only hurts mobile Interaction to Next Paint on product templates. A marketing team adds a tag manager container that never appears on the developer’s &lt;code&gt;localhost&lt;/code&gt; build. Spot PageSpeed Insights runs and agent audits both miss that class of regression unless someone remembers to re-check every affected URL after every change. We unpacked when manual checks stop scaling in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs Automated Monitoring: When Manual Checks Aren't Enough&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Synthetic versus field data adds another split. Agent-driven Lighthouse is synthetic lab evidence, closer to a controlled PageSpeed Insights run than to Chrome UX Report percentiles. Knowing when to lean on lab schedules versus real-user monitoring is a separate decision; see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/when-to-use-synthetic-vs-real-user-monitoring-performance&quot;&gt;When to Use Synthetic vs Real User Monitoring for Performance&lt;/a&gt;. Use the agent to fix what is broken in the environment you control, then keep scheduled synthetic tests on production priority URLs so regressions after merge still raise an alert.&lt;/p&gt;&lt;h2 id=&quot;how-agencies-combine-chrome-devtools-agents-with-scheduled-pagespeed-monitoring&quot; class=&quot;scroll-mt-24&quot;&gt;How agencies combine Chrome DevTools agents with scheduled PageSpeed monitoring&lt;/h2&gt;&lt;p&gt;A practical split that matches how delivery teams already work:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;During build. Use a Lighthouse AI agent on local and staging for accessibility, technical SEO, best practices, and optional agentic browsing when the product exposes agent tools. Paste Chrome’s example prompts and keep the failing audit list in the pull request.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;At release. Re-run the same priority templates on the staging hostname the client will accept, still with the agent or a manual Lighthouse pass if the agent tooling is unavailable.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;After release. Rely on scheduled PageSpeed Insights or Lighthouse runs across the portfolio, with budgets and alerts on the URLs that earn revenue or retention. That is the layer Apogee Watcher is built for: multi-tenant schedules, history, and client-ready reporting without asking a developer to babysit Chrome tabs.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Layer, do not replace. Coding agents do not remove the need for continuous monitoring any more than Lighthouse CI removes the need for field Core Web Vitals. They shorten the time from “UI changed” to “measurable defect named.” Monitoring shortens the time from “production changed” to “someone on the account knows.” If you want a fast baseline before you wire schedules, run a &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/free-domain-pagespeed-check-multi-page-reports&quot;&gt;free domain PageSpeed check&lt;/a&gt; to see multi-page lab results for a hostname. When you are ready to put priority URLs on a recurring cadence, follow &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/getting-started-apogee-watcher-step-by-step-setup-guide&quot;&gt;Getting Started with Apogee Watcher: A Step-by-Step Setup Guide&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;common-mistakes-with-chrome-devtools-agent-lighthouse-audits&quot; class=&quot;scroll-mt-24&quot;&gt;Common mistakes with Chrome DevTools agent Lighthouse audits&lt;/h2&gt;&lt;h3&gt;Treating a green agent pass as production proof&lt;/h3&gt;&lt;p&gt;Local data, auth walls, and feature flags differ from what real users and the Chrome UX Report show. A clean mobile audit on staging can still leave desktop production amber on Interaction to Next Paint. Ship the fix, then confirm on the production URL with the same device form factor you care about in the retainer report.&lt;/p&gt;&lt;h3&gt;Auditing only the homepage&lt;/h3&gt;&lt;p&gt;Agents will audit whatever page is loaded. Homepages are convenient and often wrong for ecommerce and SaaS work. If checkout, pricing, or login is where money and trust live, load those routes before you ask for a Lighthouse AI agent run.&lt;/p&gt;&lt;h3&gt;Ignoring audit disruption on shared environments&lt;/h3&gt;&lt;p&gt;Resize and reload behaviour can interrupt demos and live editing. Chrome documents that Lighthouse may change viewport size while simulating devices. Schedule agent audits the same way you schedule Lighthouse CI: on purpose, not mid-call with a client watching the same tab.&lt;/p&gt;&lt;h3&gt;Confusing Lighthouse agentic browsing with SEO rankings&lt;/h3&gt;&lt;p&gt;Agentic browsing audits help you understand assistant interaction readiness. They are not a substitute for crawlability, content quality, or Core Web Vitals programmes you already sell. Keep the category in research notes until you and the client agree what a pass ratio means commercially.&lt;/p&gt;&lt;h3&gt;Skipping history after the coding agent finishes&lt;/h3&gt;&lt;p&gt;An agent conversation disappears when the chat ends. Agencies need trend lines and budget breaches stored against the site record. Without history, last week’s “we fixed contrast” claim is hard to prove in a monthly review, and the next developer repeats the same audit from scratch.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Does a Lighthouse AI agent replace PageSpeed Insights?&lt;/strong&gt;No. The agent uses Lighthouse categories in a developer workflow inside Chrome. PageSpeed Insights remains a common lab entry point for stakeholders, and scheduled PageSpeed Insights or Lighthouse API runs remain how you watch many URLs over time. Use the agent to fix; use monitoring to watch. For the wider comparison of spot checks versus automation, see our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights versus automated monitoring&lt;/a&gt; guide.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Can coding agents audit pages that are not on the public internet?&lt;/strong&gt;Yes, within Chrome’s stated support: any page visible in the browser, including local servers and &lt;code&gt;file://&lt;/code&gt; HTML. That is one of the strongest reasons to use agents during build rather than waiting for a public staging URL. Confirm the same checks later on the hostname clients and crawlers actually hit.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Should we report agentic browsing scores to clients every month?&lt;/strong&gt;Only if you have agreed what the category means and how experimental it still is. Most retainers should keep leading with Core Web Vitals and clear lab Performance trends, then mention agentic browsing when a product roadmap or assistant integration makes it relevant. Point curious clients at the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-agentic-browsing-scoring&quot;&gt;agentic browsing scoring explainer&lt;/a&gt; before you put pass ratios on a dashboard.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Where do budgets and alerts fit if developers already run agent audits?&lt;/strong&gt;Budgets and alerts cover the gap after merge: unexpected third-party tags, plugin updates, and content changes that never open a coding agent session. Agent audits reduce defects entering the branch. Monitoring reduces defects lingering in production unnoticed. Set budgets on the same priority URLs you used in the agent session so the definition of “good” does not drift between tools.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;Chrome DevTools for agents makes Lighthouse a first-class tool in coding-agent workflows. Accessibility, SEO, best practices, and agentic browsing can be measured on the page under change, including local and staging hosts. Agencies win when they adopt that loop for delivery quality and keep scheduled, multi-site PageSpeed monitoring for everything that happens after the pull request merges. Start with Chrome’s documented prompts on your next UI branch, then put the same priority URLs on a schedule so the fix stays fixed.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23585/automate-lighthouse-audits-with-ai-agents-what-chrome-devtools-means-for-agencies</guid>
<pubDate>Tue, 28 Jul 2026 00:32:22 +0000</pubDate>
</item>
<item>
<title>Why I Started Using Tianji for Self-Hosted Monitoring</title>
<link>https://coderlegion.com/23352/why-i-started-using-tianji-for-self-hosted-monitoring</link>
<description>&lt;p&gt;As someone who runs multiple self-hosted applications, websites, and services, I’ve tried a number of monitoring tools over the years. Most either focus on one aspect website uptime, server metrics, or analytics or become expensive as your infrastructure grows.&lt;/p&gt;
&lt;p&gt;Recently, I decided to reinstall &lt;strong&gt;Tianji&lt;/strong&gt;, this time exposing it securely online so I can monitor my services from anywhere. It reminded me why it's one of my favorite open-source monitoring platforms.&lt;/p&gt;
&lt;h3 id=&quot;what-is-tianji-&quot;&gt;What is &lt;a rel=&quot;nofollow&quot; href=&quot;https://tianji.dev/&quot;&gt;Tianji&lt;/a&gt;?&lt;/h3&gt;
&lt;p&gt;Tianji is an open-source observability platform that combines several monitoring tools into a single dashboard. Instead of juggling multiple services for uptime checks, server monitoring, and website analytics, Tianji brings everything together with a clean and modern interface.&lt;/p&gt;
&lt;h3 id=&quot;what-makes-tianji-stand-out-&quot;&gt;What Makes Tianji Stand Out?&lt;/h3&gt;
&lt;h4 id=&quot;-website-uptime-monitoring&quot;&gt;🚀 Website Uptime Monitoring&lt;/h4&gt;
&lt;p&gt;You can monitor your websites, APIs, and services and receive alerts whenever something goes down. It's simple to configure and gives you a quick overview of your infrastructure's health.&lt;/p&gt;
&lt;h4 id=&quot;-privacy-friendly-website-analytics&quot;&gt;📊 Privacy Friendly Website Analytics&lt;/h4&gt;
&lt;p&gt;Unlike traditional analytics platforms that rely heavily on JavaScript trackers, Tianji provides lightweight website analytics with a strong focus on privacy. It gives useful insights without sacrificing user experience.&lt;/p&gt;
&lt;h4 id=&quot;-server-status-monitoring&quot;&gt;🖥️ Server Status Monitoring&lt;/h4&gt;
&lt;p&gt;Tianji includes a lightweight reporter agent that collects server metrics such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;Memory utilization&lt;/li&gt;
&lt;li&gt;Disk usage&lt;/li&gt;
&lt;li&gt;Network statistics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes it easy to monitor VPS instances, home labs, or dedicated servers from one dashboard.&lt;/p&gt;
&lt;h4 id=&quot;-beautiful-dashboards&quot;&gt;📈 Beautiful Dashboards&lt;/h4&gt;
&lt;p&gt;One of my favorite features is the UI. The dashboards are clean, responsive, and easy to understand. Whether you're checking uptime or reviewing analytics, everything is presented clearly.&lt;/p&gt;
&lt;h4 id=&quot;-self-hosted-and-open-source&quot;&gt;🌍 Self-Hosted and Open Source&lt;/h4&gt;
&lt;p&gt;This is probably the biggest reason I like it.&lt;/p&gt;
&lt;p&gt;Your monitoring data stays under your control. No vendor lock-in, no per-host pricing, and no concerns about sending infrastructure metrics to third-party services.&lt;/p&gt;
&lt;h3 id=&quot;my-setup&quot;&gt;My Setup&lt;/h3&gt;
&lt;p&gt;I initially ran Tianji locally for testing, but I've now moved it online so I can monitor my infrastructure remotely.&lt;/p&gt;
&lt;p&gt;My deployment runs in Docker behind a reverse proxy with HTTPS enabled. Like most self-hosted projects, the setup wasn't completely straightforward I had to work through Docker networking, PostgreSQL connectivity, reverse proxy configuration, and SSL but once everything was configured correctly, Tianji has been running reliably.&lt;/p&gt;
&lt;h3 id=&quot;a-nice-bonus-built-in-telemetry&quot;&gt;A Nice Bonus: Built in Telemetry&lt;/h3&gt;
&lt;p&gt;One feature I recently started exploring is Tianji's telemetry pixel.&lt;/p&gt;
&lt;p&gt;By embedding a tiny tracking image in blog posts or documentation, I can collect simple, privacy conscious page view statistics without integrating a heavyweight analytics platform. It's a neat feature for personal blogs, documentation sites, or static websites.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;If you're already self-hosting applications, chances are you're also running multiple monitoring tools.&lt;/p&gt;
&lt;p&gt;Tianji consolidates website monitoring, server metrics, and analytics into a single platform that is lightweight, modern, and easy to self-host. It has quickly become one of the tools I always include when provisioning a new server.&lt;/p&gt;
&lt;p&gt;If you're looking for an open-source alternative to juggling several monitoring services, Tianji is definitely worth trying.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://tianji.dev/&quot;&gt;Visit there website&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy self-hosting! &lt;a rel=&quot;nofollow&quot; href=&quot;https://monitor.myvirtualserver.com.ng/telemetry/clnzoxcy10001vy2ohi4obbi0/cmrvxyvvm000b8czcgclrenj3.gif&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;P.S. I'm using Tianji to monitor my own services, and I'll probably write a follow up post covering the Docker deployment, reverse proxy configuration with CyberPanel/OpenLiteSpeed, and some of the networking issues I encountered while exposing it to the internet.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23352/why-i-started-using-tianji-for-self-hosted-monitoring</guid>
<pubDate>Thu, 23 Jul 2026 16:23:35 +0000</pubDate>
</item>
<item>
<title>How to write incident status updates that build trust</title>
<link>https://coderlegion.com/23142/how-to-write-incident-status-updates-that-build-trust</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; During an outage your status page has a second job: holding trust while the fix is still in progress. Write plainly. Say what customers feel first, say what you are doing, and promise a time for the next update. Then keep that promise, even when there is no news. The four stages are investigating, identified, monitoring, and resolved.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea of an incident commander did not come from software. It came from wildfires. In 1970s California, a run of huge fires pushed fire chiefs to study why large responses fell apart. The cause surprised them. It was not too few firefighters or too little water. It was communication. Different teams used different words and no one shared a plan. The fix was a simple system with clear roles and clear language, &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Incident_Command_System&quot;&gt;now known as the Incident Command System&lt;/a&gt;, and on-call engineers still use a version of it today.&lt;/p&gt;
&lt;p&gt;Your status page is the communication part of that system. When something breaks, the repair happens in your code. The trust happens on the status page. This post is about the second part: how to write updates that keep people calm while the first part is still in progress.&lt;/p&gt;
&lt;h3 id=&quot;the-words-cost-real-money&quot;&gt;The words cost real money&lt;/h3&gt;
&lt;p&gt;An outage is expensive before you write a single word. In &lt;a rel=&quot;nofollow&quot; href=&quot;https://itic-corp.com/itic-2024-hourly-cost-of-downtime-report/&quot;&gt;ITIC's 2024 survey&lt;/a&gt; of more than 1,000 companies, over 90% of mid-size and large firms said one hour of downtime costs them more than &lt;em&gt;$300,000&lt;/em&gt;. For 41% of them, one hour costs between $1 million and 5 million. &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimeinstitute.com/resources/research-and-reports/annual-outage-analysis-2024&quot;&gt;Uptime Institute's outage research&lt;/a&gt; points the same way: about one in five recent outages cost more than 1 million, and more than half cost over $100,000.&lt;/p&gt;
&lt;p&gt;You do not need to be a bank for this to matter. If your store makes $2,000 a day, one bad hour during a sale can cost more than a slow afternoon, because sales are not spread evenly across the day. The size of the number changes. The shape of the problem does not.&lt;/p&gt;
&lt;p&gt;Here is the part teams forget. Some of that cost is the downtime itself. The rest is trust, and trust is where writing helps. Many providers offer an SLA, a promise to stay up with a penalty if they miss it. The penalty is almost always a service credit, which is money back on your bill. A credit refunds your invoice, not your customer's bad afternoon. Good updates cannot bring the service back. They can protect the relationship while it is down.&lt;/p&gt;
&lt;h3 id=&quot;the-four-stages-of-an-incident&quot;&gt;The four stages of an incident&lt;/h3&gt;
&lt;p&gt;Most status pages, including the big public ones, use the same four stages. They come from a shared standard, so a customer who has read one status page already understands yours.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Stage &lt;/th&gt;&lt;th&gt;What it means &lt;/th&gt;&lt;th&gt;What to write&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Investigating &lt;/td&gt;&lt;td&gt;You know something is wrong. You do not know why yet. &lt;/td&gt;&lt;td&gt;Name the impact and say you are looking into it.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Identified &lt;/td&gt;&lt;td&gt;You found the cause and are working on a fix. &lt;/td&gt;&lt;td&gt;Say what is wrong in plain words and that a fix is coming.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Monitoring &lt;/td&gt;&lt;td&gt;The fix is live. You are watching to be sure it holds. &lt;/td&gt;&lt;td&gt;Say the fix is in and you are checking that it works.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Resolved &lt;/td&gt;&lt;td&gt;The service is back and stable. &lt;/td&gt;&lt;td&gt;Say it is over, and thank people for waiting.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The order matters. Do not jump to identified because you have a hunch. If you say you found the cause and then change your story an hour later, every update after that is worth less. Move a stage forward only when the facts move with you.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-incident-lifecycle.webp&quot; alt=&quot;The four incident stages in order: investigating, identified, monitoring, resolved, each with a sample status message.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-rules-of-a-good-update&quot;&gt;The rules of a good update&lt;/h3&gt;
&lt;p&gt;Five rules cover almost every message.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with impact.&lt;/strong&gt; Say what the customer feels before you talk about servers or databases. &quot;Some payments are failing&quot; is more useful than &quot;we are seeing elevated 5xx errors on the API gateway.&quot; Only the first one means anything to a customer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share facts, not guesses.&lt;/strong&gt; Say what you have confirmed. If something is still unknown, say that too. Writing &quot;we do not yet know the cause&quot; is fine. Inventing a cause to sound in control is not. This rule earns its place under pressure: the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimeinstitute.com/resources/research-and-reports/annual-outage-analysis-2025&quot;&gt;Uptime Institute's 2025 report&lt;/a&gt; found that about 40% of organizations had a major outage caused by human error in the last three years, and 85% of those came from someone not following a procedure. An incident is exactly when tired people guess.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Promise the next update.&lt;/strong&gt; Give a real time, like &quot;next update by 15:00 UTC&quot; or &quot;another update within 30 minutes.&quot; This one line does more than any apology. It tells people they can close the tab and get on with their day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add a timestamp and a time zone.&lt;/strong&gt; &quot;In 30 minutes&quot; means nothing if the reader does not know when you posted. Every update should carry a clear time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep it plain.&lt;/strong&gt; Short sentences. No blame, no jargon, no backstory. A worried customer reads fast.&lt;/p&gt;
&lt;h3 id=&quot;a-bad-update-and-a-better-one&quot;&gt;A bad update and a better one&lt;/h3&gt;
&lt;p&gt;Here is a bad update of the kind you have all seen:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;We are aware of an issue and our team is working hard to resolve it as quickly as possible. We apologize for any inconvenience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It sounds polite and says nothing. What is broken? Who is affected? When will I hear more? A better version answers those questions:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Investigating: some customers cannot log in. Sign-ups and password resets are affected too. Our team is looking into the cause now. Next update by 14:30 UTC.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Same length. Only one of them lets a customer decide what to do next.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-incident-bad-vs-good.webp&quot; alt=&quot;A vague incident update next to a specific one that names the impact and a next-update time.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-often-to-post&quot;&gt;How often to post&lt;/h3&gt;
&lt;p&gt;Set the rhythm in your first message and keep it. Every 30 to 60 minutes is a good starting point for a serious outage. The exact number matters less than the promise. If you said 30 minutes, post at 30 minutes, even when the only news is &quot;still working, no change yet.&quot; Silence after a promised time reads as &quot;they have lost control.&quot; A boring on-time update beats an exciting late one.&lt;/p&gt;
&lt;h3 id=&quot;planned-maintenance-is-the-same-skill-calmer&quot;&gt;Planned maintenance is the same skill, calmer&lt;/h3&gt;
&lt;p&gt;Maintenance uses three stages: scheduled, in progress, and completed. The difference is that you have time to write it well in advance. Tell people what will happen, when it will happen, and whether they need to do anything. Mark it in progress while the work runs, and completed when it is done. The tone is calmer because nothing is on fire, but the shape is the same: impact, timing, next update.&lt;/p&gt;
&lt;h3 id=&quot;the-mistake-almost-everyone-makes&quot;&gt;The mistake almost everyone makes&lt;/h3&gt;
&lt;p&gt;It is not the wording. It is the second update.&lt;/p&gt;
&lt;p&gt;The first update is easy. The outage just happened, and everyone is paying attention. Then the fix takes longer than expected, the team goes quiet, and the page sits at &quot;investigating&quot; for two hours. From the outside, a frozen status page looks the same as a dead company.&lt;/p&gt;
&lt;p&gt;The fix is a habit, not a talent. Decide the next-update time in your first message, set a timer, and post again when it rings. If there is no news, that is the update: no change, still working, next check in 30 minutes. The point of the promise is that people stop refreshing and trust you to come back.&lt;/p&gt;
&lt;h3 id=&quot;write-the-message-then-give-it-a-home&quot;&gt;Write the message, then give it a home&lt;/h3&gt;
&lt;p&gt;You can write all of this by hand during an outage. Most people write worse under stress, which is the worst possible time to start from a blank box. That is why we built a free &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/incident-update-generator&quot;&gt;incident update generator&lt;/a&gt;: pick a stage, fill in a few fields, and it writes a clean message you can paste into any status page. It runs in your browser and stores nothing you type.&lt;/p&gt;
&lt;p&gt;The message still needs somewhere to live. A status page is where customers look when your product will not load, so it should be honest and easy to reach. If you are choosing an uptime number to promise, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/uptime-sla-calculator&quot;&gt;uptime SLA calculator&lt;/a&gt; turns a percentage into real minutes, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/is-98-uptime-good&quot;&gt;is 98% uptime good&lt;/a&gt; shows how small percentages hide large amounts of downtime. For the page itself, &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/status-page-you-cant-fake&quot;&gt;a status page you cannot fake&lt;/a&gt; covers how to keep it worth trusting.&lt;/p&gt;
&lt;h3 id=&quot;common-questions-2&quot;&gt;Common questions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;What should the first incident update say?&lt;/strong&gt; Say what the customer feels, say you are looking into it, and give a time for the next update. You do not need the cause yet. Waiting for the root cause before posting is the most common mistake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between investigating and identified?&lt;/strong&gt; Investigating means the cause is not confirmed. Identified means you have confirmed the cause and are working on a fix. Do not move to identified based on a hunch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should you apologize in an incident update?&lt;/strong&gt; One short, honest line is fine. Skip the long apology. A clear next-update time does more for trust than three sentences of sorry.&lt;/p&gt;
&lt;p&gt;If you have run incidents: what update cadence actually held up for you under a long outage, and did anyone ever complain that you posted too often? I have never seen that complaint, but I would like to know if it exists.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23142/how-to-write-incident-status-updates-that-build-trust</guid>
<pubDate>Tue, 21 Jul 2026 13:45:03 +0000</pubDate>
</item>
<item>
<title>Why I chose Rust over Go for an uptime monitor</title>
<link>https://coderlegion.com/23075/why-i-chose-rust-over-go-for-an-uptime-monitor</link>
<description>&lt;p&gt;I build Uptimepage, an &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;open-source uptime monitor&lt;/a&gt; and status page written in Rust. People ask why Rust and not Go, since Go is the usual pick for this kind of network service. Here is the honest answer. It is not that Rust wins everywhere. It is that one part of this job made the choice for me.&lt;/p&gt;
&lt;p&gt;The product is one promise: tell you fast and honestly when your site is slow or down. The numbers I show you, like your p99 response time, have to be clean. If my own code adds random delay, I blur the exact signal you pay for. So the runtime under the prober matters more here than it would for a normal app.&lt;/p&gt;
&lt;h3 id=&quot;garbage-collection-shows-up-in-the-tail&quot;&gt;Garbage collection shows up in the tail&lt;/h3&gt;
&lt;p&gt;Go has a garbage collector. It is fast, and most apps never feel it. But it still has to do work to free memory, and that work can land inside the millisecond timings I report. Run tens of thousands of checks at once and a pause at the wrong moment lifts a p99 number. At that point I am measuring my own runtime, not your server.&lt;/p&gt;
&lt;p&gt;Rust has no garbage collector. Memory is freed at a point I can see in the code. There is no background pause I did not write. For a tool that sells timing, that control is worth the extra work Rust asks for.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looks-like-in-memory&quot;&gt;What that looks like in memory&lt;/h3&gt;
&lt;p&gt;Here are some real numbers, with a warning attached. They come from a load test on a developer laptop, not a production server. I use them to catch a slowdown between two versions of my code, not to plan capacity. A real server does better, a small box does worse. Treat them as a floor.&lt;/p&gt;
&lt;p&gt;In one run, a single machine held 50,000 checks in flight and peaked at 933 MiB of memory. That is under one gigabyte for fifty thousand live checks. My running server uses about 42 MiB while watching its monitors, and it sits quiet when there is nothing to do. That kind of density is normal for a service with no garbage collector, and it means one small box covers a lot of monitors. I go deeper on the prober and the throughput numbers in &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/building-an-uptime-monitor-in-rust&quot;&gt;the build story&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-bug-that-never-compiles&quot;&gt;The bug that never compiles&lt;/h3&gt;
&lt;p&gt;Speed is only half of it. The other half is a class of bug that Rust refuses to build.&lt;/p&gt;
&lt;p&gt;Picture many workers writing to one shared map of results at the same time. In Go this compiles and runs. Sometimes it is fine. Sometimes two goroutines write at once, you get corrupt data or a crash, and it only happens under load, which is the worst time to find out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// compiles fine, races at runtime
results := map[string]int{}
for _, check := range checks {
    go func(c Check) {
        results[c.ID] = c.Latency // data race
    }(check)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go gives you good tools for this. There is a race detector, a sync.Mutex, and channels. But remembering to reach for them is on you.&lt;/p&gt;
&lt;p&gt;In Rust the same concurrent write does not compile. The compiler stops you until the shared map is wrapped in a lock:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// will not compile unless the shared map is a Mutex
let results = Mutex::new(HashMap::new());
std::thread::scope(|s| {
    for check in checks {
        let results = &amp;amp;results;
        s.spawn(move || {
            // each worker locks only for its own insert
            results.lock().unwrap().insert(check.id, check.latency);
        });
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each worker holds the lock only for its own insert, so the writes stay safe without blocking the others for long. For code that runs day and night across many machines, &quot;the compiler will not let you ship the race&quot; removes a whole set of late-night bugs before they exist.&lt;/p&gt;
&lt;h3 id=&quot;where-go-is-the-better-pick&quot;&gt;Where Go is the better pick&lt;/h3&gt;
&lt;p&gt;None of this makes Go a bad choice. Often it is the better one. Go is faster to learn. It builds in seconds. Its standard library for network services is excellent, and a new engineer can be useful in days. If I were building a normal web service, or something I had to ship this week, Go would be on the table and might win.&lt;/p&gt;
&lt;p&gt;Rust asks more from you first. The compiler argues with you. The build is slower. You spend time on things Go would just handle. I take that trade because this job is narrow and it rewards tight control over memory and timing.&lt;/p&gt;
&lt;h3 id=&quot;the-compiler-as-a-safety-net&quot;&gt;The compiler as a safety net&lt;/h3&gt;
&lt;p&gt;Heavy code review is how large teams catch data races and memory bugs. Rust gives you a lot of that for free. The compiler turns those mistakes into build errors, so they never reach production and never wake anyone up. Every change gets a strong, automatic check before it ships, which is a big part of why I trust the service to run unattended.&lt;/p&gt;
&lt;p&gt;It is also why Uptimepage is open source and self-hostable. You can read the code, run it on your own hardware, and export your data whenever you want, so you are never locked into a single vendor. You can start from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-honest-version&quot;&gt;The honest version&lt;/h3&gt;
&lt;p&gt;The honest version is not &quot;Rust beats Go.&quot; It is &quot;for a tool that lives or dies by timing and runs at high concurrency, Rust fit better.&quot; Pick the language for the job in front of you. Mine happened to be a job that Rust is very good at.&lt;/p&gt;
&lt;p&gt;If you have shipped a heavily concurrent network service, did you reach for Go or Rust, and did the GC ever actually show up in your tail latencies? Curious where people land when the timing is the product.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23075/why-i-chose-rust-over-go-for-an-uptime-monitor</guid>
<pubDate>Mon, 20 Jul 2026 14:44:36 +0000</pubDate>
</item>
<item>
<title>Lab green, field amber: reconciling two PageSpeed truths</title>
<link>https://coderlegion.com/23007/lab-green-field-amber-reconciling-two-pagespeed-truths</link>
<description>&lt;p&gt;Lighthouse can look healthy while CrUX still needs improvement. Here is how we decide which signal to trust when the two disagree.&lt;/p&gt;
&lt;p&gt;The Lighthouse performance score on the ticket was 92. Mobile lab LCP sat under 2.5 seconds. Someone pasted the PageSpeed Insights link into Slack with a green check, and the thread closed before lunch.&lt;/p&gt;
&lt;p&gt;Two days later Search Console still showed the URL group as Needs improvement on mobile. Chrome UX Report percentiles had not moved. The client's campaign page felt fine in a warm office on fibre; field users on mid-range phones were still waiting on the hero. Both numbers were honest, and both were answering different questions. That split is the ordinary case of lab green and field amber. Teams that treat it as a tooling bug waste a sprint arguing about which dashboard is right. Teams that treat lab and field as separate jobs prioritise more clearly and close fewer tickets on a Lighthouse performance score alone.&lt;/p&gt;
&lt;h3 id=&quot;why-lighthouse-lab-scores-and-crux-field-data-disagree-on-the-same-url&quot;&gt;Why Lighthouse lab scores and CrUX field data disagree on the same URL&lt;/h3&gt;
&lt;p&gt;Lab data from Lighthouse (the engine behind PageSpeed Insights lab results) is a controlled run: fixed device profile, throttled network, repeatable conditions. It is strong for catching regressions you introduced in a deploy, for comparing template A against template B, and for reproducing a slow interaction in DevTools. Field data from the Chrome UX Report is a distribution of real visits: real devices, real networks, real scroll depth, and real interaction paths. Google's &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/lab-and-field-data-differences&quot;&gt;lab versus field guidance on web.dev&lt;/a&gt; is blunt about the gap: Lighthouse returns one LCP candidate for a given run, while field LCP on the same URL often spans several elements depending on viewport and how far the user scrolled.&lt;/p&gt;
&lt;p&gt;Amber field percentiles with a green lab score usually mean the controlled run never hit the conditions that dominate your traffic. A consent banner that only appears for a large share of European visitors, a third-party tag that fires after a marketing campaign starts, or a product gallery that mid-range Androids struggle with can all stay invisible in a single lab profile. The reverse pattern shows up too: lab red because a synthetic throttle is harsh, while CrUX already looks healthy for the visitors who matter. Neither case is a contradiction; it is a mismatch of populations, and treating either number as the only truth usually sends engineering at the wrong work.&lt;/p&gt;
&lt;p&gt;Device form factor makes the same disagreement louder. PageSpeed Insights and CrUX already split mobile and desktop; a lab desktop run that looks green does not clear a mobile field fail on the same URL. We covered paired device reporting in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/two-device-stories-one-url-why-averages-lie-mqzm7kk9&quot;&gt;two device stories, one URL&lt;/a&gt;. Lab versus field sits next to that problem: even on one form factor, a green lab row and an amber CrUX row can coexist for weeks after someone declared the page fixed.&lt;/p&gt;
&lt;h3 id=&quot;which-pagespeed-signal-to-trust-when-lab-and-field-disagree&quot;&gt;Which PageSpeed signal to trust when lab and field disagree&lt;/h3&gt;
&lt;p&gt;We use a short decision order in triage, not a debate about which score looks better in a screenshot.&lt;/p&gt;
&lt;h4 id=&quot;when-crux-or-search-console-has-usable-field-data&quot;&gt;When CrUX or Search Console has usable field data&lt;/h4&gt;
&lt;p&gt;Search and experience claims should follow CrUX percentiles and Search Console URL-group status, not a single Lighthouse performance score. Lab still tells you what to change next. It does not redefine whether real users are already fine.&lt;/p&gt;
&lt;h4 id=&quot;when-crux-field-samples-are-missing-or-thin&quot;&gt;When CrUX field samples are missing or thin&lt;/h4&gt;
&lt;p&gt;New templates, low-traffic locales, and production paths that behave like staging often lack URL-level CrUX samples. PageSpeed Insights may fall back to origin-level field data, which can flatter or hide a single money URL. In those cases we schedule lab runs, store them, and refuse to claim Core Web Vitals are good from a performance score alone. Label those rows lab-only so nobody mistakes them for field proof.&lt;/p&gt;
&lt;h4 id=&quot;when-lighthouse-and-crux-move-in-different-directions&quot;&gt;When Lighthouse and CrUX move in different directions&lt;/h4&gt;
&lt;p&gt;A deploy that adds a script often moves lab first. A traffic mix shift or a third-party change that only hits a segment of visitors often moves field without a matching lab spike on your default profile. When both move, we treat it as confirmation. When only one moves, we write that into the ticket so nobody fixes the green side while ignoring the amber side.&lt;/p&gt;
&lt;h4 id=&quot;keep-mobile-and-desktop-core-web-vitals-as-separate-rows&quot;&gt;Keep mobile and desktop Core Web Vitals as separate rows&lt;/h4&gt;
&lt;p&gt;Averaging them into one pass/fail hides the weaker device behind a single result. A lab desktop pass does not clear a mobile CrUX fail on the same URL, and the reverse is also true. For schedules and budgets that keep both form factors honest, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-lab-green-field-amber&quot;&gt;mobile versus desktop Core Web Vitals monitoring on the Watcher blog&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;how-to-run-a-weekly-lab-versus-field-pagespeed-reconciliation&quot;&gt;How to run a weekly lab-versus-field PageSpeed reconciliation&lt;/h3&gt;
&lt;p&gt;On Monday we pull the priority URL list: homepage, primary landing templates, and conversion paths, each with mobile and desktop. For each row we record lab LCP, INP, and CLS from the latest scheduled run, plus CrUX or Search Console status when samples exist. A one-line note captures whether lab and field agree, disagree, or whether field is origin-fallback only.&lt;/p&gt;
&lt;p&gt;Disagreements go into three buckets. Lab green / field amber means investigate real-user conditions the lab profile missed: third parties, consent, device skew, or an LCP element that only appears for a large share of visits. Lab amber / field green means keep watching, but do not block a release solely on a harsh lab profile unless the URL is new and has no field history yet. Both amber means pick the metric that fails in field first, then use lab to reproduce and verify the fix.&lt;/p&gt;
&lt;p&gt;We do not paste a single PageSpeed Insights screenshot into the client channel as this week's health. Screenshots without stored history become the Tuesday-green, Thursday-panic pattern: a green lab run with nothing saved between checks. Scheduled runs, named baselines, and a short reconciliation note scale better across a roster than ad hoc one-off checks. For when manual PageSpeed Insights is enough and when it is not, the longer comparison stays on the Watcher blog: &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-lab-green-field-amber&quot;&gt;PageSpeed Insights versus automated monitoring&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;how-to-explain-lab-green-and-field-amber-to-clients&quot;&gt;How to explain lab green and field amber to clients&lt;/h3&gt;
&lt;p&gt;Clients hear green score and assume Search is happy. When field data lags or disagrees, the account manager needs one sentence that names both signals. A wording that works in our experience: &quot;Lab tests on our standard mobile profile look healthy after the deploy; real-user Core Web Vitals for this URL group are still in Needs improvement, so we are treating field as the priority and using lab to verify each fix.&quot; That sentence does not apologise for the tools. It assigns jobs: field for user and Search status, lab for engineering feedback loops.&lt;/p&gt;
&lt;p&gt;If only origin-level field data is available, say so: &quot;We do not yet have URL-level field samples for this landing page; the lab series is our early-warning signal until CrUX fills in.&quot; Honesty about sample coverage beats a blended performance score that implies certainty you do not have. The client can then fund the field problem without thinking you contradicted last week's lab screenshot, and engineering keeps a clear label on what is lab-only until URL-level CrUX appears.&lt;/p&gt;
&lt;h3 id=&quot;add-a-lab-versus-field-check-to-this-week-s-monitoring-list&quot;&gt;Add a lab-versus-field check to this week's monitoring list&lt;/h3&gt;
&lt;p&gt;Pick five money URLs and open the latest PageSpeed Insights (or monitoring) results with mobile selected. For each URL, write whether lab and field agree, disagree, or whether field is missing or origin-only. Add the same pass for desktop on the two highest-revenue paths. Change one alert or report so a lab-only improvement cannot close a ticket while CrUX on that URL group remains amber. Store the next week's runs against the same URL list so the reconciliation is a habit, not a special project after a client complaint.&lt;/p&gt;
&lt;h3 id=&quot;reconcile-lab-and-field-before-you-close-the-ticket&quot;&gt;Reconcile lab and field before you close the ticket&lt;/h3&gt;
&lt;p&gt;Treat green lab and amber field as two measurements with different jobs, not a vote to ignore one dashboard. Use the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-lab-green-field-amber&quot;&gt;PageSpeed Insights versus automated monitoring guide&lt;/a&gt; when you need the workflow split between spot checks and scheduled history, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-lab-green-field-amber&quot;&gt;mobile versus desktop monitoring guide&lt;/a&gt; when the disagreement is also a form-factor split. Reconcile both signals before you treat a Lighthouse performance score as closed.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/lab-green-field-amber-reconciling-two-pagespeed-truths&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23007/lab-green-field-amber-reconciling-two-pagespeed-truths</guid>
<pubDate>Sun, 19 Jul 2026 08:30:53 +0000</pubDate>
</item>
<item>
<title>The hidden costs of AI-built internal tools (next year's bill is not zero)</title>
<link>https://coderlegion.com/22962/the-hidden-costs-of-ai-built-internal-tools-next-years-bill-is-not-zero</link>
<description>&lt;p&gt;Infrastructure, maintenance, security updates, and the client work that did not start while the team shipped a 'free' internal stack.&lt;/p&gt;
&lt;p&gt;Someone on a developer thread put it better than most vendor decks do: no software works without infrastructure, maintenance, and security updates. Avoiding an expensive third-party tool can be rational. Pretending next year's bill will be zero is not. And while the internal team spent a month &quot;saving&quot; the subscription, which client projects were delayed or never started?&lt;/p&gt;
&lt;p&gt;That is the question we hear when agencies and product teams use AI coding tools to stand up an internal dashboard, a scraper farm, or a home-grown PageSpeed loop. The first demo looks cheap. The second year does not.&lt;/p&gt;
&lt;p&gt;What follows is how we price that trade-off when the alternative is buying a monitoring product, including our own: Apogee Watcher. We are biased toward layering a specialised tool onto the stack you already run. We are not biased toward pretending AI removes run cost.&lt;/p&gt;
&lt;h3 id=&quot;why-ai-built-internal-tools-look-free-in-month-one&quot;&gt;Why AI-built internal tools look free in month one&lt;/h3&gt;
&lt;p&gt;AI coding assistants compress the time from idea to working prototype. A senior engineer can scaffold a multi-site Lighthouse runner, a Slack notifier, and a rough report page in days instead of weeks. The invoice for that month may show only tokens, coffee, and a weekend.&lt;/p&gt;
&lt;p&gt;The demo hides four lines that do not appear on the prototype slide:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hosting, queues, storage, and secrets for something that must run every day.&lt;/li&gt;
&lt;li&gt;Dependency and runtime upgrades when Node, Chromium, or the Lighthouse API moves.&lt;/li&gt;
&lt;li&gt;Access control, audit logs, and incident response when client URLs and credentials sit in your system.&lt;/li&gt;
&lt;li&gt;The billable work that did not ship while that engineer owned the internal project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Month one is a capitalisation of attention. Month twelve is operations. Those are different ledgers, and confusing them is how &quot;free&quot; tools become expensive quietly.&lt;/p&gt;
&lt;h3 id=&quot;what-the-hidden-costs-of-ai-internal-tools-include-next-year&quot;&gt;What the hidden costs of AI internal tools include next year&lt;/h3&gt;
&lt;p&gt;We are not arguing against building. We are arguing against incomplete cost models, especially when AI made the first commit feel almost free. The spreadsheet that only lists SaaS seats will always favour the internal prototype.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure.&lt;/strong&gt; Schedulers fail. Disk fills. Rate limits trip. Someone gets paged when the overnight suite stalls. A managed product folds that into a subscription; an internal tool folds it into someone's Friday, which is still a cost even when it never appears as a line item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maintenance.&lt;/strong&gt; Lighthouse versions, PageSpeed Insights quotas, browser images, and OAuth tokens all age. The code that &quot;just works&quot; in March needs an owner in September. AI can rewrite a broken script. It does not attend the capacity meeting when three clients regress on the same weekend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security updates.&lt;/strong&gt; Anything that stores client domains, screenshots, or API keys is a security surface. Patch cadence, least privilege, and offboarding when staff leave are not optional extras. They are the price of keeping the tool on the network without becoming the next incident review.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opportunity cost.&lt;/strong&gt; This is the line the comment nails. If a strong engineer spent four weeks on an internal monitoring stack, those weeks were not spent on a client launch, a performance remediation, or a proposal that might have paid for several years of a SaaS seat. &quot;We avoided a subscription&quot; is incomplete without &quot;we deferred this revenue work.&quot;&lt;/p&gt;
&lt;p&gt;For agencies, that last line is usually the largest. Delivery capacity is the scarce asset. Software licences are not.&lt;/p&gt;
&lt;h3 id=&quot;build-versus-buy-for-portfolio-pagespeed-monitoring&quot;&gt;Build versus buy for portfolio PageSpeed monitoring&lt;/h3&gt;
&lt;p&gt;Performance monitoring is a poor place to learn that lesson the hard way. You need history across many URLs, device splits, budgets, and alerts that arrive when a deploy breaks LCP. Building that once for one client is a weekend project. Building it for forty clients with permissions, white-label reports, and fair API quotas is a product.&lt;/p&gt;
&lt;p&gt;We wrote our build-versus-buy framing for agencies in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-ci-vs-managed-monitoring-build-vs-buy-agencies?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-ai-internal-tools-hidden-costs&quot;&gt;Lighthouse CI vs Managed Monitoring: Build vs Buy for Agencies&lt;/a&gt;. The short version for this discussion: CI gates and managed monitoring solve different jobs. AI makes the CI path faster to start. It does not erase the ownership tax of keeping a multi-tenant monitoring system honest.&lt;/p&gt;
&lt;p&gt;If the honest goal is scaling client coverage without growing the monitoring headcount, the decision is less about whether AI can generate a script and more about whether you want that script to become a permanent internal product. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-ai-internal-tools-hidden-costs&quot;&gt;Scaling Without Headcount&lt;/a&gt; is the Watcher-side case for automating the loops that expand with every new domain. That is the capacity question, not the prototype question.&lt;/p&gt;
&lt;h3 id=&quot;checklist-before-you-green-light-an-ai-built-internal-tool&quot;&gt;Checklist before you green-light an AI-built internal tool&lt;/h3&gt;
&lt;p&gt;Use this before you celebrate the &quot;we built it ourselves&quot; thread:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Question &lt;/th&gt;&lt;th&gt;If the answer is no&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Who owns patches and dependency upgrades in six months? &lt;/td&gt;&lt;td&gt;Budget an owner or do not start&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Where do credentials, screenshots, and client URLs live, and who audits access? &lt;/td&gt;&lt;td&gt;Treat it as a security project, not a hackathon&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;What is the monthly infra and quota cost at full portfolio size? &lt;/td&gt;&lt;td&gt;Re-run the SaaS comparison at that size&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Which client commitments slip if this takes four weeks? &lt;/td&gt;&lt;td&gt;Put that list next to the &quot;saved subscription&quot; slide&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Will this layer onto tools we already trust, or replace them under pressure? &lt;/td&gt;&lt;td&gt;Prefer layer-don't-replace unless you have a retirement plan&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AI changes the cost of the first commit. It does not change the cost of being on call for the result. Treat ownership as part of the build estimate, not a surprise in month four.&lt;/p&gt;
&lt;h3 id=&quot;when-building-an-internal-monitoring-tool-still-makes-sense&quot;&gt;When building an internal monitoring tool still makes sense&lt;/h3&gt;
&lt;p&gt;Build when the workflow is a genuine differentiator, when compliance forces data to stay inside your boundary, or when you are extending a stack you already operate with spare capacity. Build a thin adapter. Buy the commodity loop.&lt;/p&gt;
&lt;p&gt;Do not build because a social post claimed SaaS is a tax and AI is free labour. That framing skips maintenance, security, and the client work that paid the salaries funding the experiment. Those skipped lines are where next year's bill hides.&lt;/p&gt;
&lt;h3 id=&quot;count-the-full-cost-then-choose&quot;&gt;Count the full cost, then choose&lt;/h3&gt;
&lt;p&gt;Next year's bill will not be zero. Count infrastructure, maintenance, security updates, and the projects that waited. If the total still beats a specialised product after you price the opportunity cost, build with eyes open. If it does not, buy the monitoring layer and keep your engineers on client outcomes.&lt;/p&gt;
&lt;p&gt;We built Apogee Watcher for the second case: multi-site PageSpeed schedules, budgets, and alerts without asking your team to become a second product company. Layer it on. Keep the AI assistants for the work that is uniquely yours.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/hidden-costs-ai-built-internal-tools-next-year-bill-not-zero&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22962/the-hidden-costs-of-ai-built-internal-tools-next-years-bill-is-not-zero</guid>
<pubDate>Sat, 18 Jul 2026 13:38:14 +0000</pubDate>
</item>
<item>
<title>98% uptime sounds high. It means your service is down 7.3 days a year</title>
<link>https://coderlegion.com/22889/98-uptime-sounds-high-it-means-your-service-is-down-7-3-days-a-year</link>
<description>&lt;p&gt;&lt;em&gt;Cover photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/@theblowup&quot;&gt;the blowup&lt;/a&gt; on Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I build an uptime monitor, so this question lands in my inbox a lot: is 98% uptime good?&lt;/p&gt;
&lt;p&gt;For a public website or a paid API, no. For an internal tool or a side project, it is fine. The difference is one division away.&lt;/p&gt;
&lt;p&gt;98% looks like a top grade because school taught us that 98 out of 100 is excellent. Uptime does not grade like school. The whole scale for public services lives between 99% and 100%, and serious targets differ only in the digits after the decimal. On that scale, 98% sits at the bottom.&lt;/p&gt;
&lt;h3 id=&quot;turn-the-percentage-into-time&quot;&gt;Turn the percentage into time&lt;/h3&gt;
&lt;p&gt;The allowed failure at 98% is 2%. Two percent of a year is 7.3 days. Two percent of a 30-day month is 14.4 hours. If your shop makes 2,000 USD a day, 98% uptime means you accept about $14,600 of closed-door time per year and the target still counts as met.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-98-uptime-year.webp&quot; alt=&quot;One year at 98% uptime drawn as 52 weeks of day cells: four short red outage runs totalling 7.3 days scattered through a green year.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here is the same math for every common target. The last column prices the downtime for that $2,000-a-day shop.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Uptime &lt;/th&gt;&lt;th&gt;Per day &lt;/th&gt;&lt;th&gt;Per 30-day month &lt;/th&gt;&lt;th&gt;Per year &lt;/th&gt;&lt;th&gt;Lost sales per year&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;90% &lt;/td&gt;&lt;td&gt;2.4 hours &lt;/td&gt;&lt;td&gt;3 days &lt;/td&gt;&lt;td&gt;36.5 days &lt;/td&gt;&lt;td&gt;$73,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;95% &lt;/td&gt;&lt;td&gt;1.2 hours &lt;/td&gt;&lt;td&gt;36 hours &lt;/td&gt;&lt;td&gt;18.3 days &lt;/td&gt;&lt;td&gt;$36,500&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;28.8 minutes &lt;/td&gt;&lt;td&gt;14.4 hours &lt;/td&gt;&lt;td&gt;7.3 days &lt;/td&gt;&lt;td&gt;$14,600&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99% &lt;/td&gt;&lt;td&gt;14.4 minutes &lt;/td&gt;&lt;td&gt;7.2 hours &lt;/td&gt;&lt;td&gt;3.7 days &lt;/td&gt;&lt;td&gt;$7,300&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99.5% &lt;/td&gt;&lt;td&gt;7.2 minutes &lt;/td&gt;&lt;td&gt;3.6 hours &lt;/td&gt;&lt;td&gt;1.8 days &lt;/td&gt;&lt;td&gt;$3,650&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99.9% &lt;/td&gt;&lt;td&gt;1.4 minutes &lt;/td&gt;&lt;td&gt;43 minutes &lt;/td&gt;&lt;td&gt;8.8 hours &lt;/td&gt;&lt;td&gt;$730&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99.95% &lt;/td&gt;&lt;td&gt;43 seconds &lt;/td&gt;&lt;td&gt;21.6 minutes &lt;/td&gt;&lt;td&gt;4.4 hours &lt;/td&gt;&lt;td&gt;$365&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99.99% &lt;/td&gt;&lt;td&gt;8.6 seconds &lt;/td&gt;&lt;td&gt;4.3 minutes &lt;/td&gt;&lt;td&gt;52.6 minutes &lt;/td&gt;&lt;td&gt;$73&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;99.999% &lt;/td&gt;&lt;td&gt;0.9 seconds &lt;/td&gt;&lt;td&gt;26 seconds &lt;/td&gt;&lt;td&gt;5.3 minutes &lt;/td&gt;&lt;td&gt;$7&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two jumps on this table do most of the work in real contracts. From 98% to 99.9%, the allowed downtime drops from 14.4 hours a month to 43 minutes. From 99.9% to 99.99%, it drops from 43 minutes to 4.3 minutes, and that second jump usually costs about ten times more engineering than the first while saving the shop $657 a year.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-the-downtime-matters&quot;&gt;The shape of the downtime matters&lt;/h3&gt;
&lt;p&gt;98% per month is 14.4 hours, but the number says nothing about how those hours land.&lt;/p&gt;
&lt;p&gt;Thirty minutes of planned maintenance every night at 03:00 adds up to 98% and most users never notice. One 14-hour outage on the day of your product launch is also 98%. Same score, very different month.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-98-uptime-shape.webp&quot; alt=&quot;Two 30-day bars that both score 98% uptime: thin red ticks every night versus one 14.4-hour red block on launch day.&quot;&gt;&lt;/p&gt;
&lt;p&gt;So a single uptime percentage is a summary, not the full story. When someone quotes you a number, also ask about the longest single outage and when it happened.&lt;/p&gt;
&lt;h3 id=&quot;when-98-is-enough&quot;&gt;When 98% is enough&lt;/h3&gt;
&lt;p&gt;Plenty of systems can live at 98% and nobody gets hurt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An internal wiki. People retry after lunch.&lt;/li&gt;
&lt;li&gt;A staging environment. Downtime there is often planned.&lt;/li&gt;
&lt;li&gt;A batch job that builds reports at night. It has hours of slack before anyone reads the output.&lt;/li&gt;
&lt;li&gt;A home server on a residential connection. Your power company already decided your uptime for you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shared pattern: when these go down, nobody loses money and nobody loses trust. Paying for more nines there is waste.&lt;/p&gt;
&lt;h3 id=&quot;when-it-is-not&quot;&gt;When it is not&lt;/h3&gt;
&lt;p&gt;A checkout page, a paid API, a login service. Here 98% fails twice. First the direct cost: 14.4 hours a month of failed requests and support tickets. Second the trust cost, which is larger and slower. A customer who hits your outage twice in one week does not check your uptime report. They remember that your service is the one that breaks.&lt;/p&gt;
&lt;p&gt;One more trap: an SLA is not uptime. Uptime is the measured number. An SLA is a contract promise with a penalty, and the penalty is almost always a service credit. If a provider misses its 99.9% SLA, you get part of your bill back. Your customers get nothing.&lt;/p&gt;
&lt;h3 id=&quot;what-to-aim-for-instead&quot;&gt;What to aim for instead&lt;/h3&gt;
&lt;p&gt;99.9% is the default target for customer-facing services for a reason. 43 minutes a month is enough room for a bad deploy and a couple of small failures, and a small team can hit it without heroics. Above that, each nine costs roughly ten times more and most users cannot feel the difference.&lt;/p&gt;
&lt;p&gt;If you want to run your own numbers, I keep a free &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/uptime-sla-calculator&quot;&gt;uptime SLA calculator&lt;/a&gt; and an &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/error-budget-calculator&quot;&gt;error budget calculator&lt;/a&gt; on our site; both work without signup.&lt;/p&gt;
&lt;p&gt;What target do you actually run in production, and did you pick it or inherit it? I am curious how many teams measured before they promised.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22889/98-uptime-sounds-high-it-means-your-service-is-down-7-3-days-a-year</guid>
<pubDate>Fri, 17 Jul 2026 13:24:32 +0000</pubDate>
</item>
<item>
<title>The status page you can't fake: measured uptime, not published</title>
<link>https://coderlegion.com/22760/the-status-page-you-cant-fake-measured-uptime-not-published</link>
<description>&lt;p&gt;A status page is the one dashboard a company publishes about its own service. It is also the one place where the company has a reason to look good. That is a problem. If the page can be edited to look better than reality, it stops being useful. So when you build or choose a status page, ask one thing: can someone hide a real outage on it?&lt;/p&gt;
&lt;p&gt;The short answer: a status page you can trust builds its uptime bar from real checks, not from the incidents a person chose to publish.&lt;/p&gt;
&lt;h3 id=&quot;two-layers-and-only-one-is-yours-to-edit&quot;&gt;Two layers, and only one is yours to edit&lt;/h3&gt;
&lt;p&gt;A good status page has two parts. The first part is the measured data: the green and red timeline, the 90-day bar, the uptime percent. The second part is incidents: the notes a person writes to explain what broke, what they are doing, and when it is fixed. On the page they sit next to each other and look the same. They are not the same, and mixing them is where trust gets lost.&lt;/p&gt;
&lt;p&gt;The measured data answers one question: what did the checks see? The incident notes answer a different one: what does the team want to say about it? The first is a fact. The second is a story. A status page you can trust lets the team write the story, but keeps them away from the facts.&lt;/p&gt;
&lt;p&gt;The story layer also includes the postmortem, the write-up you post after an outage. A postmortem is honesty you add on purpose. You explain what broke and why, because you choose to. The bar works the other way. It shows the failure on its own, whether you write anything or not. You control the story. You do not control the facts.&lt;/p&gt;
&lt;h3 id=&quot;the-trap-a-bar-that-only-shows-what-you-published&quot;&gt;The trap: a bar that only shows what you published&lt;/h3&gt;
&lt;p&gt;The common mistake is to build the uptime bar from incidents. It feels natural. You already open an incident when something breaks, so why not color the timeline from incidents too? Now the bar turns red only where an incident exists and is marked public.&lt;/p&gt;
&lt;p&gt;The problem comes on the day an outage has no public incident. Maybe no one published it. Maybe a setting was wrong. Maybe the monitor was added to the page after the outage, so its incident was saved as private and never checked again. In every case the timeline shows green over a real red day, and it does this quietly. The uptime number goes up. The customer sees 100 percent over a week they remember as broken.&lt;/p&gt;
&lt;p&gt;One version of this trap is easy to build by accident, so it is worth explaining. You decide &quot;is this incident public?&quot; once, at the moment the incident opens, based on whether the monitor was on a public page right then. Then you save that answer and never look again. In the code it looks like a live check. It is really a photo taken one time. Move the monitor onto the page a day later, and its past outages stay hidden, because the photo was taken before the monitor was there. Any yes or no mark that is set once and then trusted forever has this problem.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-is-a-rule-not-a-switch&quot;&gt;The fix is a rule, not a switch&lt;/h3&gt;
&lt;p&gt;The bar should come from the measured data. It needs only one rule to stay calm: wait for confirmation before you count downtime. You do not want one failed check from one place to turn a whole day red, because networks are noisy and one bad checker is not an outage. So you wait for agreement: more than one region failing at the same time, for more than one check in a row. That one rule is enough to keep a short blip from becoming a red day.&lt;/p&gt;
&lt;p&gt;One thing matters here: the same rule feeds both your alerts and your uptime bar. If the rule that wakes your on-call person is the same rule that colors the timeline, the two can never tell different stories. The moment you add a second rule just for the bar, it will drift from the first, and the page will disagree with itself. On-call gets paged, but the public history says everything was fine.&lt;/p&gt;
&lt;p&gt;Publishing stays where it belongs, on the notes. The team decides whether to write an incident, what to say, and when to post the all-clear. They do not decide whether last Tuesday was down. The checks already decided that.&lt;/p&gt;
&lt;h3 id=&quot;test-the-status-page-you-already-have&quot;&gt;Test the status page you already have&lt;/h3&gt;
&lt;p&gt;You can check any status page, including your own, in a few minutes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add a monitor to a page after it has already had an outage. Does the history show the outage, or does it start clean from the day you added the monitor?&lt;/li&gt;
&lt;li&gt;Take a real incident and unpublish it. Does the bar keep the red day, or does the day turn green?&lt;/li&gt;
&lt;li&gt;Make one region fail for one second. Does the whole day go red, or does the bar stay calm?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A page you can trust shows the outage in the first test, keeps the red in the second, and stays calm in the third. A page that fails these is usually not lying on purpose. It just built its timeline on top of what people chose to publish, and that always has holes.&lt;/p&gt;
&lt;h3 id=&quot;where-this-leaves-you&quot;&gt;Where this leaves you&lt;/h3&gt;
&lt;p&gt;This is how I built it into &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;Uptimepage&lt;/a&gt;: the 90-day bar and each status light come from confirmed downtime, measured across regions with a confirmation rule, not from what someone chose to publish. Incidents and postmortems are the layer you write by hand. It is AGPL and open source on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;GitHub&lt;/a&gt;, and there is a &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/building-an-uptime-monitor-in-rust&quot;&gt;longer write-up on the internals&lt;/a&gt; if you want the Rust side.&lt;/p&gt;
&lt;p&gt;You should not be able to fake your uptime. You should not be able to fake it by accident either. The bar is a measurement. Keep it one.&lt;/p&gt;
&lt;p&gt;How does your status page compute its uptime bar, from checks or from published incidents? Curious what people run and whether anyone has been bitten by the frozen-flag version of this.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22760/the-status-page-you-cant-fake-measured-uptime-not-published</guid>
<pubDate>Wed, 15 Jul 2026 13:51:58 +0000</pubDate>
</item>
<item>
<title>More clients, same team: scale agency monitoring without headcount</title>
<link>https://coderlegion.com/22652/more-clients-same-team-scale-agency-monitoring-without-headcount</link>
<description>&lt;p&gt;When the roster grows but hiring does not, decide which PageSpeed loops to automate and which still need a human owner.&lt;/p&gt;
&lt;p&gt;The account director signed two retainers in March and asked in April why performance reporting still felt like a side project. Same delivery team. Same Slack channels. Three more client domains in the portfolio, and suddenly the Friday &quot;quick PageSpeed sweep&quot; was eating a full afternoon.&lt;/p&gt;
&lt;p&gt;Nobody in that room wanted to hire a monitoring intern. They wanted the work they already sold to stop expanding faster than the people doing it. That is the shape of the problem we hear most often when agencies talk about automation: not a dislike of tools, but a roster that outran the headcount plan.&lt;/p&gt;
&lt;p&gt;Below is the split we use in conversations with agency ops leads: what still needs a human, where scheduled systems return real hours, and how to tell the difference before you buy another subscription.&lt;/p&gt;
&lt;h3 id=&quot;why-agency-rosters-outrun-performance-monitoring-capacity&quot;&gt;Why agency rosters outrun performance monitoring capacity&lt;/h3&gt;
&lt;p&gt;Most agencies do not fail at performance because they lack expertise. They fail because monitoring work scales with URLs and deploys, not with billable projects.&lt;/p&gt;
&lt;p&gt;Each new client adds discovery (what should we watch?), cadence (how often?), ownership (who responds at 6pm?), and reporting (what does the client see monthly?). Those steps are small in isolation. Stack ten clients and they compound into a parallel job nobody scoped in the SOW.&lt;/p&gt;
&lt;p&gt;Hiring fixes part of that, but margins in 2026 rarely allow a dedicated performance analyst per five sites. The teams that grow without burning out usually redraw the boundary: humans own judgement, client context, and remediation; machines own repetition, storage, and threshold checks.&lt;/p&gt;
&lt;p&gt;That boundary is not philosophical. It is a weekly calendar problem.&lt;/p&gt;
&lt;h3 id=&quot;which-performance-monitoring-tasks-still-need-a-human&quot;&gt;Which performance monitoring tasks still need a human&lt;/h3&gt;
&lt;p&gt;Automation is a poor substitute for work that requires negotiation or interpretation. Keep people on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scoping which URLs matter when the client has six checkout variants and marketing insists every campaign landing page is &quot;critical.&quot;&lt;/li&gt;
&lt;li&gt;Explaining a regression to a non-technical stakeholder without turning a Slack alert into panic. Context, history, and what you tried last month still belong to account leads.&lt;/li&gt;
&lt;li&gt;Prioritising fixes when three templates breach budgets at once and only one developer has capacity this sprint.&lt;/li&gt;
&lt;li&gt;Deciding whether a one-point INP wobble is noise or the start of a script conflict after a tag-manager change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those tasks use institutional memory. A cron job can tell you CLS crossed 0.15 on &lt;code&gt;/pricing&lt;/code&gt;. It cannot tell you the client is in renewal week and needs a calm summary instead of raw numbers.&lt;/p&gt;
&lt;p&gt;If your team is small, protect human time for those conversations. That is where retainers renew or fray.&lt;/p&gt;
&lt;h3 id=&quot;what-to-automate-first-in-agency-pagespeed-monitoring&quot;&gt;What to automate first in agency PageSpeed monitoring&lt;/h3&gt;
&lt;p&gt;The loops that drain agencies are surprisingly uniform. In our experience, automation earns its keep when it replaces the same manual sequence every week:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Finding URLs worth monitoring after sitemaps change or a client launches a microsite.&lt;/li&gt;
&lt;li&gt;Running PageSpeed tests on a fixed schedule for mobile and desktop without someone opening a browser tab.&lt;/li&gt;
&lt;li&gt;Storing results so &quot;what did we see before the deploy?&quot; is a query, not an archaeology project in Google Drive.&lt;/li&gt;
&lt;li&gt;Sending alerts when agreed thresholds break, with cooldowns so the channel is not spammed by lab noise.&lt;/li&gt;
&lt;li&gt;Generating client-facing summaries from that stored history instead of copying scores into slides by hand.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That list is operational, not glamorous. It is also where we see teams reclaim half a day per week once the boring spine is in place.&lt;/p&gt;
&lt;p&gt;The case for doing it now, including how manual effort grows with site count, is spelled out on our main blog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-agencies-need-automated-performance-monitoring-in-2026?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-agency-automation-scaling&quot;&gt;Why agencies need automated performance monitoring in 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That Watcher piece holds the fuller argument, numbers, and what &quot;automated&quot; means feature by feature.&lt;/p&gt;
&lt;h3 id=&quot;why-automated-monitoring-fails-without-owners-and-thresholds&quot;&gt;Why automated monitoring fails without owners and thresholds&lt;/h3&gt;
&lt;p&gt;Buying a tool without naming owners produces a expensive reminder to run manual checks anyway.&lt;/p&gt;
&lt;p&gt;Common failure modes we see after a rushed rollout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every site uses different thresholds because nobody wrote a default policy, so alerts mean different things to different account managers.&lt;/li&gt;
&lt;li&gt;Discovery is turned on but nobody reviews new URLs, so quotas burn on low-value routes while checkout sits untested.&lt;/li&gt;
&lt;li&gt;Reports go out on schedule with pretty charts and no narrative, so clients treat them as spam.&lt;/li&gt;
&lt;li&gt;Alerts route to a shared inbox with no first responder, so regressions age until someone notices in a quarterly review.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automation amplifies whatever process you already have. If the process is &quot;whoever is free runs Lighthouse,&quot; the tool becomes a faster version of the same chaos.&lt;/p&gt;
&lt;p&gt;Fix the policy first: default budgets, priority URL sets, one named responder per client, and a short internal note on what triggers escalation versus what waits for the weekly scan. Then wire the scheduler.&lt;/p&gt;
&lt;h3 id=&quot;automate-or-keep-human-a-split-for-five-to-fifteen-person-teams&quot;&gt;Automate or keep human: a split for five-to-fifteen person teams&lt;/h3&gt;
&lt;p&gt;You do not need a transformation programme. You need a written line between repeatable and judgement-heavy work.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Work type &lt;/th&gt;&lt;th&gt;Automate &lt;/th&gt;&lt;th&gt;Keep human&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;URL inventory after sitemap changes &lt;/td&gt;&lt;td&gt;Yes, with a weekly human review of new routes &lt;/td&gt;&lt;td&gt;Final call on &quot;priority&quot; URLs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scheduled lab tests on agreed templates &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Ad-hoc deep dives during incidents&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Threshold alerts to Slack or email &lt;/td&gt;&lt;td&gt;Yes, with cooldowns &lt;/td&gt;&lt;td&gt;Triage and client wording&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Monthly PDF or email summaries &lt;/td&gt;&lt;td&gt;Yes, from stored runs &lt;/td&gt;&lt;td&gt;Executive summary paragraph&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Root-cause analysis after a deploy &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Developer + account lead&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Retainer scope when metrics stay red &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Ops or delivery lead&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Start with one client who already trusts you. Document their five priority URLs, set conservative thresholds, route alerts to one person, and run that pattern for two weekly cycles. If the alert noise is tolerable and the history answers &quot;when did this break?&quot;, copy the template to the next client.&lt;/p&gt;
&lt;p&gt;Scaling without adding headcount is less about heroics and more about cloning a boring playbook. The brand-side walkthrough of that idea, including how multi-tenant monitoring fits agency portfolios, lives here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-agency-automation-scaling&quot;&gt;Scaling a digital agency without headcount&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;four-questions-before-the-next-performance-monitoring-retainer&quot;&gt;Four questions before the next performance monitoring retainer&lt;/h3&gt;
&lt;p&gt;Before you sign another monitoring retainer, answer four questions in writing (internal doc is fine):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who owns first response when an alert fires outside business hours?&lt;/li&gt;
&lt;li&gt;Which three to five URLs per client are non-negotiable in week one?&lt;/li&gt;
&lt;li&gt;What default thresholds apply before you tune per site?&lt;/li&gt;
&lt;li&gt;What is explicitly out of scope for the monitoring fee versus remediation?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If those answers exist, automation is a capacity multiplier. If they do not, you are automating confusion.&lt;/p&gt;
&lt;p&gt;Pick the client whose reporting currently hurts the most. Implement the split table for that account only. Measure one month: hours spent on manual runs, number of regressions caught before the client noticed, and whether the account lead still dreads month-end.&lt;/p&gt;
&lt;p&gt;Small proof beats a platform rollout memo every time.&lt;/p&gt;
&lt;h3 id=&quot;audit-recurring-performance-tasks-before-you-add-another-client&quot;&gt;Audit recurring performance tasks before you add another client&lt;/h3&gt;
&lt;p&gt;Block thirty minutes with whoever runs client delivery. List every recurring performance task from the last two weeks. Mark each row automate, keep human, or delete (yes, some rituals should die). For anything marked automate, name the tool or script and the owner who verifies output every Friday.&lt;/p&gt;
&lt;p&gt;You will likely find the same four loops: find URLs, run tests, store numbers, tell someone. Automate those first. Keep your best people for the conversations that follow the alert.&lt;/p&gt;
&lt;p&gt;More clients with the same humans is workable when repetition stops living in people's calendars.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/more-clients-same-team-scale-agency-monitoring-without-headcount&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22652/more-clients-same-team-scale-agency-monitoring-without-headcount</guid>
<pubDate>Tue, 14 Jul 2026 19:31:01 +0000</pubDate>
</item>
<item>
<title>Error budgets, explained: SLOs, burn rate, and when to stop shipping</title>
<link>https://coderlegion.com/22497/error-budgets-explained-slos-burn-rate-and-when-to-stop-shipping</link>
<description>&lt;p&gt;If you have ever seen a team argue about whether they can ship a risky change, an error budget is the tool that ends the argument with a number. Here is how it works, with the formulas and a simple way to turn it into alerts.&lt;/p&gt;
&lt;h3 id=&quot;what-an-error-budget-is&quot;&gt;What an error budget is&lt;/h3&gt;
&lt;p&gt;An uptime target has a second number hidden inside it. Say you promise 99.9% uptime. You are also saying that 0.1% is allowed to fail. Put that 0.1% into real time, and that is your error budget: the downtime you can have before you break the promise.&lt;/p&gt;
&lt;p&gt;This changes how you look at downtime. It stops being a mistake to feel bad about and becomes a budget you can spend: on a risky deploy, on a slow service you depend on, or on a migration. When the budget runs low you slow down. When it is healthy you can move fast.&lt;/p&gt;
&lt;h3 id=&quot;the-formula&quot;&gt;The formula&lt;/h3&gt;
&lt;p&gt;An SLO is your reliability target, for example 99.9%. The gap to 100% is the failure you are allowed. Multiply that gap by the length of the window, and you get real time you can spend.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error budget = window x (1 - SLO)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 99.9% SLO over a 30-day month is 2,592,000 seconds times 0.001. That is 43 minutes and 12 seconds. That is the whole budget for the month, shared across every incident, not a fresh 43 minutes each day.&lt;/p&gt;
&lt;p&gt;So three nines is not &quot;never go down&quot;. It is a 43-minute budget each month. Every extra nine costs about ten times more engineering, for downtime that most users never notice. This is why Google says that 100% is the wrong target for almost every service.&lt;/p&gt;
&lt;h3 id=&quot;burn-rate-is-the-speed&quot;&gt;Burn rate is the speed&lt;/h3&gt;
&lt;p&gt;The total tells you how much you can spend. It does not tell you how fast. Two services can both sit at 99.9% for the month: one loses the budget slowly, the other loses it all in a single bad hour. Burn rate tells them apart.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;burn rate = (1 - measured) / (1 - SLO)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A burn rate of 1x spends the whole window exactly. 2x spends it in half the time. Below 1x, you finish the month with budget left. Above 1x, the rate tells you the deadline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;budget runs out in = window / burn rate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At 2x, a 30-day budget is gone in fifteen days. At 14.4x it is gone in about two days. That same 14.4x spends 2% of the budget in one hour, which is the level most fast alerts are set to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-error-budget-overspent.webp&quot; alt=&quot;Burn-down chart at 99.0% measured against a 99.9% SLO: an amber line hits zero after about a tenth of the month, labelled gone in 3d.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;turning-burn-rate-into-alerts&quot;&gt;Turning burn rate into alerts&lt;/h3&gt;
&lt;p&gt;One threshold is not enough. It either alerts too late or it sends too many false alarms. The common fix uses two windows: a long one to confirm the problem is real, and a short one to clear the alert quickly once you fix it. Both have to be burning for the alert to fire. For a 30-day budget, these are the usual settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast page: 2% of the budget in 1 hour (with a 5-minute short window). This is a 14.4x burn.&lt;/li&gt;
&lt;li&gt;Page: 5% in 6 hours (30-minute short window). This is a 6x burn.&lt;/li&gt;
&lt;li&gt;Slow ticket: 10% in 3 days (6-hour short window). This is a 1x burn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fast page catches a sudden outage. The slow ticket catches a slow problem that would still use up the whole month if nobody looked.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-is-the-point&quot;&gt;The rule is the point&lt;/h3&gt;
&lt;p&gt;The math is the easy part. The value comes from a rule you agree on before anything breaks. The rule is simple. When the budget runs out, risky launches stop, and the team works on reliability until the budget grows back. While the budget is healthy, you ship and you take the risk.&lt;/p&gt;
&lt;p&gt;One more rule keeps it fair. If you never spend your budget, your SLO is too strict, and you are paying for reliability that no user asked for.&lt;/p&gt;
&lt;h3 id=&quot;try-the-numbers&quot;&gt;Try the numbers&lt;/h3&gt;
&lt;p&gt;I turned these formulas into a free, no sign-up &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/error-budget-calculator&quot;&gt;error budget calculator&lt;/a&gt;. You enter an SLO and your measured availability, and it shows budget spent, budget left, burn rate, and a burn-down chart. There is also an &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/tools/uptime-sla-calculator&quot;&gt;uptime SLA calculator&lt;/a&gt; with the full downtime-per-nine table.&lt;/p&gt;
&lt;p&gt;How does your team set its SLOs and alert thresholds? I would like to hear what works in practice.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22497/error-budgets-explained-slos-burn-rate-and-when-to-stop-shipping</guid>
<pubDate>Mon, 13 Jul 2026 14:13:45 +0000</pubDate>
</item>
<item>
<title>System Monitoring for AI Networks: Real-Time Health Checks &amp; Dashboards</title>
<link>https://coderlegion.com/22199/system-monitoring-for-ai-networks-real-time-health-checks-dashboards</link>
<description>&lt;p&gt;&lt;strong&gt;Your multi-agent system is running. You have no idea if it’s healthy. Agents are fighting, quality is degrading, costs are exploding, and you don’t know until users complain. I built Socratic-performance to solve this: real-time monitoring for agent networks with health dashboards, automatic anomaly detection, and predictive alerts. Know your system is broken before your users do.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Problem: Flying Blind with AI Systems&lt;/strong&gt;&lt;br&gt;
Scenario 1: The Silent Degradation&lt;br&gt;
Your recommendation system worked well for 6 months.&lt;/p&gt;
&lt;p&gt;Then performance slowly degraded:&lt;/p&gt;
&lt;p&gt;Month 1: 89% accuracy (fine)&lt;br&gt;
Month 2: 87% accuracy (slight dip)&lt;br&gt;
Month 3: 84% accuracy (acceptable)&lt;br&gt;
Month 4: 79% accuracy (problem)&lt;br&gt;
Month 5: 72% accuracy (critical)&lt;br&gt;
You don’t notice until Month 5 when users complain.&lt;/p&gt;
&lt;p&gt;By then, you’ve lost €500k in revenue.&lt;/p&gt;
&lt;p&gt;Root cause? A data pipeline started failing 2 months ago. But you had no visibility.&lt;/p&gt;
&lt;p&gt;Scenario 2: The Resource Explosion&lt;br&gt;
Your agents are running. Resources are being consumed.&lt;/p&gt;
&lt;p&gt;You have no visibility into:&lt;/p&gt;
&lt;p&gt;Which agent is consuming most CPU?&lt;br&gt;
Why did GPU usage spike at 3 AM?&lt;br&gt;
Which API calls are most expensive?&lt;br&gt;
Is one agent running in a loop, wasting money?&lt;br&gt;
One agent goes haywire and spends €10k/day on API calls before you notice.&lt;/p&gt;
&lt;p&gt;Scenario 3: The Agent Conflict&lt;br&gt;
Your agents are supposed to work together.&lt;/p&gt;
&lt;p&gt;Instead, they’re fighting:&lt;/p&gt;
&lt;p&gt;Agent A makes a decision&lt;br&gt;
Agent B overrides it&lt;br&gt;
Agent C reverts Agent B’s change&lt;br&gt;
System thrashes, accomplishes nothing&lt;br&gt;
You don’t see the conflict. Users see slowness. You blame infrastructure.&lt;/p&gt;
&lt;p&gt;Scenario 4: The Cascading Failure&lt;br&gt;
One agent fails. It was supposed to handle 10% of traffic.&lt;/p&gt;
&lt;p&gt;System routes that traffic to other agents.&lt;/p&gt;
&lt;p&gt;Other agents become overloaded.&lt;/p&gt;
&lt;p&gt;They start failing too.&lt;/p&gt;
&lt;p&gt;Within minutes, the entire system is down.&lt;/p&gt;
&lt;p&gt;But you didn’t see the first failure. By the time you notice, it’s too late.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Traditional Monitoring Fails for AI Systems&lt;/strong&gt;&lt;br&gt;
Traditional Monitoring Tracks Infrastructure&lt;br&gt;
Traditional tools measure:&lt;/p&gt;
&lt;p&gt;CPU usage&lt;br&gt;
Memory usage&lt;br&gt;
Network latency&lt;br&gt;
Disk space&lt;br&gt;
Uptime&lt;br&gt;
These tell you if your server is alive. They don’t tell you if your system is working correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Systems Need Business Monitoring&lt;/strong&gt;&lt;br&gt;
You need to know:&lt;/p&gt;
&lt;p&gt;Is accuracy degrading?&lt;br&gt;
Are agents conflicting?&lt;br&gt;
Is cost per inference increasing?&lt;br&gt;
Are users satisfied?&lt;br&gt;
Is the system still aligned with what we’re optimizing for?&lt;br&gt;
Traditional monitoring can’t answer these questions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Systems Have Emergent Failures&lt;/strong&gt;&lt;br&gt;
Infrastructure problems: One component fails, error is obvious.&lt;/p&gt;
&lt;p&gt;AI problems: System keeps running but produces wrong answers. System is slow not because of infrastructure but because agents are fighting. System costs explode not because of overloaded hardware but because agents are running inefficient loops.&lt;/p&gt;
&lt;p&gt;These failures don’t show up in infrastructure metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Solution: Socratic-Performance&lt;/strong&gt;&lt;br&gt;
Socratic-performance is a comprehensive monitoring system for AI networks.&lt;/p&gt;
&lt;p&gt;It measures what matters:&lt;/p&gt;
&lt;p&gt;Agent health (are they working correctly?)&lt;br&gt;
System health (are all agents working together?)&lt;br&gt;
Business metrics (are we achieving what we’re optimizing for?)&lt;br&gt;
Resource efficiency (are we using resources well?)&lt;br&gt;
Trend detection (is something degrading?)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Metrics: The 5 Pillars&lt;/strong&gt;&lt;br&gt;
Pillar 1: Agent Health&lt;/p&gt;
&lt;p&gt;For each agent:&lt;/p&gt;
&lt;p&gt;Accuracy (decisions are correct)&lt;br&gt;
Latency (decisions are fast)&lt;br&gt;
Reliability (agent doesn’t fail)&lt;br&gt;
Consistency (agent is stable over time)&lt;br&gt;
Resource usage (agent is efficient)&lt;br&gt;
RecommendationAgent Health:&lt;/p&gt;
&lt;p&gt;├─ Accuracy: 89% (target: 90%) ⚠️ slightly low&lt;/p&gt;
&lt;p&gt;├─ Latency: 245ms (target: &amp;lt;300ms) ✓ good&lt;/p&gt;
&lt;p&gt;├─ Reliability: 99.8% (target: 99.9%) ✓ acceptable&lt;/p&gt;
&lt;p&gt;├─ Consistency: 0.94 (how stable?) ✓ stable&lt;/p&gt;
&lt;p&gt;└─ Resource Usage: 45% of budget ✓ efficient&lt;/p&gt;
&lt;p&gt;Pillar 2: System Health&lt;/p&gt;
&lt;p&gt;For the entire system:&lt;/p&gt;
&lt;p&gt;Coherence (agents working together or fighting?)&lt;br&gt;
Throughput (how many decisions per second?)&lt;br&gt;
Quality (end-to-end accuracy)&lt;br&gt;
Cost efficiency (cost per decision)&lt;br&gt;
User satisfaction (are users happy?)&lt;br&gt;
System Health Dashboard:&lt;/p&gt;
&lt;p&gt;├─ Coherence: 0.92 (agents agree) ✓ healthy&lt;/p&gt;
&lt;p&gt;├─ Throughput: 1,250 decisions/sec ✓ good&lt;/p&gt;
&lt;p&gt;├─ Quality: 86% accuracy ✓ acceptable&lt;/p&gt;
&lt;p&gt;├─ Cost: €0.012 per decision (target: €0.010) ⚠️ slight overage&lt;/p&gt;
&lt;p&gt;└─ User Satisfaction: 4.2/5.0 ✓ good&lt;/p&gt;
&lt;p&gt;Pillar 3: Resource Efficiency&lt;/p&gt;
&lt;p&gt;CPU usage per agent&lt;br&gt;
Memory usage per decision&lt;br&gt;
API costs&lt;br&gt;
Total cost per outcome&lt;br&gt;
Budget remaining&lt;br&gt;
Resource Usage:&lt;/p&gt;
&lt;p&gt;├─ CPU: 60% of available (⚠️ approaching limit)&lt;/p&gt;
&lt;p&gt;├─ Memory: 45% of available ✓ good&lt;/p&gt;
&lt;p&gt;├─ API Cost: €18,500 this month (budget: €20k) ✓ on track&lt;/p&gt;
&lt;p&gt;├─ Cost per decision: €0.012 (target: €0.010) ⚠️ monitor&lt;/p&gt;
&lt;p&gt;└─ Efficiency trend: ↘️ degrading (was €0.009 last month)&lt;/p&gt;
&lt;p&gt;Pillar 4: Anomaly Detection&lt;/p&gt;
&lt;p&gt;Automatically detect:&lt;/p&gt;
&lt;p&gt;Accuracy suddenly dropped&lt;br&gt;
Agent started failing&lt;br&gt;
Cost spiked&lt;br&gt;
Conflict detected between agents&lt;br&gt;
Traffic pattern unusual&lt;br&gt;
Anomalies Detected (Last 24h):&lt;/p&gt;
&lt;p&gt;├─ ⚠️ 11:45 AM - Accuracy drop detected&lt;/p&gt;
&lt;p&gt;│  └─ RecommendationAgent accuracy: 89% → 73%&lt;/p&gt;
&lt;p&gt;│     Likely cause: data quality issue detected&lt;/p&gt;
&lt;p&gt;│     Confidence: 92%&lt;/p&gt;
&lt;p&gt;├─ ⚠️ 2:15 PM - Cost spike detected&lt;/p&gt;
&lt;p&gt;│  └─ API calls increased 3.5x normal&lt;/p&gt;
&lt;p&gt;│     Agent: DataEnrichmentAgent&lt;/p&gt;
&lt;p&gt;│     Duration: 15 minutes&lt;/p&gt;
&lt;p&gt;├─ ✓ 4:30 PM - Agent recovered&lt;/p&gt;
&lt;p&gt;└─ ℹ️ 8:00 PM - Unusual traffic pattern&lt;/p&gt;
&lt;p&gt;   └─ Peak traffic 2 hours early&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  All agents handling well
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pillar 5: Predictive Alerts&lt;/p&gt;
&lt;p&gt;Not just “problem is happening now” but “problem is coming”:&lt;/p&gt;
&lt;p&gt;Predictive Alerts:&lt;/p&gt;
&lt;p&gt;├─ ⚠️ MEDIUM PRIORITY - Accuracy will degrade in ~4 hours&lt;/p&gt;
&lt;p&gt;│  └─ Trend analysis shows 0.5% per hour decline&lt;/p&gt;
&lt;p&gt;│     At current rate, hits alert threshold at 3:30 PM&lt;/p&gt;
&lt;p&gt;│     Recommendation: Investigate data pipeline&lt;/p&gt;
&lt;p&gt;│&lt;/p&gt;
&lt;p&gt;├─ ⚠️ LOW PRIORITY - Cost will exceed budget in 8 days&lt;/p&gt;
&lt;p&gt;│  └─ Current burn rate: €2,500/day&lt;/p&gt;
&lt;p&gt;│     Budget remaining: €20,000&lt;/p&gt;
&lt;p&gt;│     At current rate, exhausted by May 22&lt;/p&gt;
&lt;p&gt;│     Recommendation: Optimize expensive queries&lt;/p&gt;
&lt;p&gt;│&lt;/p&gt;
&lt;p&gt;└─ 🔵 INFO - Seasonal traffic increase expected&lt;/p&gt;
&lt;p&gt;   └─ Historical pattern suggests traffic increase this Friday&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Agents may need scaling

  Plan ahead
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Real-World Impact: Examples&lt;/strong&gt;&lt;br&gt;
Case Study 1: Detecting Silent Degradation&lt;br&gt;
Day 1: System accuracy is 89%. Dashboard shows green.&lt;/p&gt;
&lt;p&gt;Day 2: Socratic-performance detects 0.3% decline. Not critical yet, but flagged for investigation.&lt;/p&gt;
&lt;p&gt;Day 3: Decline continues. Now at 87%. Alert escalates to medium priority. Investigation begins.&lt;/p&gt;
&lt;p&gt;Day 4: Root cause found – data pipeline processing outdated information. Fixed.&lt;/p&gt;
&lt;p&gt;Result: Caught degradation on Day 2. Total impact: ~€50k revenue loss.&lt;/p&gt;
&lt;p&gt;Without monitoring: Degradation continues until Day 12. Total impact: €500k+ revenue loss.&lt;/p&gt;
&lt;p&gt;Savings: €450k+ by detecting early.&lt;/p&gt;
&lt;p&gt;Case Study 2: Preventing Resource Explosion&lt;br&gt;
Monday 3 AM: One agent starts behaving oddly, spinning in a loop.&lt;/p&gt;
&lt;p&gt;Monday 3:05 AM: API cost anomaly detected. System alert triggered.&lt;/p&gt;
&lt;p&gt;Monday 3:08 AM: Engineer receives alert. Investigates.&lt;/p&gt;
&lt;p&gt;Monday 3:15 AM: Agent behavior identified and corrected.&lt;/p&gt;
&lt;p&gt;Result: Total API waste: €2,500 (caught within 15 minutes).&lt;/p&gt;
&lt;p&gt;Without monitoring: Agent continues all week. Total damage: €80k+ in wasted API calls.&lt;/p&gt;
&lt;p&gt;Savings: €77,500.&lt;/p&gt;
&lt;p&gt;Case Study 3: Detecting Agent Conflict&lt;br&gt;
Morning: All agents running. System seems fine.&lt;/p&gt;
&lt;p&gt;9:30 AM: Socratic-performance detects high “conflict score”. Agents are making contradictory decisions and reverting each other.&lt;/p&gt;
&lt;p&gt;9:35 AM: Team reviews conflict logs and identifies the issue.&lt;/p&gt;
&lt;p&gt;9:50 AM: Conflict resolved. System coherence restored.&lt;/p&gt;
&lt;p&gt;Result: 20 minutes of suboptimal performance. System throughput drops 15% during incident but fully recovered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Without monitoring: Conflict might go unnoticed. Users report slowness. Team spends hours debugging infrastructure (wrongly) before finding agent conflict.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Architecture: How Monitoring Works&lt;br&gt;
Component 1: Metric Collection&lt;br&gt;
Every agent reports metrics continuously:&lt;/p&gt;
&lt;p&gt;Agent Status Update (every 60 seconds):&lt;/p&gt;
&lt;p&gt;├─ Agent ID: RecommendationAgent_1&lt;/p&gt;
&lt;p&gt;├─ Timestamp: 2026-05-14T14:32:00Z&lt;/p&gt;
&lt;p&gt;├─ Decisions made: 1,250&lt;/p&gt;
&lt;p&gt;├─ Accuracy: 0.89&lt;/p&gt;
&lt;p&gt;├─ Avg latency: 245ms&lt;/p&gt;
&lt;p&gt;├─ Errors: 2&lt;/p&gt;
&lt;p&gt;├─ API calls: 3,500&lt;/p&gt;
&lt;p&gt;├─ API cost: €42&lt;/p&gt;
&lt;p&gt;├─ Resources:&lt;/p&gt;
&lt;p&gt;│  ├─ CPU: 45%&lt;/p&gt;
&lt;p&gt;│  └─ Memory: 340MB&lt;/p&gt;
&lt;p&gt;└─ Dependencies:&lt;/p&gt;
&lt;p&gt;   ├─ DataService: healthy&lt;/p&gt;
&lt;p&gt;   └─ VectorDB: healthy&lt;/p&gt;
&lt;p&gt;Component 2: Aggregation&lt;br&gt;
All agent metrics aggregated into system-wide view:&lt;/p&gt;
&lt;p&gt;System Status (aggregated):&lt;/p&gt;
&lt;p&gt;├─ Total agents: 12&lt;/p&gt;
&lt;p&gt;├─ Healthy agents: 11&lt;/p&gt;
&lt;p&gt;├─ Degraded agents: 1 (DataEnrichmentAgent)&lt;/p&gt;
&lt;p&gt;├─ Failed agents: 0&lt;/p&gt;
&lt;p&gt;├─ System accuracy: 86%&lt;/p&gt;
&lt;p&gt;├─ System throughput: 1,250 decisions/sec&lt;/p&gt;
&lt;p&gt;├─ System cost rate: €18.50/minute&lt;/p&gt;
&lt;p&gt;└─ Health score: 92/100&lt;/p&gt;
&lt;p&gt;Component 3: Anomaly Detection&lt;br&gt;
Real-time detection of unusual patterns:&lt;/p&gt;
&lt;p&gt;Anomaly Detection Algorithm:&lt;/p&gt;
&lt;p&gt;├─ Baseline: Historical pattern of metric values&lt;/p&gt;
&lt;p&gt;├─ Current: Today's metric values&lt;/p&gt;
&lt;p&gt;├─ Deviation: How much different is today?&lt;/p&gt;
&lt;p&gt;├─ Threshold: Alert if deviation &amp;gt; 3 std deviations&lt;/p&gt;
&lt;p&gt;├─ Confidence: How sure are we?&lt;/p&gt;
&lt;p&gt;└─ Action: Alert if confidence &amp;gt; 90%&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;├─ Baseline accuracy: 89% (average over past month)&lt;/p&gt;
&lt;p&gt;├─ Current accuracy: 73%&lt;/p&gt;
&lt;p&gt;├─ Deviation: 16 percentage points&lt;/p&gt;
&lt;p&gt;├─ Std deviation: 2.5 percentage points&lt;/p&gt;
&lt;p&gt;├─ Z-score: 16/2.5 = 6.4 (extremely unusual!)&lt;/p&gt;
&lt;p&gt;├─ Confidence: 99.99%&lt;/p&gt;
&lt;p&gt;└─ Action: CRITICAL ALERT&lt;/p&gt;
&lt;p&gt;Component 4: Root Cause Analysis&lt;br&gt;
When anomaly detected, system investigates:&lt;/p&gt;
&lt;p&gt;Accuracy dropped from 89% to 73%. Root cause analysis:&lt;/p&gt;
&lt;p&gt;Checking agent inputs:&lt;/p&gt;
&lt;p&gt;├─ Data quality: ✓ good&lt;/p&gt;
&lt;p&gt;├─ Data volume: ✓ normal&lt;/p&gt;
&lt;p&gt;├─ Feature values: ✓ in normal range&lt;/p&gt;
&lt;p&gt;└─ → Not a data problem&lt;/p&gt;
&lt;p&gt;Checking agent internals:&lt;/p&gt;
&lt;p&gt;├─ Agent code: ✓ unchanged&lt;/p&gt;
&lt;p&gt;├─ Agent parameters: ✓ stable&lt;/p&gt;
&lt;p&gt;├─ Agent resources: ✓ sufficient&lt;/p&gt;
&lt;p&gt;└─ → Not an agent code problem&lt;/p&gt;
&lt;p&gt;Checking dependencies:&lt;/p&gt;
&lt;p&gt;├─ DataService: ✓ healthy&lt;/p&gt;
&lt;p&gt;├─ VectorDB: ✓ fast&lt;/p&gt;
&lt;p&gt;├─ APIClient: ✓ responsive&lt;/p&gt;
&lt;p&gt;└─ → Not a dependency problem&lt;/p&gt;
&lt;p&gt;Checking output usage:&lt;/p&gt;
&lt;p&gt;├─ Downstream services: ⚠️ changed&lt;/p&gt;
&lt;p&gt;│  └─ RankingService now expects different format&lt;/p&gt;
&lt;p&gt;│     Old format: {&quot;score&quot;: 0.89}&lt;/p&gt;
&lt;p&gt;│     New format: {&quot;score&quot;: 0.89, &quot;confidence&quot;: 0.95}&lt;/p&gt;
&lt;p&gt;│  └─ RankingService confused by missing field&lt;/p&gt;
&lt;p&gt;│     Uses default value (causes low scores)&lt;/p&gt;
&lt;p&gt;└─ → ROOT CAUSE FOUND&lt;/p&gt;
&lt;p&gt;Problem: Downstream dependency changed format&lt;/p&gt;
&lt;p&gt;Solution: Update agent output format&lt;/p&gt;
&lt;p&gt;Action: Auto-fix applied (if possible) or alert engineer&lt;br&gt;
Component 5: Predictive Alerts&lt;br&gt;
Don’t wait for problem to happen:&lt;/p&gt;
&lt;p&gt;Trend Analysis:&lt;/p&gt;
&lt;p&gt;Last 7 days:&lt;/p&gt;
&lt;p&gt;├─ Day 1: Accuracy 89.0%&lt;/p&gt;
&lt;p&gt;├─ Day 2: Accuracy 88.8% (-0.2%)&lt;/p&gt;
&lt;p&gt;├─ Day 3: Accuracy 88.4% (-0.4%)&lt;/p&gt;
&lt;p&gt;├─ Day 4: Accuracy 87.8% (-0.6%)&lt;/p&gt;
&lt;p&gt;├─ Day 5: Accuracy 87.0% (-0.8%)&lt;/p&gt;
&lt;p&gt;├─ Day 6: Accuracy 86.0% (-1.0%)&lt;/p&gt;
&lt;p&gt;└─ Day 7: Accuracy 84.8% (-1.2%)&lt;/p&gt;
&lt;p&gt;Trend: Declining at 0.25% per day (accelerating)&lt;/p&gt;
&lt;p&gt;Projection:&lt;/p&gt;
&lt;p&gt;├─ In 5 days: 84% (warning threshold)&lt;/p&gt;
&lt;p&gt;├─ In 8 days: 82% (alert threshold)&lt;/p&gt;
&lt;p&gt;├─ In 12 days: 79% (critical threshold)&lt;/p&gt;
&lt;p&gt;Prediction confidence: 85%&lt;/p&gt;
&lt;p&gt;Alert timing:&lt;/p&gt;
&lt;p&gt;├─ Medium alert issued (accuracy trending down)&lt;/p&gt;
&lt;p&gt;├─ Recommended investigation: Now (while easy to fix)&lt;/p&gt;
&lt;p&gt;└─ Critical alert will trigger in ~8 days (if trend continues)&lt;br&gt;
Implementation: Setting Up Monitoring&lt;/p&gt;
&lt;p&gt;Step 1: Enable Performance Tracking&lt;br&gt;
from socratic_performance import PerformanceMonitor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;monitor = PerformanceMonitor(

    system_name=&quot;RecommendationEngine&quot;,

    tracking_enabled=True,

    metrics=[

        &quot;accuracy&quot;,

        &quot;latency&quot;,

        &quot;cost&quot;,

        &quot;resource_usage&quot;,

        &quot;coherence&quot;

    ]

)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 2: Agent Reporting&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Agents report metrics automatically

agent = RecommendationAgent(

    monitoring=monitor,

    report_interval=60  # seconds

)



# Every 60 seconds, agent reports:

# - How many decisions made

# - Accuracy achieved

# - Latency

# - Resources used

# - Any errors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 3: Dashboard Setup&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Start monitoring dashboard

dashboard = monitor.create_dashboard(

    port=8000,

    refresh_interval=30  # seconds

)



# Access at: http://localhost:8000

# See real-time metrics, historical trends, anomalies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 4: Alert Configuration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Set alert thresholds

monitor.set_alert_threshold(

    metric=&quot;accuracy&quot;,

    warning_level=0.85,

    critical_level=0.80,

    alert_on_trend=True

)



monitor.set_alert_threshold(

    metric=&quot;cost_per_decision&quot;,

    warning_level=0.015,

    critical_level=0.020

)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 5: Integration with Team&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send alerts to team

monitor.connect_slack(

    webhook=&quot;https://hooks.slack.com/services/...&quot;

)



monitor.connect_email(

    recipients=[&quot;*Emails are not allowed*&quot;]

)



# PagerDuty for critical alerts

monitor.connect_pagerduty(

    api_token=&quot;...&quot;

)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dashboard Features&lt;br&gt;
Real-Time Status&lt;br&gt;
Live view of all agents:&lt;/p&gt;
&lt;p&gt;Agent Status (Live):&lt;/p&gt;
&lt;p&gt;┌─────────────────────────────────────┐&lt;/p&gt;
&lt;p&gt;│ RecommendationAgent      ✓ HEALTHY                                    │&lt;/p&gt;
&lt;p&gt;│ Accuracy: 89% | Latency: 245ms                                                │&lt;/p&gt;
&lt;p&gt;│ Cost: €12.50/min | CPU: 45%                                                      │&lt;/p&gt;
&lt;p&gt;├─────────────────────────────────────┤&lt;/p&gt;
&lt;p&gt;│ FilterAgent              ✓ HEALTHY                                                   │&lt;/p&gt;
&lt;p&gt;│ Accuracy: 95% | Latency: 120ms                                                 │&lt;/p&gt;
&lt;p&gt;│ Cost: €2.50/min | CPU: 20%                                                        │&lt;/p&gt;
&lt;p&gt;├─────────────────────────────────────┤&lt;/p&gt;
&lt;p&gt;│ RankerAgent              ⚠ DEGRADED                                            │&lt;/p&gt;
&lt;p&gt;│ Accuracy: 82% | Latency: 890ms                                                 │&lt;/p&gt;
&lt;p&gt;│ Cost: €5.00/min | CPU: 85%                                                         │&lt;/p&gt;
&lt;p&gt;├─────────────────────────────────────┤&lt;/p&gt;
&lt;p&gt;│ PersonalizerAgent        ✓ HEALTHY                                            │&lt;/p&gt;
&lt;p&gt;│ Accuracy: 87% | Latency: 150ms                                                 │&lt;/p&gt;
&lt;p&gt;│ Cost: €3.50/min | CPU: 35%                                                        │&lt;/p&gt;
&lt;p&gt;└─────────────────────────────────────┘&lt;br&gt;
Historical Trends&lt;br&gt;
View metrics over time:&lt;/p&gt;
&lt;p&gt;Accuracy Trend (Last 30 days):&lt;/p&gt;
&lt;p&gt;90% ┤     ╱╲&lt;/p&gt;
&lt;p&gt;89% ┤   ╱  ╲    ╱╲&lt;/p&gt;
&lt;p&gt;88% ┤  ╱    ╲  ╱  ╲&lt;/p&gt;
&lt;p&gt;87% ┤╱      ╲╱    ╲&lt;/p&gt;
&lt;p&gt;86% ┤              ╲    ╱&lt;/p&gt;
&lt;p&gt;85% ┤               ╲  ╱&lt;/p&gt;
&lt;p&gt;84% ┤                ╲╱&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;└──────────────────────────

 May 1         May 15      May 31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Current: 89%&lt;/p&gt;
&lt;p&gt;Average: 87.2%&lt;/p&gt;
&lt;p&gt;Trend: ↗️ Improving&lt;/p&gt;
&lt;p&gt;Projection: Will reach 91% in 2 weeks&lt;br&gt;
Anomaly Timeline&lt;br&gt;
When problems occurred:&lt;/p&gt;
&lt;p&gt;Anomaly History (Last 7 days):&lt;/p&gt;
&lt;p&gt;May 8  10:45 AM  ⚠️  Accuracy spike (negative)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   11:20 AM  ✓  Resolved

   Duration: 35 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;May 10  3:15 AM  ⚠️  API cost spike&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    3:45 AM  ✓  Resolved

    Duration: 30 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;May 12  2:00 PM  🔵 Unusual traffic pattern&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    3:00 PM  ✓  System handled well

    No resolution needed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;May 14  2:32 PM  ⚠️ Agent conflict detected&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    2:40 PM  ✓ In progress

    Currently resolving...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cost Analysis&lt;br&gt;
Where is money going?&lt;/p&gt;
&lt;p&gt;Daily Cost Breakdown:&lt;/p&gt;
&lt;p&gt;API Calls:      €50.00/day (40%)&lt;/p&gt;
&lt;p&gt;├─ DataService: €20.00&lt;/p&gt;
&lt;p&gt;├─ VectorDB:    €18.00&lt;/p&gt;
&lt;p&gt;└─ LLM:         €12.00&lt;/p&gt;
&lt;p&gt;Compute:        €60.00/day (48%)&lt;/p&gt;
&lt;p&gt;├─ CPU:         €35.00&lt;/p&gt;
&lt;p&gt;├─ GPU:         €20.00&lt;/p&gt;
&lt;p&gt;└─ Memory:      €5.00&lt;/p&gt;
&lt;p&gt;Storage:        €14.00/day (12%)&lt;/p&gt;
&lt;p&gt;├─ Database:    €8.00&lt;/p&gt;
&lt;p&gt;└─ Cache:       €6.00&lt;/p&gt;
&lt;p&gt;Total:          €124/day (€3,720/month)&lt;/p&gt;
&lt;p&gt;Budget:         €5,000/month&lt;/p&gt;
&lt;p&gt;Status:         ✓ On track&lt;br&gt;
The Philosophy: Transparency Enables Action&lt;br&gt;
You can’t manage what you can’t see.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Traditional monitoring made infrastructure visible. But AI systems need a different kind of visibility.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You need to see:&lt;/p&gt;
&lt;p&gt;Are my agents working correctly?&lt;br&gt;
Are they working together?&lt;br&gt;
Is the system still aligned with what I want?&lt;br&gt;
What’s degrading and why?&lt;br&gt;
Socratic-performance provides this transparency.&lt;/p&gt;
&lt;p&gt;With transparency, you can act. Fix problems before they become disasters. Optimize before costs spiral. Scale before bottlenecks form.&lt;/p&gt;
&lt;p&gt;Use Socratic-Performance for System Monitoring&lt;br&gt;
The real-time monitoring system described in this post is production-ready and open source:&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nireus79/Socrates&quot;&gt;https://github.com/Nireus79/Socrates&lt;/a&gt;&lt;br&gt;
PyPI Package: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/socratic-performance/&quot;&gt;https://pypi.org/project/socratic-performance/&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nireus79/Socrates/tree/main/socratic-performance&quot;&gt;https://github.com/Nireus79/Socrates/tree/main/socratic-performance&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Quick Start&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Install the monitoring system

pip install socratic-performance



from socratic_performance import PerformanceMonitor



monitor = PerformanceMonitor(

    system_name=&quot;MySystem&quot;,

    tracking_enabled=True

)



# Connect your agents

agent = MyAgent(monitoring=monitor)



# View dashboard

dashboard = monitor.create_dashboard(port=8000)

# Access at: http://localhost:8000



# Get current metrics

metrics = monitor.get_metrics()

print(f&quot;System accuracy: {metrics.accuracy:.1%}&quot;)

print(f&quot;Cost rate: €{metrics.cost_per_minute:.2f}/min&quot;)



# Get anomalies

anomalies = monitor.get_anomalies()

for anomaly in anomalies:

    print(f&quot;⚠️ {anomaly.description}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full examples and documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Nireus79/Socrates&quot;&gt;https://github.com/Nireus79/Socrates&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Complete Socratic Ecosystem&lt;br&gt;
Socratic-performance is part of the complete Socrates AI system (11 modules, 2,300+ tests):&lt;/p&gt;
&lt;p&gt;Socratic-nexus: Multi-provider LLM client&lt;br&gt;
Socratic-morality: Constitutional governance with 13 modules&lt;br&gt;
Socratic-agents: Multi-agent orchestration with conflict resolution&lt;br&gt;
Socratic-knowledge: Enterprise RAG with multi-tenancy&lt;br&gt;
Socratic-learning: Self-improving agents&lt;br&gt;
Socratic-analyzer: Code quality analysis&lt;br&gt;
Socratic-performance: Real-time monitoring(this post)&lt;br&gt;
Socratic-workflow: Workflow orchestration&lt;br&gt;
Socratic-conflict: Conflict resolution between agents&lt;br&gt;
Socratic-docs: Auto-documentation&lt;br&gt;
Socratic-maturity: Project maturity tracking&lt;br&gt;
All modules work together seamlessly. Use individual packages or the complete platform.&lt;/p&gt;
&lt;p&gt;Available on PyPI under MIT License: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/user/Nireus79/&quot;&gt;https://pypi.org/user/Nireus79/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22199/system-monitoring-for-ai-networks-real-time-health-checks-dashboards</guid>
<pubDate>Fri, 10 Jul 2026 05:35:56 +0000</pubDate>
</item>
<item>
<title>4 things writing an HTTP prober in Rust taught me</title>
<link>https://coderlegion.com/22048/4-things-writing-an-http-prober-in-rust-taught-me</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; An uptime prober is a strange HTTP client, so I swapped reqwest for raw hyper. It needs the opposite of what a normal client gives you: no connection pool, so the cold path gets measured instead of hidden; every phase timed, even when connect fails; a hard SSRF block on the URL the user typed; and exact failure reasons instead of one flat error. Four tricks below, each worth copying even if you never build a monitor.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I built an uptime checker in Rust. Like most people, I used &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/seanmonstar/reqwest&quot;&gt;reqwest&lt;/a&gt; first. Then I dropped it and moved down to raw &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hyperium/hyper&quot;&gt;hyper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Not because reqwest is slow. It is great, and for app code you should use it. But a prober is a strange kind of HTTP client. It makes one request, and its whole job is to measure that request and report exactly what happened. A normal client is built to do the opposite: make requests fast and hide the messy details. Every trick below is a messy detail I needed to keep.&lt;/p&gt;
&lt;p&gt;Here are four things building it taught me. Each one is a habit worth copying, even if you never write a monitor.&lt;/p&gt;
&lt;h3 id=&quot;1-a-connection-pool-lies-to-a-monitor&quot;&gt;1. A connection pool lies to a monitor&lt;/h3&gt;
&lt;p&gt;The first thing reqwest gives you is a connection pool, and it is the first thing I had to remove.&lt;/p&gt;
&lt;p&gt;A pool keeps TCP connections open and reuses them. The second request to a host skips DNS, skips the TCP handshake, skips TLS, and comes back much faster. For app code that is a free speed boost. For a monitor it is a lie. If I reuse a warm connection, the &quot;connect time&quot; I report is near zero, because I did not connect, I borrowed. A user watching a slow TLS handshake slowly rise in one region would see nothing, because the pool hid the handshake.&lt;/p&gt;
&lt;p&gt;So the connector makes exactly one request per connection, then drops it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;/// No pooling: the caller drives exactly one request over the returned
/// stream, then drops it.
pub(crate) async fn timed_connect(
    p: &amp;amp;ConnectParams&amp;lt;'_&amp;gt;,
    host: &amp;amp;str,
    port: u16,
    is_https: bool,
) -&amp;gt; Result&amp;lt;TimedConnection, ConnectError&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every check pays the full cost of a fresh connection, because the full cost is the thing I am trying to measure. The lesson is general: if you are timing a request, a warm pool is measuring the wrong thing. Reuse is a feature that erases exactly the numbers a monitor exists to show.&lt;/p&gt;
&lt;h3 id=&quot;2-put-the-timings-inside-the-error&quot;&gt;2. Put the timings inside the error&lt;/h3&gt;
&lt;p&gt;Splitting a request into DNS, TCP, TLS, and time to first byte is easy on the happy path. The interesting question is what happens when connect fails.&lt;/p&gt;
&lt;p&gt;The naive design loses everything on failure. You get an &lt;code&gt;Err&lt;/code&gt; and no numbers, so a hung TLS handshake looks the same as a dead DNS server. Both are just &quot;failed.&quot; That is the worst moment to have no data.&lt;/p&gt;
&lt;p&gt;The fix is small, and I now use it everywhere: make the error carry the phases that finished before the one that broke.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub(crate) enum ConnectError {
    Dns(anyhow::Error),
    NoAddrs { dns_ms: u16 },
    Connect { err: io::Error, dns_ms: u16 },
    Tls { err: io::Error, dns_ms: u16, connect_ms: u16 },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A TLS failure still reports how long DNS and TCP took. So when a cert handshake hangs, the chart still shows DNS was 12ms, connect was 45ms, and then TLS took the rest. The shape of the error tells you what broke. You are never left guessing which layer stalled, even on the path where everything went wrong. reqwest gives you one flat &lt;code&gt;Error&lt;/code&gt; at the very end. Here the error type holds the answer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;one probe, one fresh connection, each phase on its own clock:

  DNS         TCP connect      TLS handshake     request + TTFB
  +-----------+----------------+-----------------+----------------&amp;gt;
   12ms        45ms             (hung)            never reached

  TLS failed. The error still carries dns_ms=12, connect_ms=45,
  so you know exactly which layer stalled, not just &quot;it failed&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-the-ssrf-check-goes-after-dns-not-before&quot;&gt;3. The SSRF check goes after DNS, not before&lt;/h3&gt;
&lt;p&gt;This is the one most people building a fetch-any-URL feature get wrong, and it is the scary one.&lt;/p&gt;
&lt;p&gt;A checker fetches whatever URL the user typed. That is the feature. It is also a classic &lt;a rel=&quot;nofollow&quot; href=&quot;https://owasp.org/www-community/attacks/Server_Side_Request_Forgery&quot;&gt;SSRF&lt;/a&gt; engine. Someone signs up, points a check at &lt;code&gt;http://169.254.169.254/latest/meta-data/&lt;/code&gt;, and now your prober is reading cloud credentials from inside your own network and showing them the reply. Point it at &lt;code&gt;http://10.0.0.5:6379&lt;/code&gt; and it is a port scanner for your private subnet.&lt;/p&gt;
&lt;p&gt;The trap is thinking you can block this by checking the hostname. You cannot. The classic bypass is DNS rebinding: the name looks public and passes your check, but it resolves to &lt;code&gt;127.0.0.1&lt;/code&gt;. The block has to happen after resolution, on the real IP you are about to dial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let addrs: Vec&amp;lt;SocketAddr&amp;gt; = p
    .resolver
    .resolve_addrs(host)
    .await
    .map_err(ConnectError::Dns)?
    .into_iter()
    .filter(|ip| p.ssrf_guard.allow(*ip))   // block loopback, private, link-local, metadata
    .map(|ip| SocketAddr::new(ip, port))
    .collect();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resolve first, filter every resolved IP, then connect only to what survives. And because each redirect hop reconnects through this same guarded path, a &lt;code&gt;Location:&lt;/code&gt; header pointing at &lt;code&gt;169.254.169.254&lt;/code&gt; gets rejected exactly like a directly typed one. The safety sits in the single place all connections pass through, so there is nothing to forget on the redirect path. If you ever fetch a user-supplied URL server-side, this is the check that matters, and the hostname version is not it.&lt;/p&gt;
&lt;h3 id=&quot;4-stop-grepping-your-error-strings&quot;&gt;4. Stop grepping your error strings&lt;/h3&gt;
&lt;p&gt;When a check goes red the user gets one line, and that line has to be right. &quot;Something went wrong&quot; trains people to ignore alerts. &quot;Certificate expired&quot; tells them what to fix in five seconds.&lt;/p&gt;
&lt;p&gt;The tempting way to produce that line is to match on error message text. It is a trap: those strings change between library versions and read differently on every OS. The message is not an API. So instead the connector downcasts to the real error and reads the reason straight out of it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn tls_reason(io: &amp;amp;io::Error) -&amp;gt; &amp;amp;'static str {
    use rustls::CertificateError;
    let Some(rustls::Error::InvalidCertificate(cert)) =
        io.get_ref().and_then(|e| e.downcast_ref::&amp;lt;rustls::Error&amp;gt;())
    else {
        return &quot;tls&quot;;
    };
    match cert {
        CertificateError::Expired =&amp;gt; &quot;certificate expired&quot;,
        CertificateError::UnknownIssuer =&amp;gt; &quot;certificate not trusted&quot;,
        CertificateError::NotValidForName =&amp;gt; &quot;certificate hostname mismatch&quot;,
        CertificateError::Revoked =&amp;gt; &quot;certificate revoked&quot;,
        _ =&amp;gt; &quot;certificate invalid&quot;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An expired cert, an untrusted issuer, and a hostname mismatch are three different problems with three different fixes, and the user should never have to guess which one they have. The &lt;code&gt;io::ErrorKind&lt;/code&gt; and the rustls error already carry the truth. The trick is to refuse to flatten it into a string and then re-parse the string you just made.&lt;/p&gt;
&lt;h3 id=&quot;so-reqwest-or-not-&quot;&gt;So, reqwest or not?&lt;/h3&gt;
&lt;p&gt;For your program? Almost certainly reqwest. Going down to hyper means you own redirects, decompression with a size cap, timeout budgeting per hop, and the request-target rules for HTTP/1 versus HTTP/2. That is real code with real edge cases, and every one is a place reqwest would have been correct for free. If I needed to fetch a config file at startup, writing this connector would be a mistake.&lt;/p&gt;
&lt;p&gt;It is worth it here for one reason: measuring and guarding the request is the product, not a detail of it. When the request itself is the thing you sell, you want to own every millisecond and every IP it touches.&lt;/p&gt;
&lt;p&gt;Uptimepage is open source, AGPL-3.0, and the whole probe path is in the repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;github.com/uptimepage/uptimepage&lt;/a&gt;. If you want the wider build story, one binary and two databases, &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/building-an-uptime-monitor-in-rust&quot;&gt;that is a separate post&lt;/a&gt;. Or just &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;start free on the hosted tier&lt;/a&gt; and point a check at something.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22048/4-things-writing-an-http-prober-in-rust-taught-me</guid>
<pubDate>Tue, 07 Jul 2026 14:10:44 +0000</pubDate>
</item>
<item>
<title>Email bombing through uptime monitoring pages</title>
<link>https://coderlegion.com/22003/email-bombing-through-uptime-monitoring-pages</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/email-bombing-uptime-pages&quot;&gt;uptimepage.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I build an uptime monitoring tool. Part of my job is to attack it before someone else does. This post is about one attack that most people in this space do not talk about: using monitoring tools to flood someone's inbox with email.&lt;/p&gt;
&lt;p&gt;I will show you how it works from the attacker side. Then I will show you how to stop it from the builder side. If you run any tool that sends email to an address a user typed in, this is your problem too.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cover photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/photos/a-close-up-of-a-cell-phone-with-various-app-icons-X8ejw0g0C_g&quot;&gt;Brian J. Tromp&lt;/a&gt; on Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Any tool that emails an address you type in can be turned into an inbox flood, and monitoring tools are ideal for it: many tools, many monitors per account, and real senders that pass SPF and DKIM so the mail lands in the inbox, not spam. If you build one, cap confirm emails by target address across the whole platform, say who added the address, add a one-click stop link (RFC 8058), block role and own-domain addresses, and skip the confirm email when the user is already signed in with that address.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-email-bombing-is&quot;&gt;What email bombing is&lt;/h3&gt;
&lt;p&gt;Email bombing is simple. You flood one inbox with so many messages that the person cannot use it. Hundreds of emails in a few minutes. When the attacker uses signup forms to do it, people call it &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.m3aawg.org/rel-WebFormHeader&quot;&gt;subscription bombing or list bombing&lt;/a&gt;. Same idea. Anti-abuse groups describe it as a denial-of-service attack on your inbox, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.itbrew.com/stories/2026/03/20/attacking-the-inbox-it-pros-seeing-rise-in-subscription-bombing&quot;&gt;IT teams saw a rise in 2026&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The goal is usually not the flood itself. The flood is a cover.&lt;/p&gt;
&lt;p&gt;Say an attacker just stole your card and bought something. Your bank sends an email or a text: &quot;Did you make this payment?&quot; If your inbox has hundreds of new emails in a few minutes, you will never see that one bank message. By the time you dig it out, the money is gone. The flood buys the attacker time and quiet. Security teams have &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.proofpoint.com/us/blog/email-and-cloud-threats/subscription-bombing-hides-real-cyberattacks&quot;&gt;watched attackers use this trick to hide real fraud&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is not always about money. Some ransomware crews use email bombing as step one. They flood a worker with mail, then call and pretend to be the IT help desk: &quot;We see the spam, let us help you fix it.&quot; The panic is the way in. The flood softens the target for the real attack. The US cyber agency CISA &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-131a&quot;&gt;documented this play&lt;/a&gt;: email bombing plus a fake help-desk chat on Microsoft Teams, used by the Black Basta ransomware crew to get in. &lt;a rel=&quot;nofollow&quot; href=&quot;https://thehackernews.com/2025/06/former-black-basta-members-use.html&quot;&gt;Former members kept using it through 2025&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-uptime-pages-make-a-good-weapon&quot;&gt;Why uptime pages make a good weapon&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://uptimepage.dev/static/marketing/blog-hacker.webp&quot; alt=&quot;Green binary code glowing on a screen next to a laptop keyboard in the dark&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/photos/black-laptop-computer-turned-on-with-green-screen-QHDVlXhIacg&quot;&gt;Moritz Erken&lt;/a&gt; on Unsplash.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here is the part that surprised me the first time I looked.&lt;/p&gt;
&lt;p&gt;Most uptime and status tools let you add an email address for alerts. You type an address, the tool sends a &quot;confirm your email&quot; message, and you click a link to turn alerts on. Normal stuff.&lt;/p&gt;
&lt;p&gt;Now look at it as an attacker. The tool will send an email to any address I type. I do not need to own that address. I type the victim's email and hit save. The confirm email goes to the victim, not to me.&lt;/p&gt;
&lt;p&gt;One email is not a bomb. But there are many of these tools. And inside each tool I can make many monitors, each with its own alert email. So I write a small script. Sign up, add the victim's address to fifty monitors, move to the next tool, repeat. The victim gets a wave of &quot;please confirm&quot; emails from services they have never heard of.&lt;/p&gt;
&lt;p&gt;The mean part is where these emails come from. They come from real companies with a good name. They pass the checks that tell Gmail a sender is real (SPF and DKIM, if you want the terms). So they land in the main inbox, not in spam. &lt;a rel=&quot;nofollow&quot; href=&quot;https://cacm.acm.org/practice/subscription-bombing-email-under-attack/&quot;&gt;One study in 2026&lt;/a&gt; found that most of these emails were not marked as spam and reached the inbox. A normal spam filter usually will not save the victim, because on paper none of it is spam.&lt;/p&gt;
&lt;h3 id=&quot;the-builder-side-how-not-to-be-the-tool-that-floods-people&quot;&gt;The builder side: how not to be the tool that floods people&lt;/h3&gt;
&lt;p&gt;I will be honest. When I first tested my own tool, it had this hole. The confirm email had no real limit and no clear owner. So I fixed it. Here is what actually helped, in order.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rate limits are the floor.&lt;/strong&gt; Cap how many confirm emails one address can get per day across your whole platform, not just per account. If one attacker opens twenty accounts, a per-account limit does nothing. Count by the target address.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Say who and why.&lt;/strong&gt; My old confirm email just said &quot;confirm this address.&quot; Now it says who added it and to what: &quot;Acme Inc added this address to a monitor.&quot; If a stranger gets one, at least they can see what is happening.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give a fast way out.&lt;/strong&gt; Put a one-click &quot;this was not me, stop&quot; link in the email. One click kills that address on your tool. No login, no support ticket. There is a standard for this, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8058&quot;&gt;RFC 8058&lt;/a&gt;, so mail apps can show a stop button right in the message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block the obvious traps.&lt;/strong&gt; Do not allow role addresses like postmaster@ or abuse@. They are shared mailboxes, not a person who asked for alerts. And do not allow your own company domains, because a tool that emails itself can start a mail loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skip the email when you can.&lt;/strong&gt; If the person adding the address is logged in with that same email through Google or GitHub, you already know they own it. Mark it confirmed and send nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is clever. It is boring plumbing. But boring plumbing is what keeps your name off the list of services used in someone's email bomb.&lt;/p&gt;
&lt;h3 id=&quot;if-you-are-the-one-getting-bombed&quot;&gt;If you are the one getting bombed&lt;/h3&gt;
&lt;p&gt;Do not delete everything in a panic. That is exactly what the attacker wants. Somewhere in that pile there may be one real email that matters, like a bank alert or a password reset you did not ask for.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Find the real one first.&lt;/strong&gt; Search your inbox for words like &quot;payment&quot;, &quot;login&quot;, &quot;password&quot;, or your bank's name. Handle that one before anything else, and call your bank if you see a charge you did not make.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cut it off at the source.&lt;/strong&gt; Use the stop and unsubscribe links in the junk. Most real services put them there. It is slow work, but it is what actually reduces the flood.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalate if it keeps coming.&lt;/strong&gt; Tell your email provider. Gmail and others can filter a flood once they see the pattern. And if someone calls you &quot;from IT&quot; right after the flood starts, be careful. That call can be part of the attack.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;one-last-thing&quot;&gt;One last thing&lt;/h3&gt;
&lt;p&gt;I like uptime tools. I build one. But any tool that will email a stranger on request can be turned into a small weapon, and most builders never test for it. If you run one of these, spend an afternoon trying to bomb yourself. You will learn more from that hour than from any security checklist, and you will find the holes before someone else does.&lt;/p&gt;
&lt;p&gt;More reading: &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog/building-an-uptime-monitor-in-rust&quot;&gt;how I built this monitor&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/abuse-policy&quot;&gt;how to report abuse to us&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22003/email-bombing-through-uptime-monitoring-pages</guid>
<pubDate>Mon, 06 Jul 2026 13:14:50 +0000</pubDate>
</item>
<item>
<title>Dashboard Design for Incident Response</title>
<link>https://coderlegion.com/21769/dashboard-design-for-incident-response</link>
<description>&lt;h3 id=&quot;most-dashboards-answer-one-question-is-everything-okay-&quot;&gt;Most dashboards answer one question: &lt;em&gt;Is everything okay?&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;During an incident, nobody's asking that.&lt;/p&gt;
&lt;p&gt;The real question: &lt;em&gt;What broke, where, and what changed?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most dashboards fail at incidents because they were built for monitoring, not troubleshooting.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;A typical dashboard shows CPU, memory, disk, network, requests, uptime.&lt;/p&gt;
&lt;p&gt;Useful for routine checks.&lt;/p&gt;
&lt;p&gt;During an outage? Just noise.&lt;/p&gt;
&lt;p&gt;You're not looking for reassurance. You're looking for evidence.&lt;/p&gt;
&lt;h3 id=&quot;two-different-jobs&quot;&gt;Two Different Jobs&lt;/h3&gt;
&lt;p&gt;Most teams put everything on one dashboard. That's a compromise that doesn't work for either job.&lt;/p&gt;
&lt;p&gt;Monitoring dashboard: Is the platform healthy? SLAs being met? Resources used correctly?&lt;/p&gt;
&lt;p&gt;Incident dashboard: What failed? When? What changed? Where do I look next?&lt;/p&gt;
&lt;p&gt;Same tools, different purposes.&lt;/p&gt;
&lt;h3 id=&quot;what-works-during-an-outage&quot;&gt;What Works During an Outage&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Error rate front and center.&lt;/strong&gt; 5XX errors, exceptions, failed transactions. Failures tell the story faster than CPU metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timeline on the graph.&lt;/strong&gt; Mark deployments, infrastructure changes, scaling events. Most incidents start right after something changed. Make this visible in one second.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency health.&lt;/strong&gt; A healthy app talking to a dead database is not healthy. Dependencies often point to root cause faster than app metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Golden signals.&lt;/strong&gt; Latency, traffic, errors, saturation. These beat hundreds of infrastructure metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logs visible.&lt;/strong&gt; Top exceptions, error spikes, failed endpoints. Reduce tab-switching during incidents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service map.&lt;/strong&gt; Which services depend on the failing one? Visual dependency maps answer this instantly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alert state.&lt;/strong&gt; Which alerts fired? Which started first? First alert usually beats alert #100 for root cause.&lt;/p&gt;
&lt;h3 id=&quot;the-test&quot;&gt;The Test&lt;/h3&gt;
&lt;p&gt;For every panel: How does this help me resolve the incident faster?&lt;/p&gt;
&lt;p&gt;If the answer isn't obvious, remove it.&lt;/p&gt;
&lt;h3 id=&quot;example&quot;&gt;Example&lt;/h3&gt;
&lt;p&gt;EKS outage. Don't show cluster CPU and memory.&lt;/p&gt;
&lt;p&gt;Show:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Failed requests by service&lt;/li&gt;
&lt;li&gt;Pod restarts&lt;/li&gt;
&lt;li&gt;Readiness failures&lt;/li&gt;
&lt;li&gt;Recent deployments&lt;/li&gt;
&lt;li&gt;HPA scaling events&lt;/li&gt;
&lt;li&gt;Dependency latency&lt;/li&gt;
&lt;li&gt;Top exceptions&lt;/li&gt;
&lt;li&gt;Queue backlogs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One tells you the cluster exists. The other helps you fix it.&lt;/p&gt;
&lt;h3 id=&quot;the-point&quot;&gt;The Point&lt;/h3&gt;
&lt;p&gt;Monitoring dashboards tell you something broke.&lt;/p&gt;
&lt;p&gt;Incident dashboards help you figure out why.&lt;/p&gt;
&lt;p&gt;During an outage, only the second one matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;devops-sre-monitoring-kubernetes-incidentresponse-dashboard&quot;&gt;DevOps #SRE #Monitoring #Kubernetes #IncidentResponse #Dashboard&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21769/dashboard-design-for-incident-response</guid>
<pubDate>Thu, 02 Jul 2026 15:12:56 +0000</pubDate>
</item>
<item>
<title>Building an uptime monitor in Rust: one binary, two databases, 130K checks/sec per core</title>
<link>https://coderlegion.com/21623/building-an-uptime-monitor-in-rust-one-binary-two-databases-130k-checks-sec-per-core</link>
<description>&lt;p&gt;I spent the last few months building &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;Uptimepage&lt;/a&gt;, an open-source uptime monitor and status page written in Rust. This post is the build story: the decisions that shaped it, the parts I rewrote, and the numbers that came out the other side.&lt;/p&gt;
&lt;p&gt;The whole thing ships as one self-contained binary of about 23 MB, plus Postgres and ClickHouse. You can &lt;code&gt;docker compose up&lt;/code&gt; and self-host it, or use the hosted tier. Source is AGPL-3.0 on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-one-binary&quot;&gt;Why one binary&lt;/h3&gt;
&lt;p&gt;A status page is a small surface with a lot of moving parts behind it: a scheduler, probe workers, an HTTP client, a time-series writer, an incident detector, an alerting fan-out, a web UI, a JSON API. The usual answer is a handful of services and a message bus between them.&lt;/p&gt;
&lt;p&gt;I went the other way. One process, one binary, the same image whether it runs the control plane or a remote probe. No queue to operate, no version skew between services, no &quot;which container is wedged&quot; at 3am. The cost is that you have to be careful about what shares a thread and what can stall what. Most of the engineering below is about keeping those boundaries clean inside a single process.&lt;/p&gt;
&lt;p&gt;Stack: Rust 1.95 (edition 2024), Tokio, Axum 0.8, Askama for compile-time HTML templates, HTMX 2 for partial swaps so there is no SPA framework to ship. The API stays the single source of truth because every UI mutation hits the same &lt;code&gt;/api/v1/*&lt;/code&gt; endpoint a script would.&lt;/p&gt;
&lt;h3 id=&quot;two-databases-on-purpose&quot;&gt;Two databases, on purpose&lt;/h3&gt;
&lt;p&gt;Monitors are low-cardinality relational data that gets mutated by API calls: targets, regions, channels, incidents, plans. That is Postgres. Check results are append-only, high-cardinality, and almost always queried by time range. That is ClickHouse.&lt;/p&gt;
&lt;p&gt;Trying to force one of those into the other is where uptime monitors usually fall over. Putting billions of check results in Postgres turns every dashboard query into a sequential scan. Putting your relational config in ClickHouse means fighting its update model forever. So: Postgres 18 for the world, ClickHouse 26.3 for the firehose. Both run their migrations at process startup, so there is no separate migrator to forget.&lt;/p&gt;
&lt;h3 id=&quot;the-http-client-i-did-not-want-to-write&quot;&gt;The HTTP client I did not want to write&lt;/h3&gt;
&lt;p&gt;The first version used a popular high-level HTTP client. It worked, but a monitor is a weird HTTP workload: you connect once per target per interval, you never reuse the connection, and you care about the timing of each phase more than the body.&lt;/p&gt;
&lt;p&gt;So I dropped down to &lt;code&gt;hyper&lt;/code&gt; and &lt;code&gt;hyper-util&lt;/code&gt; with &lt;code&gt;rustls&lt;/code&gt; and built a connector that times DNS resolution, TCP connect, and the TLS handshake as separate numbers, then runs the request over &lt;code&gt;hyper::client::conn&lt;/code&gt; and aborts the connection task the moment the body is read. Each result carries &lt;code&gt;dns_ms&lt;/code&gt;, &lt;code&gt;connect_ms&lt;/code&gt;, &lt;code&gt;tls_ms&lt;/code&gt;, and &lt;code&gt;ttfb_ms&lt;/code&gt; as distinct columns, which is what makes &quot;it got slow but it is the DNS, not your server&quot; a thing the dashboard can actually say.&lt;/p&gt;
&lt;p&gt;The rewrite paid for itself. On a single core the client sustains around 130K checks/sec at saturation, roughly 7.7 microseconds per check. That was a 44 to 56 percent throughput gain over the old path. A chunk of it was a &lt;code&gt;url::parse&lt;/code&gt; call hiding in the redirect policy that cost 7.5 percent on its own and just vanished. Two cores get to about 153K. Scaling goes sub-linear past four cores because of shared HTTP/2 connection state and the pool mutex, which is a fine problem to have for this workload.&lt;/p&gt;
&lt;p&gt;These numbers come from a laptop and a &lt;code&gt;loadtest&lt;/code&gt; binary, so I treat them as regression detection, not capacity planning. The headline in-process run pushed 252K requests/sec sustained at p99 393ms. The design goal is around 50K concurrent in-flight checks per node with under 50ms p99 of per-check overhead on top of the network.&lt;/p&gt;
&lt;h3 id=&quot;one-heap-not-a-timer-per-target&quot;&gt;One heap, not a timer per target&lt;/h3&gt;
&lt;p&gt;The naive scheduler spawns a timer task per monitor. That falls apart at fleet size: thousands of tasks, thousands of wakeups, memory that grows with the number of targets.&lt;/p&gt;
&lt;p&gt;Instead there is a single driver task that owns one &lt;code&gt;BinaryHeap&amp;lt;Reverse&amp;lt;Due&amp;gt;&amp;gt;&lt;/code&gt;, a min-heap keyed by the next due &lt;code&gt;Instant&lt;/code&gt; for the whole fleet. Memory stays flat in fleet size. Each target gets a deterministic jitter offset hashed from its UUID so a thousand monitors on a 60s interval do not all fire on the same tick. Generation and sequence counters mean a re-scheduled target supersedes its stale heap entry instead of double-firing. The registry refresh that pulls config from Postgres runs on its own task with exponential backoff, so a Postgres hiccup never stalls dispatch: the scheduler keeps running on what it last knew.&lt;/p&gt;
&lt;h3 id=&quot;failure-isolation-inside-one-process&quot;&gt;Failure isolation inside one process&lt;/h3&gt;
&lt;p&gt;Because everything is one process, one bad target cannot be allowed to take down the rest. Three patterns do most of that work. Per-host circuit breakers trip when a host keeps failing, so it fails fast with &lt;code&gt;circuit_open&lt;/code&gt; instead of tying up a worker on a timeout, then probes half-open after a cooldown. A per-tenant host throttle bulkhead caps how many checks can be in flight against one host at once; over the cap, a check is recorded as throttled and degraded rather than piling on, and it never pages. And singleflight on RDAP collapses domain-expiry checks for the same domain across many tenants into one upstream probe, with sticky last-good state so a flaky registrar does not flip the monitor red.&lt;/p&gt;
&lt;p&gt;The worker pool itself is a task-per-dispatch gated by a semaphore sized to a max-concurrency setting, with an SSRF guard filtering resolved IPs before any connect.&lt;/p&gt;
&lt;h3 id=&quot;modeling-time-series-in-clickhouse&quot;&gt;Modeling time series in ClickHouse&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;check_results&lt;/code&gt; table is a &lt;code&gt;MergeTree&lt;/code&gt; ordered by &lt;code&gt;(org_id, target_id, region, timestamp)&lt;/code&gt;. The &lt;code&gt;org_id&lt;/code&gt; leads the sort key so each tenant gets a sparse-index slice, but it is deliberately kept out of the partition key (partition is by day) so we do not end up with millions of partitions.&lt;/p&gt;
&lt;p&gt;The columns are where the storage savings live:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Timestamps are &lt;code&gt;DateTime('UTC')&lt;/code&gt; with &lt;code&gt;CODEC(DoubleDelta, ZSTD(1))&lt;/code&gt;. Check intervals are near-constant, so DoubleDelta crushes the gaps to almost nothing.&lt;/li&gt;
&lt;li&gt;Numeric phase columns (&lt;code&gt;duration_ms&lt;/code&gt;, &lt;code&gt;dns_ms&lt;/code&gt;, &lt;code&gt;connect_ms&lt;/code&gt;, &lt;code&gt;tls_ms&lt;/code&gt;, &lt;code&gt;ttfb_ms&lt;/code&gt;, response code and size) use &lt;code&gt;CODEC(T64, ZSTD(1))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt; and &lt;code&gt;agent_id&lt;/code&gt; are &lt;code&gt;LowCardinality(String)&lt;/code&gt;, status is an &lt;code&gt;Enum8&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retention is per row. There is a &lt;code&gt;ttl_days&lt;/code&gt; column with &lt;code&gt;TTL timestamp + toIntervalDay(ttl_days)&lt;/code&gt;, stamped from the org's plan at write time. A plan that buys longer history needs zero schema change: new rows just carry a bigger number.&lt;/p&gt;
&lt;p&gt;On top of the raw table sit two &lt;code&gt;AggregatingMergeTree&lt;/code&gt; materialized views: a per-minute rollup kept 30 days and an hourly rollup kept 13 months, both holding &lt;code&gt;quantilesState&lt;/code&gt; for p50/p95/p99 and per-status counts. Reads route by range: anything inside 30 days hits the minute rollup, older ranges hit the hour rollup, and raw reads are capped at a 90-day span. A dashboard asking for &quot;last 24h p99 latency&quot; never touches a raw row.&lt;/p&gt;
&lt;h3 id=&quot;regional-probes-without-a-second-brain&quot;&gt;Regional probes without a second brain&lt;/h3&gt;
&lt;p&gt;You can run probes in multiple regions, but I did not want each region to be its own little system. So an agent is the same binary in agent mode, running as a stateless probe with no database, no web, no alerting. Adding a region adds execution capacity, never a second control plane.&lt;/p&gt;
&lt;p&gt;An agent pulls its region's config from the control plane with ETag/304 handling, serves its last-known config if the control plane blips, and pauses if its token is revoked. It ships results back in batches that reuse one UUID across retries so a lost ack cannot double-count. Region and agent identity are derived server-side from the bearer token, never sent in the payload, so a probe cannot claim to be somewhere it is not. A separate long-poll loop handles interactive &quot;check now&quot; so a button press in the UI runs on a real remote probe within milliseconds.&lt;/p&gt;
&lt;p&gt;Region is the partition dimension end to end, so every read can slice by region: per-region latency series, per-region incident scope, &quot;down in Singapore, up in Helsinki.&quot;&lt;/p&gt;
&lt;h3 id=&quot;incidents-as-a-follower-not-a-gatekeeper&quot;&gt;Incidents as a follower, not a gatekeeper&lt;/h3&gt;
&lt;p&gt;The incident detector is a background task that follows the &lt;code&gt;check_results&lt;/code&gt; stream and writes into the Postgres &lt;code&gt;incidents&lt;/code&gt; table. The rule I held to: it never touches the hot write path, never gates check execution, and never produces alerts directly.&lt;/p&gt;
&lt;p&gt;The detection itself is boring on purpose: two or more consecutive unhealthy results with no open incident opens one, two or more consecutive healthy results closes it. A small flap threshold absorbs single-result blips. The cross-tenant walk is keyset-paginated so memory stays bounded as tenants grow, and a unique index on open incidents resolves the race when two ticks try to open the same one: only the winner pages.&lt;/p&gt;
&lt;p&gt;Opening or resolving fires a non-blocking signal to the escalation engine, which does repeat-until-acknowledged paging on a per-monitor cadence across about fourteen transports (Slack, generic signed webhooks, Telegram, PagerDuty, ntfy, Pushover, Discord, email, and more), with sharded per-incident locks so a reconcile and an inbound signal can never double-page the same episode. Channel secrets are sealed at rest with AES-GCM and never echoed back.&lt;/p&gt;
&lt;h3 id=&quot;automation-as-a-first-class-surface&quot;&gt;Automation as a first-class surface&lt;/h3&gt;
&lt;p&gt;Because the API is the single source of truth, the rest came almost for free:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A self-describing OpenAPI 3.1 spec with Swagger UI.&lt;/li&gt;
&lt;li&gt;An official &lt;a rel=&quot;nofollow&quot; href=&quot;https://registry.terraform.io/providers/uptimepage/uptimepage&quot;&gt;Terraform provider&lt;/a&gt; so you can manage monitors and notification channels as code.&lt;/li&gt;
&lt;li&gt;An &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;MCP server&lt;/a&gt; so an LLM client can query your monitors and incidents over OAuth 2.1, scope-gated and audited, with per-action confirmation on the few write tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-it-is&quot;&gt;Where it is&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;Uptimepage&lt;/a&gt; is live, free to start with no card, and AGPL-3.0 open source. The core is not paywalled: checks, status pages, subscribers, the API, and every alert channel are in the free tier. It monitors itself and serves its own status badges from the running binary, which is the most honest dogfood I could think of.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Site and hosted tier: &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev&quot;&gt;https://uptimepage.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/uptimepage/uptimepage&quot;&gt;https://github.com/uptimepage/uptimepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More build notes: &lt;a rel=&quot;nofollow&quot; href=&quot;https://uptimepage.dev/blog&quot;&gt;https://uptimepage.dev/blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Happy to answer anything about the Rust internals in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21623/building-an-uptime-monitor-in-rust-one-binary-two-databases-130k-checks-sec-per-core</guid>
<pubDate>Tue, 30 Jun 2026 17:20:57 +0000</pubDate>
</item>
<item>
<title>Conversation monitoring for voice agents: the six metrics that matter</title>
<link>https://coderlegion.com/21611/conversation-monitoring-for-voice-agents-the-six-metrics-that-matter</link>
<description>&lt;h2 id=&quot;the-six-conversation-metrics-that-catch-what-voice-agent-dashboards-miss&quot;&gt;The six conversation metrics that catch what voice agent dashboards miss&lt;/h2&gt;
&lt;p&gt;Most teams have shipped voice agents and most are not happy with them. In Hamming AI's State of Voice AI 2026, 87 percent of companies have deployed voice agents but only 12 percent are satisfied with the quality. The gap is not latency or uptime. It is conversation-level quality: contradictions across turns, missed intents, escalations that fire too late, and sentiment that slides before a hang-up. Standard dashboards do not surface any of that.&lt;/p&gt;
&lt;p&gt;The dashboards from a couple of years ago stopped at three numbers: latency, completion rate, and sentiment. Those catch the obvious failures, the calls that time out, do not resolve, or end with an openly angry customer. What they miss is the long tail where every number reads green and the experience still degrades. These six conversation-level metrics catch that long tail. You do not need all six on day one. Start with completion and turn coherence, then add the rest as call volume grows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turn coherence.&lt;/strong&gt; Does the assistant stay consistent across turns. It catches a fact confirmed in turn 3 and contradicted in turn 7, a tool result that comes back but never gets used, and a reference like &quot;the second option you mentioned&quot; the assistant has forgotten. Each turn reads fine on its own; the failure is in the connective tissue between them, so a single-turn check never sees it. Score it with a multi-turn coherence rubric over the full message history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intent confidence.&lt;/strong&gt; Did the system identify what the caller actually asked at the entry point. It catches a vague opening routed to a default flow, a mistranscribed accent that feeds the classifier a garbled input, and two requests in one turn where only one gets handled. Intent taxonomy is org-specific, so this is usually a custom check, and it pairs with audio-transcription scoring when STT drift is the real cause rather than the classifier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Completion rate.&lt;/strong&gt; Did the call resolve the caller's goal. Track two versions: customer-perspective resolution (did they get what they came for) and agent-perspective task completion (did the assistant do the right thing). The split is the signal. A correct out-of-policy refusal scores high on task completion and low on resolution, which tells you the failure was policy-induced, not a capability gap. The delta between the two is more useful than either alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentiment trend.&lt;/strong&gt; The slope across turns, not the static end-of-call sentiment. It catches a caller who enters neutral, sours by turn 3, and hangs up before the agent notices, and it credits the agent that de-escalates a frustrated caller back to positive. Score sentiment per turn and alert on negative slopes that cross a threshold; on outbound calls a falling slope often predicts a hang-up a couple of turns out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalation triggers.&lt;/strong&gt; When and why a call hits a policy or scope boundary that needs a human. It catches escalate-out-of-caution, where the agent could have handled it but handed off anyway, and the reverse, where the agent should have escalated and tried to muscle through instead. Pair a refusal-justification check with a handoff-quality check that scores whether the human got enough context to avoid re-collecting the basics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeat-question signal.&lt;/strong&gt; Did the caller ask the same thing twice because the first answer was not useful. This is the one that catches silent CSAT decline: intent is right, coherence is high, completion fires positive, but the caller had to ask three times. Score it with a custom signal that compares semantic similarity between the caller's turns and flags pairs above a threshold.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The metrics are most useful in combination, not one at a time. Group calls by the set of verdicts rather than by a single score. Low completion plus high repeat-question plus neutral sentiment is the silent-degradation pattern worth fixing. High completion plus declining sentiment plus one escalation is usually recovered-after-friction and fine to leave alone. Clustering the low-scoring calls into named issues, each with a root cause and a fix, is what turns six metrics into an actionable backlog instead of six dashboards to scan.&lt;/p&gt;
&lt;p&gt;A few things that go wrong in practice. Running all six on 50 calls is noise, so start with two and add the rest once each score is stable. Treating sentiment as a single end-of-call label throws away the slope, which is the part that carries the signal. Conflating an escalation trigger (the agent decided to hand off) with an escalation outcome (the human resolved it) hides a real failure cluster. And skipping repeat-question because it needs a custom evaluator drops the highest-signal metric for quiet CSAT decline.&lt;/p&gt;
&lt;p&gt;The point is that the 12 percent satisfaction gap lives at the conversation level, and infrastructure dashboards never reach it. Completion and turn coherence get you started; the other four cover the failures that only show up once you are looking at the whole conversation rather than a single turn.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21611/conversation-monitoring-for-voice-agents-the-six-metrics-that-matter</guid>
<pubDate>Tue, 30 Jun 2026 07:35:54 +0000</pubDate>
</item>
<item>
<title>Two device stories, one URL: why averages lie</title>
<link>https://coderlegion.com/21592/two-device-stories-one-url-why-averages-lie</link>
<description>&lt;p&gt;One checkout URL can look healthy in a blended score while mobile passes and desktop fails. Portfolio averages hide the same split. Here is how we report paired device data instead.&lt;/p&gt;
&lt;p&gt;The account manager pasted a portfolio screenshot into Slack: average Core Web Vitals pass rate across twelve client sites was 81 percent, up three points from last month. Engineering opened the same week’s export and found checkout on one retailer at 94 on mobile and 41 on desktop, both from the same URL, both in field data. The average looked like progress; the paired rows looked like a revenue problem waiting for a marketing campaign to send desktop traffic to a route that could not convert.&lt;/p&gt;
&lt;p&gt;Same URL, two device stories. That is normal for Core Web Vitals, not a tooling glitch. Google’s own field datasets already treat mobile and desktop as separate populations: the Chrome UX Report (CrUX) stores experiences by form factor, and Search Console’s Core Web Vitals report labels each URL group independently for mobile and desktop. The gap is not that vendors lack the split; it is that internal dashboards and client decks collapse the split back into one comforting number.&lt;/p&gt;
&lt;h3 id=&quot;why-mobile-and-desktop-core-web-vitals-diverge-on-one-url&quot;&gt;Why mobile and desktop Core Web Vitals diverge on one URL&lt;/h3&gt;
&lt;p&gt;Largest Contentful Paint often tracks different elements after responsive layout: a hero image on desktop and a text block or product tile on mobile. Interaction to Next Paint reflects touch versus pointer paths, menu density, and how much main-thread work survives on lower-end hardware. Cumulative Layout Shift spikes when ads, banners, or consent UI appear only under certain viewports. None of that shows up if you only store a single “page score” per URL.&lt;/p&gt;
&lt;p&gt;Lab runs make the split obvious when you compare mobile and desktop emulation on the same template. Field data from CrUX does the same by form factor, which is why PageSpeed Insights shows separate rows and accepts &lt;code&gt;strategy=mobile&lt;/code&gt; or &lt;code&gt;strategy=desktop&lt;/code&gt; in its API. Google’s &lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/lab-and-field-data-differences&quot;&gt;web.dev guidance on lab versus field data&lt;/a&gt; adds a further wrinkle: Lighthouse returns the same LCP candidate every time, while field data on one URL often spans several LCP elements depending on viewport and scroll depth. Stopping at whichever row looks better, or averaging them for a client deck, hides that variance. For metric definitions and fix paths, our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lcp-inp-cls-what-each-core-web-vital-means-and-how-to-fix-it?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-two-device-stories-averages-lie&quot;&gt;LCP, INP, and CLS guide on the Watcher blog&lt;/a&gt; stays the reference.&lt;/p&gt;
&lt;h3 id=&quot;where-blended-scores-and-portfolio-averages-mislead&quot;&gt;Where blended scores and portfolio averages mislead&lt;/h3&gt;
&lt;p&gt;We see three patterns that produce misleading comfort in dashboards and decks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single-number client reports.&lt;/strong&gt; A weekly PDF that lists one LCP or one “performance score” per URL without a form-factor column implies one experience. Sponsors assume the number covers how most users browse; in our experience mobile share is often higher on the URLs that matter, but desktop still carries high-value sessions on B2B and admin-heavy flows. Public CrUX aggregates show the same skew: mobile origin pass rates typically trail desktop by several points, so a blended headline can flatter phone-heavy sites even when mobile is the weaker side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Portfolio rollups.&lt;/strong&gt; Agency dashboards that average pass rates across domains treat a mobile fail on checkout and a desktop pass on a blog post as partial credit. The rollup climbs while the conversion path stays broken. We prefer worst-of or paired counts: how many priority URLs pass on both mobile and desktop, not how many pass on either. Search Console labels URL groups separately per device; a group can be Good on mobile and Need improvement on desktop. Exporting one “site health” percentage without that dimension is how an eighty-one percent portfolio average coexists with checkout at forty-one on desktop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Good enough” thresholds on the stronger device.&lt;/strong&gt; Teams sometimes ship when desktop clears a budget while mobile INP stays red because desktop was the default Lighthouse profile in continuous integration. The deploy log shows green; support tickets mention “laggy on phone.” A related trap is treating PageSpeed Insights origin-level field data as proof that a specific URL is fine: when URL-level CrUX samples are thin, PSI falls back to origin aggregates, which can mask a single high-traffic template that fails on one form factor only.&lt;/p&gt;
&lt;p&gt;Each pattern shares the same fix: store and show mobile and desktop as separate series for the same URL, then alert on the weaker side.&lt;/p&gt;
&lt;h3 id=&quot;how-we-report-paired-device-data-to-clients-and-internally&quot;&gt;How we report paired device data to clients and internally&lt;/h3&gt;
&lt;p&gt;We stopped leading with averages in QBR slides. The first table for priority URLs lists mobile LCP, INP, and CLS beside desktop values for the same route, with a column for “both pass” rather than “either pass.” When one side lacks field data, we label it lab-only and do not merge it into a blended pass/fail. For alerts, device-specific thresholds beat a single budget line: a regression that hits mobile INP on checkout triggers the same channel as a desktop LCP fail on the homepage, but the message names the form factor so triage does not waste a day reproducing on the wrong profile.&lt;/p&gt;
&lt;p&gt;Internal stand-ups that read “mobile fail / desktop pass” in the subject line have saved us from closing tickets too early, because the split is visible before anyone opens the chart. When a client asks for one headline number, we give them a paired summary: “ten of twelve money URLs pass on both mobile and desktop; two fail on mobile only.” That sentence is less flattering than an eighty-one percent average, and it is the one that gets engineering budget for the checkout script audit. Schedules, budgets, and portfolio views are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-two-device-stories-averages-lie&quot;&gt;mobile versus desktop Core Web Vitals monitoring on the Watcher blog&lt;/a&gt;; Watcher holds the checklist, while Hashnode holds the reporting argument.&lt;/p&gt;
&lt;h3 id=&quot;what-to-change-in-monitoring-this-week&quot;&gt;What to change in monitoring this week&lt;/h3&gt;
&lt;p&gt;If your stack stores one row per URL, add form factor to the grain before the next client send. Re-run baselines on five conversion URLs with mobile and desktop explicitly selected in PageSpeed Insights or your monitoring product, and note where field data exists for each. When PSI shows origin-level field data only, tag those rows as origin fallback so nobody mistakes them for URL-level proof on both devices. Rewrite one alert policy so a mobile-only breach on a tagged “money URL” cannot be drowned out by desktop green scores on the same path, and add a “both pass” metric for priority tiers if portfolio reporting is automated; the first time that metric drops while the average rises, you will know the blended number was hiding a split.&lt;/p&gt;
&lt;h3 id=&quot;next-step&quot;&gt;Next step&lt;/h3&gt;
&lt;p&gt;Pick one checkout or lead-form URL and publish a paired mobile/desktop baseline internally before the next client-facing report goes out. Use the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-two-device-stories-averages-lie&quot;&gt;mobile and desktop monitoring guide&lt;/a&gt; for schedules and budgets, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lcp-inp-cls-what-each-core-web-vital-means-and-how-to-fix-it?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-two-device-stories-averages-lie&quot;&gt;LCP, INP, and CLS explainer&lt;/a&gt; when someone asks what the columns mean. Two device stories on one URL are expected; one averaged story is the number that usually costs you the argument.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/two-device-stories-one-url-why-averages-lie-mqzm7kk9&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21592/two-device-stories-one-url-why-averages-lie</guid>
<pubDate>Mon, 29 Jun 2026 19:35:23 +0000</pubDate>
</item>
<item>
<title>First two weeks of a performance onboarding (realistic scope)</title>
<link>https://coderlegion.com/21518/first-two-weeks-of-a-performance-onboarding-realistic-scope</link>
<description>&lt;p&gt;New monitoring clients often expect fixes in week one. Realistic onboarding scope is narrower: access, URLs, owners, baseline, and thresholds before anyone promises remediation.&lt;/p&gt;
&lt;p&gt;A new client signed the monitoring retainer on Monday and asked on Wednesday when checkout would be green in Lighthouse. Fair question: the SOW mentioned Core Web Vitals, and their last agency had sent a PDF full of red bars.&lt;/p&gt;
&lt;p&gt;We had not finished agreeing which checkout URL counted, who owned first response on alerts, or whether mobile field data even existed for that template yet. The work was not missing; the calendar was wrong.&lt;/p&gt;
&lt;p&gt;Most onboarding failures we see are scope failures, not skill failures. Teams either treat week one like a mini audit with twenty remediation tickets, or they spend fourteen days in tooling setup and call it done without a baseline the client can repeat. Below is the two-week frame we use with agencies: what we promise, what we defer, and how it lines up with the longer checklists on our main blog.&lt;/p&gt;
&lt;h3 id=&quot;what-goes-wrong-when-onboarding-scope-is-vague-on-day-one&quot;&gt;What goes wrong when onboarding scope is vague on day one&lt;/h3&gt;
&lt;p&gt;Without a written two-week boundary, three collisions are common. Sales language drifts into delivery: &quot;We will monitor performance&quot; becomes &quot;we will improve Core Web Vitals&quot; in the client's head while engineering still waits for staging access. The URL list balloons when someone exports a sitemap, adds every campaign landing page from the last three years, and schedules tests before anyone marks which routes actually convert. Remediation starts before measurement is trusted when a developer fixes hero images on the homepage while checkout still lacks field data and nobody has agreed on alert severity.&lt;/p&gt;
&lt;p&gt;Procurement and delivery can both be happy if week one and week two have named outputs. Those outputs are not &quot;site is fast&quot;; they are &quot;we know what we watch, who acts, and what broke first.&quot;&lt;/p&gt;
&lt;h3 id=&quot;week-one-what-to-lock-before-you-run-pagespeed-tests&quot;&gt;Week one: what to lock before you run PageSpeed tests&lt;/h3&gt;
&lt;p&gt;Week one is contracts and context, not optimisation. We aim to close five agreements in writing (email is fine):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Primary domain and environments (production first; staging only if the client expects pre-release checks).&lt;/li&gt;
&lt;li&gt;Five to ten priority URLs: homepage, main conversion path, one high-traffic template, and commerce or form routes if they exist.&lt;/li&gt;
&lt;li&gt;Mobile and desktop coverage (both, unless the client has a documented mobile-only audience).&lt;/li&gt;
&lt;li&gt;Alert recipients and a named first responder (one person, not a channel full of @here).&lt;/li&gt;
&lt;li&gt;Reporting rhythm: weekly internal scan, monthly client-facing review, and who owns each.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If access is slow, week one still produces a blocker log: who owes credentials, which firewall ticket is open, which analytics consent rule blocks synthetic tests. That log is a deliverable, and it prevents week two from quietly slipping to week four.&lt;/p&gt;
&lt;p&gt;Our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/site-audit-checklist-onboarding-client-performance-monitoring?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-performance-onboarding-two-weeks&quot;&gt;site audit checklist for onboarding new clients into performance monitoring&lt;/a&gt; has the full tick list for access, URL inventory, and handover. On Hashnode we are only arguing for order: scope and owners before quotas and schedules.&lt;/p&gt;
&lt;h3 id=&quot;week-two-baseline-thresholds-and-the-first-client-safe-summary&quot;&gt;Week two: baseline, thresholds, and the first client-safe summary&lt;/h3&gt;
&lt;p&gt;Week two is when tools earn their place. Run initial tests on the priority URL set from week one, record LCP, INP, and CLS for mobile and desktop where data exists, and mark pages with no field data so nobody interprets a lab-only score as a production guarantee.&lt;/p&gt;
&lt;p&gt;Set conservative thresholds on those same URLs. Tight budgets on day ten usually mean alert noise by day seventeen, so we would rather start slightly loose, watch one full weekly cycle, and tighten after we see variance.&lt;/p&gt;
&lt;p&gt;Wire alerts to the routes agreed in week one: one internal channel for regressions that need same-day eyes, and a separate, slower route for client-visible summaries if the contract includes them.&lt;/p&gt;
&lt;p&gt;Draft a short &quot;what we monitor and why&quot; note the account lead can send without engineering in the thread. Three paragraphs cover which URLs, which metrics, what happens when a threshold breaks, and what is explicitly out of scope for the monitoring retainer.&lt;/p&gt;
&lt;p&gt;End week two with three actions from the baseline, each with an owner and a due date. They can be investigative (&quot;confirm whether tag X loads on checkout&quot;) rather than shipped fixes; the client should see prioritisation, not a promise that all three close before month end.&lt;/p&gt;
&lt;h3 id=&quot;what-we-deliberately-defer-past-the-first-two-weeks&quot;&gt;What we deliberately defer past the first two weeks&lt;/h3&gt;
&lt;p&gt;Saying no early protects the retainer later. Full template coverage across hundreds of URLs belongs in month two unless the contract priced it; CI gate design, CrUX versus lab reconciliation workshops, and third-party script triage across every tag manager container are real work, but they are not week-two work.&lt;/p&gt;
&lt;p&gt;We also defer client-facing dashboard polish, because a screenshot of a green score without context creates the same false confidence as the PDF they received from the previous vendor. Remediation sprints stay separate from monitoring setup unless the SOW merged them with hours attached; mixing the two in fortnight one is how agencies absorb unpaid engineering.&lt;/p&gt;
&lt;p&gt;If the client pushes for fixes immediately, we point to the three actions from the baseline and offer a change order for implementation. Monitoring onboarding is complete when everyone can answer what broke first, who got pinged, and what we do next.&lt;/p&gt;
&lt;h3 id=&quot;how-the-two-week-frame-maps-to-ongoing-cwv-operations&quot;&gt;How the two-week frame maps to ongoing CWV operations&lt;/h3&gt;
&lt;p&gt;Fortnight one is intake; the recurring habit starts afterward. After week two, we hand teams the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/core-web-vitals-monitoring-checklist-for-agencies?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-performance-onboarding-two-weeks&quot;&gt;Core Web Vitals monitoring checklist for agencies&lt;/a&gt; for weekly scans, monthly deep dives, and deploy checks. That document assumes monitoring is live and scoped. Without the two-week frame, teams treat the checklist like a one-off audit and abandon it when the first sprint ends.&lt;/p&gt;
&lt;p&gt;In practice the split looks like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Phase &lt;/th&gt;&lt;th&gt;Question it answers &lt;/th&gt;&lt;th&gt;Typical owner&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Weeks 1–2 (this post) &lt;/td&gt;&lt;td&gt;What do we watch, who responds, what is the baseline? &lt;/td&gt;&lt;td&gt;Delivery lead + technical lead&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Week 3 onward (Watcher checklist) &lt;/td&gt;&lt;td&gt;What do we do every Monday and every month? &lt;/td&gt;&lt;td&gt;Account manager + engineering&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Agencies managing several clients benefit from the same shape every time. Portfolio onboarding is repetitive; the client's internal politics are not. Reusing the two-week boundary makes it easier to compare sites without pretending each onboarding is bespoke heroics.&lt;/p&gt;
&lt;h3 id=&quot;what-to-put-in-the-client-email-at-the-end-of-week-two&quot;&gt;What to put in the client email at the end of week two&lt;/h3&gt;
&lt;p&gt;We keep the end-of-fortnight email short enough to forward inside the client's organisation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirmed URL list (with one-line business reason per URL).&lt;/li&gt;
&lt;li&gt;Baseline table for LCP, INP, CLS on those URLs (note where field data is missing).&lt;/li&gt;
&lt;li&gt;Alert routes and first responder name.&lt;/li&gt;
&lt;li&gt;Top three findings with owners (not necessarily fixes).&lt;/li&gt;
&lt;li&gt;Explicit &quot;starts week three&quot; items: expanded coverage, remediation quotes, workshop dates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That email sets expectations better than another Lighthouse export: procurement sees deliverables, engineering sees assigned tickets, and sponsors see that monitoring is operational rather than a slide in the pitch deck.&lt;/p&gt;
&lt;h3 id=&quot;next-step-1&quot;&gt;Next step&lt;/h3&gt;
&lt;p&gt;If you are onboarding a client this month, write the two-week outputs before you buy more test quota. Use the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/site-audit-checklist-onboarding-client-performance-monitoring?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-performance-onboarding-two-weeks&quot;&gt;site audit checklist&lt;/a&gt; for ticks inside each week, then move into the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/core-web-vitals-monitoring-checklist-for-agencies?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-performance-onboarding-two-weeks&quot;&gt;agency CWV checklist&lt;/a&gt; once alerts have survived a full weekly cycle without being muted. Realistic scope is not pessimism; it is how monitoring retainers survive contact with real clients.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/first-two-weeks-of-a-performance-onboarding-realistic-scope&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21518/first-two-weeks-of-a-performance-onboarding-realistic-scope</guid>
<pubDate>Sun, 28 Jun 2026 16:03:42 +0000</pubDate>
</item>
<item>
<title>When to Use Synthetic vs Real User Monitoring for Performance</title>
<link>https://coderlegion.com/21496/when-to-use-synthetic-vs-real-user-monitoring-for-performance</link>
<description>&lt;p&gt;Performance monitoring conversations often collapse two different questions into one: &quot;Are we fast in a controlled test?&quot; and &quot;Are we fast for real visitors?&quot; Synthetic monitoring answers the first; real user monitoring (RUM) answers the second. Teams that pick only one layer usually discover the gap after a deploy, when lab scores look fine but conversion drops on mobile, or when CrUX turns red a week later. What follows is a practical split for web teams and agencies: what each method measures, when each is worth the operational cost, and how scheduled PageSpeed monitoring with CrUX context fits beside first-party RUM without forcing a rip-and-replace.&lt;/p&gt;&lt;h2 id=&quot;what-is-synthetic-monitoring-for-web-performance&quot; class=&quot;scroll-mt-24&quot;&gt;What is synthetic monitoring for web performance?&lt;/h2&gt;&lt;p&gt;Synthetic monitoring runs automated tests against URLs you choose, on a schedule you control, in a lab environment, loading each page with a fixed device profile, simulated network, and throttled CPU (for example Lighthouse mobile emulation through &lt;a rel=&quot;nofollow&quot; href=&quot;https://pagespeed.web.dev/&quot;&gt;PageSpeed Insights&lt;/a&gt;) so results stay comparable run to run.&lt;/p&gt;&lt;p&gt;Typical outputs include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Core Web Vitals from the lab run (LCP, INP, CLS where Lighthouse reports them).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Lighthouse Performance score and supporting timings (FCP, TBT, Speed Index).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Diagnostics and opportunities (render-blocking resources, image sizing, third-party cost).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Chrome User Experience Report (CrUX) percentiles when Google publishes field data for that URL and strategy.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Synthetic monitoring is not a one-off PSI paste into a browser tab. It is the same class of measurement, repeated and stored so you can trend, alert, and report. Our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs automated monitoring&lt;/a&gt; guide explains why spot checks fail once you manage more than a handful of URLs. Agencies use synthetic schedules to cover client portfolios (homepage, pricing, lead forms, checkout paths, and campaign landers), usually on both mobile and desktop strategies.&lt;/p&gt;&lt;h2 id=&quot;what-is-real-user-monitoring-rum&quot; class=&quot;scroll-mt-24&quot;&gt;What is real user monitoring (RUM)?&lt;/h2&gt;&lt;p&gt;Real user monitoring collects performance data from actual browser sessions on your site. A JavaScript snippet, or tag manager injection, records timings, navigation, and often custom attributes (logged-in state, plan tier, locale) as users browse, which is why RUM reflects real networks, devices, cache states, ad blockers, and consent banners rather than a fixed lab profile.&lt;/p&gt;&lt;p&gt;First-party RUM products (PageVitals, DebugBear, SpeedCurve, Datadog RUM, and others) let you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Segment by page, device, geography, and custom tags.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;See distributions and percentiles from your traffic, not only Google's aggregate.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Monitor routes CrUX rarely covers well (authenticated app shells, low-volume templates).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Tie performance to business events when you connect analytics or product data.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That traffic mix is also why RUM is harder to compare week to week unless you control for seasonality and campaigns. For metric definitions shared by both lab and field programmes, start with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/what-are-core-web-vitals-a-practical-guide-for-2026&quot;&gt;What Are Core Web Vitals?&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lcp-inp-cls-what-each-core-web-vital-means-and-how-to-fix-it&quot;&gt;LCP, INP, and CLS explained&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;crux-vs-first-party-rum-both-are-field-data-but-not-the-same-thing&quot; class=&quot;scroll-mt-24&quot;&gt;CrUX vs first-party RUM: both are field data, but not the same thing&lt;/h2&gt;&lt;p&gt;Teams comparing RUM vs synthetic monitoring sometimes treat CrUX as &quot;free RUM,&quot; yet it is field data without session-level detail, and that distinction matters when you choose tooling or write a client report.&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Source&lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;th&gt;Strength&lt;/th&gt;&lt;th&gt;Limit&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CrUX&lt;/td&gt;&lt;td&gt;Google's aggregated Chrome experience for URLs with enough traffic&lt;/td&gt;&lt;td&gt;Aligns with Search Console and SEO reporting; no snippet required&lt;/td&gt;&lt;td&gt;Rolling window; URL-level; limited segmentation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;First-party RUM&lt;/td&gt;&lt;td&gt;Your snippet on your origin&lt;/td&gt;&lt;td&gt;Session paths, custom tags, logged-in views&lt;/td&gt;&lt;td&gt;Consent, tag governance, maintenance per site&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Synthetic lab&lt;/td&gt;&lt;td&gt;Scheduled Lighthouse-style runs&lt;/td&gt;&lt;td&gt;Same-day, repeatable, works on staging&lt;/td&gt;&lt;td&gt;Simulated users, not real cache or ad mix&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;PageSpeed Insights and many monitoring tools show CrUX beside lab results, which helps public marketing URLs, but that pairing does not replace RUM when stakeholders ask what paying customers on 4G in Germany experience on a specific checkout step. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/mobile-vs-desktop-core-web-vitals-monitoring-both&quot;&gt;Mobile vs desktop Core Web Vitals monitoring&lt;/a&gt; matters here as well: CrUX and lab both split by strategy, yet RUM segmentation is where device and connection reality show up for product decisions.&lt;/p&gt;&lt;h2 id=&quot;synthetic-vs-rum-comparison-for-web-teams&quot; class=&quot;scroll-mt-24&quot;&gt;Synthetic vs RUM: comparison for web teams&lt;/h2&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Question&lt;/th&gt;&lt;th&gt;Synthetic (scheduled lab)&lt;/th&gt;&lt;th&gt;First-party RUM&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Needs a script on the site?&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Works on staging / pre-release URLs?&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Usually no (no real traffic)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Comparable numbers every run?&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Depends on traffic mix&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Catches deploy regressions same day?&lt;/td&gt;&lt;td&gt;Yes, if scheduled&lt;/td&gt;&lt;td&gt;Yes, with enough sessions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Good for SEO / CrUX alignment?&lt;/td&gt;&lt;td&gt;Lab + CrUX in PSI output&lt;/td&gt;&lt;td&gt;CrUX separate; RUM is your own dataset&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Portfolio scale without per-site snippets?&lt;/td&gt;&lt;td&gt;Favourable for agencies&lt;/td&gt;&lt;td&gt;Harder (consent and client approvals)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Multistep flows (checkout, signup)?&lt;/td&gt;&lt;td&gt;Per-URL unless tool supports journeys&lt;/td&gt;&lt;td&gt;Strong when tagged correctly&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Neither approach wins outright because they answer different risk questions: synthetic shows whether yesterday's release broke lab thresholds on URLs you list, while RUM shows whether real users felt it across segments you care about.&lt;/p&gt;&lt;h2 id=&quot;when-synthetic-monitoring-is-the-right-choice&quot; class=&quot;scroll-mt-24&quot;&gt;When synthetic monitoring is the right choice&lt;/h2&gt;&lt;p&gt;Choose synthetic as your primary web performance layer when you manage many sites or URLs and need one schedule, one history store, and one alert policy without installing tags on every client origin. That is the common agency constraint on marketing sites, brochureware, and public app URLs where the client will not approve another analytics script.&lt;/p&gt;&lt;p&gt;Synthetic also fits when you need repeatable baselines before and after a change. Lab runs on the same URL, strategy, and daypart make comparisons honest, whereas field data drifts when traffic mix shifts. The same logic applies to low-traffic or new pages where CrUX may show &quot;no data&quot; for months while scheduled lab runs still return LCP, INP, and CLS every time.&lt;/p&gt;&lt;p&gt;Use synthetic for staging, preview, or password-protected builds before production, because RUM cannot run where users do not visit yet. Pair schedules with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/the-complete-guide-to-performance-budgets-for-web-teams&quot;&gt;performance budgets&lt;/a&gt; and alerts when you need a steady measurement source, and with portfolio reporting when clients want trend lines instead of a PDF assembled from manual PSI tabs; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/automated-vs-manual-pagespeed-testing-a-time-and-cost-comparison&quot;&gt;Automated vs manual PageSpeed testing&lt;/a&gt; shows the time cost when that layer is missing. Synthetic monitoring still does not replace backend APM, error tracking, or session replay: it measures web URLs in the browser lab, plus CrUX where published, not API latency inside your cluster.&lt;/p&gt;&lt;h2 id=&quot;when-real-user-monitoring-is-the-better-fit&quot; class=&quot;scroll-mt-24&quot;&gt;When real user monitoring is the better fit&lt;/h2&gt;&lt;p&gt;Choose first-party RUM when authenticated experiences dominate the product. Dashboards, account settings, and checkout after login often lack stable CrUX samples, and RUM with custom tags is how you prove INP on those routes. The same applies when stakeholders require segment-level proof by plan tier, locale, experiment bucket, or campaign source, because CrUX will not break down your traffic that way.&lt;/p&gt;&lt;p&gt;RUM also belongs in the stack when you already operate observability tooling and need web vitals beside traces and errors, when CI/CD or feature flags need session-level validation after release, or when multistep journeys must be measured as flows rather than isolated URLs. Legal and client approval for another script varies by estate, and if RUM is already installed you can add synthetic for URLs RUM under-samples or for pre-production checks, while teams with synthetic schedules add RUM where lab and CrUX disagree with support tickets or conversion data.&lt;/p&gt;&lt;h2 id=&quot;how-agencies-combine-synthetic-and-rum-across-client-portfolios&quot; class=&quot;scroll-mt-24&quot;&gt;How agencies combine synthetic and RUM across client portfolios&lt;/h2&gt;&lt;p&gt;Agencies rarely get a single answer across thirty clients, but a workable default is to run scheduled synthetic on mobile and desktop for public marketing and lead-gen URLs, read CrUX from the same test results, and keep budgets and alerts in one place without a snippet on every domain. For flagship SaaS or ecommerce clients with RUM already installed, keep their RUM vendor for logged-in app routes and use synthetic for the URLs the retainer covers and for prospects during &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-prospecting-workflow-analyze-report-qualify-reach-out&quot;&gt;performance audit outreach&lt;/a&gt;; when clients refuse third-party scripts, synthetic plus CrUX is often the entire programme, so say that explicitly in proposals rather than implying session replay is included.&lt;/p&gt;&lt;p&gt;In client reporting, separate slides for lab trends on priority URLs and for RUM segment summaries from the analytics team. Blending both into one green chart invites arguments about methodology. For tool selection by client shape, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/comparing-pagespeed-monitoring-tools-features-agencies-need&quot;&gt;comparing PageSpeed monitoring tools for agencies&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagevitals-vs-apogee-watcher-rum-cicd-agency-portfolio-monitoring&quot;&gt;PageVitals vs Apogee Watcher&lt;/a&gt; when RUM and CI/CD are part of the brief. SaaS product teams face a similar split at single-company scale; our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/performance-monitoring-saas-metrics-product-teams&quot;&gt;SaaS performance monitoring guide&lt;/a&gt; covers URL priorities without repeating the full decision tree here.&lt;/p&gt;&lt;h2 id=&quot;how-scheduled-pagespeed-monitoring-fits-beside-rum-vendors&quot; class=&quot;scroll-mt-24&quot;&gt;How scheduled PageSpeed monitoring fits beside RUM vendors&lt;/h2&gt;&lt;p&gt;Apogee Watcher is built for scheduled synthetic monitoring across many sites: PageSpeed Insights-backed lab runs, CrUX when Google returns field data, vitals and optional FCP/TBT budgets, portfolio alerts, and multi-tenant organisations for agencies. We do not ship first-party RUM; we sit beside RUM, APM, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/lighthouse-ci-vs-managed-monitoring-build-vs-buy-agencies&quot;&gt;Lighthouse CI&lt;/a&gt; gates rather than replacing them, so when a client needs session-level RUM, PageVitals, DebugBear, or an observability vendor still belongs in the stack.&lt;/p&gt;&lt;p&gt;Watcher is often the better primary monitor when you run many client domains under one subscription without per-site RUM rollout, when &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-how-apogee-watcher-discovers-pages-automatically&quot;&gt;automated page discovery&lt;/a&gt; should keep new templates on the schedule without spreadsheet maintenance, when retainers promise ongoing CWV oversight on public URLs rather than full product instrumentation, or when you want prospecting and free evaluation via the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;domain PageSpeed check&lt;/a&gt; before a client signs. Configure mobile and desktop strategies per URL, set budgets, route alerts, and review history in the app, and use RUM elsewhere when the question requires your traffic, your tags, and your logged-in routes.&lt;/p&gt;&lt;h2 class=&quot;scroll-mt-24&quot;&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What is the difference between RUM and synthetic monitoring?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Synthetic monitoring runs controlled lab tests on chosen URLs on a schedule. RUM records performance from real user sessions in the browser, usually via a script you install. Synthetic is repeatable and works without site traffic; RUM reflects actual devices, networks, and behaviour, which is why most mature teams treat the two as complementary rather than interchangeable.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Is CrUX the same as RUM?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. CrUX is Google's aggregated field dataset for URLs with sufficient Chrome traffic, while RUM is first-party data you collect from your visitors. Both describe real loads in the field sense, but CrUX is not session-level RUM and cannot replace custom segmentation on your origin.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Should I use synthetic or RUM for Core Web Vitals?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Use both when you can. Synthetic and CrUX (via scheduled PSI runs) cover public URLs, staging, and low-traffic pages; first-party RUM covers segmented and authenticated experiences CrUX does not explain. SEO and client reporting often lean on CrUX, while product teams often lean on RUM for route-level proof.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Do agencies need RUM on every client site?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Many retainers cover public marketing and key templates where synthetic plus CrUX is enough, and you add RUM when the contract includes logged-in app routes, the client already owns a RUM tool, or stakeholders require session-level segmentation that CrUX cannot supply.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Can synthetic monitoring replace RUM?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Not fully. Synthetic answers whether lab thresholds breached on listed URLs, while RUM answers what users experienced broken down by segment, so use synthetic for portfolio scale and pre-release checks and RUM where traffic truth and custom tags matter.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Does Apogee Watcher include RUM?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Watcher schedules PageSpeed lab tests and shows CrUX where available, and you should layer it with your RUM or observability vendor when you need first-party session data on authenticated or heavily segmented routes.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How often should synthetic tests run compared to RUM sampling?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Synthetic frequency is a schedule you set (daily, weekly, or higher on risky URLs), while RUM sampling is continuous or rate-limited by your vendor. Match synthetic cadence to release risk, use RUM for always-on field truth where installed, and see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-schedule-pagespeed-monitoring-test-frequency-priority-portfolio&quot;&gt;test frequency and priority across a portfolio&lt;/a&gt; for scheduling patterns across many sites.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;Pick synthetic when you need repeatable lab coverage at portfolio scale without a snippet on every domain, and RUM when session segments and authenticated routes drive decisions. Most mature stacks use both, with clear owners for each signal, rather than debating which single tool should own performance end to end.&lt;/p&gt;&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;Start a free Apogee Watcher account&lt;/a&gt; to schedule mobile and desktop tests with vitals budgets across your sites, or run a &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/check&quot;&gt;free domain PageSpeed check&lt;/a&gt; to baseline a URL before you add RUM or expand monitoring.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21496/when-to-use-synthetic-vs-real-user-monitoring-for-performance</guid>
<pubDate>Sun, 28 Jun 2026 08:53:55 +0000</pubDate>
</item>
<item>
<title>OpenTelemetry-native, all-in-one, simple</title>
<link>https://coderlegion.com/20644/opentelemetry-native-all-in-one-simple</link>
<description>&lt;p&gt;&lt;strong&gt;The standard observability stack:&lt;/strong&gt;&lt;br&gt;
Grafana + Loki + Tempo + Prometheus. Four services to deploy, four configs to learn, dashboards to set up before you see a single trace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I actually needed:&lt;/strong&gt;&lt;br&gt;
One place to see traces, logs, and metrics from my services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I built:&lt;/strong&gt;&lt;br&gt;
One container. &lt;code&gt;docker compose up&lt;/code&gt;. Done.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awtklcqp2tqobs8d4t1w.png&quot; alt=&quot;Dasboard Overview&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I work with .NET microservices and at some point I went through the full Grafana setup. And look, it works fine, it's a solid stack. But for what I needed it was way too much. I spent more time configuring things than actually looking at my data. The .NET Aspire dashboard is on the other end: you get a nice UI, but nothing is persisted. Close the browser and everything's gone. No customization either.&lt;/p&gt;
&lt;p&gt;So I built something in between.&lt;/p&gt;
&lt;h3 id=&quot;what-it-is&quot;&gt;What it is&lt;/h3&gt;
&lt;p&gt;OtlpDashboard is a single container that receives OpenTelemetry data and stores it. It speaks standard OTLP on port 4317 (gRPC) and 4318 (HTTP/Protobuf). If your app already exports telemetry, you just point it here. No collector needed (works behind one too if you already have it).&lt;/p&gt;
&lt;p&gt;Storage is SQLite by default, so zero config, just a file. You can switch to PostgreSQL or SQL Server if you want.&lt;/p&gt;
&lt;p&gt;The frontend is a Vue 3 / Nuxt 4 SPA served on the same port. You get a grid of widgets you can customize: stat cards, line charts, sparklines, gauges, heatmaps, log streams, trace viewers, and a few more. Auth is on by default.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bohqrnn79xxeh1u52x65.png&quot; alt=&quot;Trace details&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-ai-helped-me-ship-this-in-under-a-month&quot;&gt;How AI helped me ship this in under a month&lt;/h3&gt;
&lt;p&gt;I want to be upfront about this. I used Claude Code a lot, especially for the frontend. I'm a backend dev. I know .NET, EF Core, gRPC. But Vue and Nuxt? Not really my thing. Without Claude Code I would have spent days stuck on CSS and component wiring. Instead I could focus on what I actually care about: the OTLP ingestion, the storage layer, how widgets work.&lt;/p&gt;
&lt;p&gt;All the architecture and decisions are mine. The AI did the typing.&lt;/p&gt;
&lt;p&gt;And honestly, I think that's the best use of these tools right now if you're a solo dev. They don't replace you. They cover the parts where you're slow, so you can ship something that would normally need a small team.&lt;/p&gt;
&lt;h3 id=&quot;some-stuff-i-like-about-how-it-turned-out&quot;&gt;Some stuff I like about how it turned out&lt;/h3&gt;
&lt;p&gt;You can install widget packs from Git repos. There's a &lt;code&gt;pack.json&lt;/code&gt; that bundles widgets and dashboards together, so if you have a team you can share a curated setup.&lt;/p&gt;
&lt;p&gt;There's also a &lt;code&gt;spec&lt;/code&gt; engine for building custom widgets with Mustache templates. It's sandboxed with DOMPurify, no JS eval. I wanted people to be able to build their own cards without opening security holes.&lt;/p&gt;
&lt;p&gt;Retention is configurable per signal. Set 7 days for traces, 30 for metrics, whatever you want. Old data gets cleaned up on its own.&lt;/p&gt;
&lt;p&gt;And there's an MCP server at &lt;code&gt;/mcp&lt;/code&gt; if you use Claude Code or similar tools. You can query your telemetry from the LLM directly. I didn't expect to use this much but it's actually pretty handy when debugging.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;Quickest way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up --build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This starts everything including a sample workload that generates real data, so you're not staring at an empty dashboard.&lt;/p&gt;
&lt;p&gt;Or just check the live demo. It's a static build with mock data, login accepts any password: &lt;a rel=&quot;nofollow&quot; href=&quot;https://adiakys.github.io/OtlpDashboard/&quot;&gt;Live demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The repo is here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Adiakys/OtlpDashboard&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It's GPLv3. If you try it out let me know what you think, I'm curious what's missing or broken.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/20644/opentelemetry-native-all-in-one-simple</guid>
<pubDate>Mon, 15 Jun 2026 23:34:33 +0000</pubDate>
</item>
<item>
<title>From Reactive to Proactive: How Smart Alerts Change Performance Monitoring</title>
<link>https://coderlegion.com/19416/from-reactive-to-proactive-how-smart-alerts-change-performance-monitoring</link>
<description>&lt;p&gt;An account manager forwarded us a Search Console screenshot at 4:47 p.m. on a Friday. LCP on the homepage had been in the red for eleven days. Engineering had not been in the thread because nobody was assigned to watch the numbers until a sponsor noticed.&lt;/p&gt;&lt;p&gt;That is reactive performance monitoring in practice: fast tools, slow humans, and a calendar that only opens when someone else raises the alarm.&lt;/p&gt;&lt;p&gt;Proactive monitoring is the opposite shape. Scheduled lab tests run whether or not anyone remembered to open PageSpeed Insights. Performance budgets turn history into rules. Alerts fire when those rules break, with enough context to triage and enough restraint that people still read them. The word “smart” in product copy usually means the second part: alerts that match how teams actually work, not a louder siren.&lt;/p&gt;&lt;p&gt;This post is for agency leads who already automate some tests but still learn about regressions from client inboxes. We walk through what reactive looks like, what proactive requires, and how to wire alerts so they change behaviour instead of training everyone to mute the integration.&lt;/p&gt;&lt;h2&gt;What reactive performance monitoring looks like on agency teams&lt;/h2&gt;&lt;p&gt;Reactive does not mean “no tools.” Most agencies we speak with have PageSpeed Insights bookmarks, a monthly spreadsheet, and a standing item on the retainer review slide. Reactive means the &lt;strong&gt;signal arrives after&lt;/strong&gt; the business event you wanted to prevent.&lt;/p&gt;&lt;p&gt;Common patterns:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spot checks after deploy.&lt;/strong&gt; Someone runs PSI on staging, ships, and assumes production matches until a complaint arrives.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quarterly or monthly report pulls.&lt;/strong&gt; CrUX and Lighthouse exports go into a deck. Useful for storytelling; too slow to catch a plugin update that moved INP on Tuesday.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-led discovery.&lt;/strong&gt; The sponsor pastes a screenshot, asks “is this new?”, and the team scrambles to reproduce on a laptop that may not match the lab conditions you use in production monitoring.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alerting without policy.&lt;/strong&gt; Webhooks fire on every metric twitch. By week three the channel is muted and the organisation is back to reactive, only now with guilt about ignored notifications.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each pattern shares a cost: engineering time spent proving what happened instead of fixing it, and account teams explaining delays that sound like neglect even when the underlying fix is straightforward.&lt;/p&gt;&lt;p&gt;Reactive monitoring also scales badly across portfolios. At five client sites you can hero your way through manual checks. At twenty-five, the maths stops working unless you hire a role whose entire job is tab discipline. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/scaling-without-headcount&quot;&gt;Scaling a digital agency without headcount&lt;/a&gt; is really a story about moving discovery, testing, and notification into infrastructure that does not grow linearly with site count.&lt;/p&gt;&lt;h2&gt;What proactive performance monitoring means in practice&lt;/h2&gt;&lt;p&gt;Proactive monitoring answers a narrower question on a fixed schedule: &lt;strong&gt;did any URL we care about cross a line we agreed in advance?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;That sentence has three concrete parts.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Scheduled tests&lt;/strong&gt; replace memory. Mobile and desktop lab runs hit the same URL set on a cadence you can defend in a statement of work: daily for commerce, weekly for brochure sites, and so on. Results land in a time series so “was it always this slow?” becomes a chart, not a debate.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Performance budgets&lt;/strong&gt; replace vibes. Budgets store maximum LCP, INP, CLS, score floors, and supporting metrics per site and strategy. They are the contract line between “watching numbers” and “doing something when numbers fail.” Our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/the-complete-guide-to-performance-budgets-for-web-teams&quot;&gt;complete guide to performance budgets for web teams&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/performance-budget-thresholds-template&quot;&gt;performance budget thresholds template&lt;/a&gt; give starting numbers; you still tune per client.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Alerts&lt;/strong&gt; connect breaches to people. Without alerts, budgets are a dashboard ornament. With naive alerts, budgets become spam. Proactive monitoring needs the third layer to be designed as carefully as the first two.&lt;/p&gt;&lt;p&gt;Proactive is not the same as “more notifications.” It is earlier signal with clearer ownership. The client email on Friday becomes a digest on Wednesday that engineering already acknowledged.&lt;/p&gt;&lt;h2&gt;Automated performance alerts: budgets, cooldowns, and digests&lt;/h2&gt;&lt;p&gt;Teams that call alerts “smart” usually mean a bundle of behaviours that protect human attention:&lt;/p&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Behaviour&lt;/th&gt;&lt;th&gt;What it prevents&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Budget-driven triggers&lt;/td&gt;&lt;td&gt;Alerts on metrics nobody agreed to care about&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cooldowns&lt;/td&gt;&lt;td&gt;The same homepage LCP breach paging you every run until someone ships&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Digest-style delivery&lt;/td&gt;&lt;td&gt;Forty separate messages for forty URLs on one bad deploy&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Resolution when metrics recover&lt;/td&gt;&lt;td&gt;Stale “open” fires that no longer match production&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Severity and routing policy&lt;/td&gt;&lt;td&gt;Critical meaning “drop everything” instead of “anything red”&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Cooldowns deserve a full sentence because agencies misunderstand them. A cooldown is not permission to ignore a sustained failure. It is how you keep the first breach credible while the fix is in progress. If checkout LCP fails Monday and still fails Tuesday, you still need visibility; you do not need forty identical Slack posts that teach the channel to tune out.&lt;/p&gt;&lt;p&gt;Digest-style email is the pattern we ship today in Apogee Watcher: one mail per site per budget-check run, worst pages first, totals for how many URLs breached, and a cap on detail rows so the message stays scannable. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-performance-budgets-email-alerts&quot;&gt;product spotlight on performance budgets and email alerts&lt;/a&gt; walks through what lands in the inbox after each scheduled run. Slack and additional channels are on the roadmap; the policy thinking below applies regardless of transport.&lt;/p&gt;&lt;p&gt;Smart alerts also fail when routing is vague. If &lt;code&gt;#general&lt;/code&gt; receives every severity, proactive monitoring reverts to reactive noise. The fix is usually a one-page policy, not a new tool. Our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/slack-alert-policy-template-for-web-performance-teams&quot;&gt;Slack alert policy template for web performance teams&lt;/a&gt; defines channels, P1–P4 examples, and cooldown language you can paste into client documentation.&lt;/p&gt;&lt;h2&gt;Reactive vs proactive monitoring: what changes for agencies&lt;/h2&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Dimension&lt;/th&gt;&lt;th&gt;Reactive&lt;/th&gt;&lt;th&gt;Proactive&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Trigger&lt;/td&gt;&lt;td&gt;Client email, ranking drop, manual curiosity&lt;/td&gt;&lt;td&gt;Scheduled test + budget breach&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Evidence&lt;/td&gt;&lt;td&gt;Screenshot, single PSI run&lt;/td&gt;&lt;td&gt;Time series, URL list, metric vs threshold&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Timing&lt;/td&gt;&lt;td&gt;Days to weeks after change&lt;/td&gt;&lt;td&gt;Next run after deploy (hours to days, per cadence)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Owner&lt;/td&gt;&lt;td&gt;Whoever saw the message first&lt;/td&gt;&lt;td&gt;Named triage role in alert policy&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Client conversation&lt;/td&gt;&lt;td&gt;“We are investigating”&lt;/td&gt;&lt;td&gt;“We detected X on URL Y; fix in progress”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Scale limit&lt;/td&gt;&lt;td&gt;Heroics and spreadsheets&lt;/td&gt;&lt;td&gt;Automation with policy guardrails&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The shift is operational, not philosophical. You do not need a new performance practice religion. You need infrastructure that runs when people are in meetings, plus rules that tell them which meetings to skip when a digest arrives.&lt;/p&gt;&lt;p&gt;Agencies selling retainers get a second benefit: proactive proof. A digest with timestamps and agreed thresholds is easier to defend than a Lighthouse capture taken after the client complained. That lines up with &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/why-agencies-need-automated-performance-monitoring-in-2026&quot;&gt;why agencies need automated performance monitoring in 2026&lt;/a&gt; and with prospecting workflows that lead with evidence rather than generic pitches.&lt;/p&gt;&lt;h2&gt;How to move one client site from reactive to proactive&lt;/h2&gt;&lt;p&gt;Pick the noisiest or highest-revenue property first. A pilot beats a big-bang policy rewrite.&lt;/p&gt;&lt;h3&gt;1. Lock the URL set&lt;/h3&gt;&lt;p&gt;List money URLs: homepage, primary conversion path, top landing pages, and any commerce routes that bypass cache. If you onboard clients with a checklist, reuse the URL section from our &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/site-audit-checklist-onboarding-client-performance-monitoring&quot;&gt;site audit checklist for performance monitoring onboarding&lt;/a&gt;. Discovery tools can expand the list later; start with templates that actually ship revenue.&lt;/p&gt;&lt;h3&gt;2. Set budgets before alert channels&lt;/h3&gt;&lt;p&gt;Write mobile and desktop thresholds that match the contract or your internal standard. Separate &lt;strong&gt;client-facing&lt;/strong&gt; numbers from &lt;strong&gt;internal early warning&lt;/strong&gt; if you use both; only one layer should interrupt people at night. Mixing them in a single channel produces either false calm or false panic.&lt;/p&gt;&lt;h3&gt;3. Choose delivery to match how the team works&lt;/h3&gt;&lt;p&gt;Email digests suit account leads who live in inboxes. Slack suits engineering-heavy teams with on-call rotation. Pick one primary interrupt path per severity; duplicating the same alert to Slack and email “just in case” wastes attention.&lt;/p&gt;&lt;h3&gt;4. Write a one-page alert policy&lt;/h3&gt;&lt;p&gt;Document: which channel is P1, what “two consecutive breaches” means for money URLs, who triages before client comms, and cooldown expectations. If you cannot classify last month’s real alerts into P1–P3 in under a minute, the levels are decorative.&lt;/p&gt;&lt;h3&gt;5. Run the loop once on purpose&lt;/h3&gt;&lt;p&gt;Lower a threshold on a URL you control, wait for the scheduled run, confirm the digest or channel message lists the expected metric, then restore the threshold. You are validating routing, not performance bragging rights.&lt;/p&gt;&lt;h3&gt;6. Fold results into the client rhythm&lt;/h3&gt;&lt;p&gt;Point the monthly review at open breaches and fixes verified by the next green run. &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/monthly-performance-review-template-for-agency-teams&quot;&gt;Monthly performance review templates for agency teams&lt;/a&gt; work better when the agenda is five live issues, not fifty green checks.&lt;/p&gt;&lt;p&gt;For portfolio-wide setup, follow &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;how to set up automated PageSpeed monitoring for multiple sites&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/core-web-vitals-monitoring-checklist-for-agencies&quot;&gt;Core Web Vitals monitoring checklist for agencies&lt;/a&gt; so proactive habits match the tooling.&lt;/p&gt;&lt;p&gt;Choose one client whose monitoring channel is noisiest or whose sponsor is most likely to forward Search Console screenshots. Document money URLs, mobile and desktop budget numbers, and who triages alerts before account management replies. Turn on scheduled tests if they are not already running, set budgets to match the contract, point alert delivery at inboxes or channels people actually read, and run one controlled breach to confirm the message shape.&lt;/p&gt;&lt;h2&gt;Performance alert policy: fix alert fatigue before you retune thresholds&lt;/h2&gt;&lt;p&gt;When teams say alerts do not work, they usually mean one of three things:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Severity inflation.&lt;/strong&gt; Everything is critical, so critical means nothing.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Route sprawl.&lt;/strong&gt; Alerts go to channels nobody owns, or to clients before engineering confirms cause.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metric sprawl.&lt;/strong&gt; Every Lighthouse sub-score pages someone, even when the retainer only mentions LCP and INP.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Fix routing and severity before you argue about whether LCP should be 2.4 or 2.5 seconds. Export a week of notifications, count how many were labelled urgent, and count how many got a human reply in-thread. High urgent count with low reply count means policy, not tuning.&lt;/p&gt;&lt;p&gt;We have seen agencies cut noise by half without touching budgets: one urgent channel, one team channel, everything else to weekly review. The Hashnode companion piece on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/alert-policies-people-actually-follow-spoiler-fewer-routes-win&quot;&gt;alert policies people actually follow&lt;/a&gt; goes deeper on routing; the headline is the same: fewer routes, clearer meaning.&lt;/p&gt;&lt;h2&gt;How Apogee Watcher fits the proactive loop&lt;/h2&gt;&lt;p&gt;Apogee Watcher is built around the boring loop agencies need at scale: organisations, sites, scheduled PageSpeed tests, budgets per strategy, and alerts when stored lab results breach those budgets. Page discovery and multi-tenant dashboards matter, but proactive monitoring lives or dies on whether anyone hears about a breach in time to act.&lt;/p&gt;&lt;p&gt;Today, reliable delivery for budget violations is &lt;strong&gt;email digest to organisation admins&lt;/strong&gt;: per site, per run, structured summary, cooldowns to limit repeat noise while a URL stays red. In-product signals complement mail for teams that live inside the app. Additional channels (Slack and webhooks) extend the same budget and cooldown model as they ship; the product page for &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/alerts-and-digests&quot;&gt;Smart Alerts&lt;/a&gt; states what is live versus planned so copy stays honest.&lt;/p&gt;&lt;p&gt;Watcher does not replace your alert policy. It gives you scheduled evidence and a default notification shape that respects inboxes. You still name who triages, what counts as client-ready communication, and which URLs belong in the contract.&lt;/p&gt;&lt;p&gt;If you are comparing manual PSI habits to automation, read &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/pagespeed-insights-vs-automated-monitoring-when-manual-checks-arent-enough&quot;&gt;PageSpeed Insights vs automated monitoring&lt;/a&gt; for where each tool belongs. PSI remains excellent for deep dives on one URL; it is a poor system of record for twenty client properties.&lt;/p&gt;&lt;p&gt;Proactive performance monitoring is not a dashboard vanity metric. It is the habit of learning about regressions from your own schedule, with alerts quiet enough that someone will still answer when the digest says checkout LCP crossed the line you wrote down.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up&quot;&gt;Create a free account&lt;/a&gt;&lt;/strong&gt; to run scheduled PageSpeed tests, set performance budgets, and receive budget-violation digests across your client portfolio.&lt;/p&gt;&lt;h2&gt;FAQ&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Is proactive monitoring the same as real-user monitoring (RUM)?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Proactive monitoring in this sense is usually &lt;strong&gt;scheduled lab testing&lt;/strong&gt; against budgets, so you catch regressions soon after deploys even when field data lags. RUM shows what visitors experience in the wild. Strong agency programmes use both; they answer different questions. Lab alerts are faster to wire consistently across many sites.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How often should scheduled tests run for a proactive setup?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Match cadence to risk and plan limits. High-traffic commerce often warrants daily runs on money URLs; lower-risk brochure sites may be weekly. The point is regularity you can cite in a retainer, not hourly noise unless you have a clear incident process.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Will proactive alerts guarantee better Core Web Vitals rankings?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Alerts do not change rankings by themselves. They change &lt;strong&gt;time to fix&lt;/strong&gt;, which protects SEO and conversion work you already sold. Rankings still depend on what you ship after the alert.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Do I need Slack to be proactive?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No. Email digests, ticketing webhooks, and in-app review can be proactive if someone owns triage. Slack helps when engineering already rotates on-call there; it hurts when alerts land in channels nobody monitors.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What is the minimum team size for this to pay off?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;One technical owner can run proactive monitoring across many sites if budgets and policies are written once and reused. The break-even is usually well before you would hire a full-time “performance monitoring” role, especially compared with manual PSI tours each week.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How does this relate to performance budgets in CI/CD?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;CI budgets catch regressions before deploy; scheduled monitoring catches production changes, third-party scripts, and CMS edits that never touched your pipeline. Agencies often need the second layer because client sites change outside your release process. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-performance-budgets-in-ci-cd-pipelines&quot;&gt;performance budgets in CI/CD pipelines&lt;/a&gt; for the build-time side.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/19416/from-reactive-to-proactive-how-smart-alerts-change-performance-monitoring</guid>
<pubDate>Mon, 01 Jun 2026 11:05:46 +0000</pubDate>
</item>
<item>
<title>Your Server Is at 97% CPU Right Now. Would You Know?</title>
<link>https://coderlegion.com/18494/your-server-is-at-97-cpu-right-now-would-you-know</link>
<description>&lt;p&gt;Here's how it usually goes:&lt;/p&gt;
&lt;p&gt;You deploy something. Traffic is light. Server load sits at 15% and you move on to the next thing. Then traffic grows, or a cron job stacks on itself, or a memory leak slowly eats through your RAM over 72 hours. By the time you notice, the server is thrashing, responses take 8 seconds, and your app is effectively dead.&lt;/p&gt;
&lt;p&gt;The frustrating part is that the tools to catch this have been on your server the entire time. &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt; ship with every Linux distribution ever made. Nobody installs them. They're just... there. Waiting for someone to actually ask.&lt;/p&gt;
&lt;p&gt;So I wrote a script that asks every hour and logs a warning when the answer is bad.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-script&quot;&gt;The Script&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash

CHECK=&quot;✓&quot;
CROSS=&quot;✗&quot;

# --- Configuration ---
THRESHOLD=80                    # Alert when usage exceeds this %
LOG_FILE=&quot;/var/log/resource-monitor.log&quot;
DATE=$(date '+%Y-%m-%d %H:%M:%S')

# --- CPU Usage ---
CPU=$(top -bn1 | grep &quot;Cpu(s)&quot; | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1 | xargs printf &quot;%.0f&quot;)

# --- RAM Usage ---
RAM=$(free | awk '/Mem:/ {printf &quot;%.0f&quot;, $3/$2*100}')

echo &quot;[$DATE] CPU: ${CPU}% | RAM: ${RAM}%&quot;

# --- CPU Alert ---
if [ &quot;$CPU&quot; -gt &quot;$THRESHOLD&quot; ]; then
  echo &quot;$CROSS [$DATE] WARNING: CPU at ${CPU}% (threshold: ${THRESHOLD}%)&quot; | tee -a &quot;$LOG_FILE&quot;
else
  echo &quot;$CHECK CPU OK: ${CPU}%&quot;
fi

# --- RAM Alert ---
if [ &quot;$RAM&quot; -gt &quot;$THRESHOLD&quot; ]; then
  echo &quot;$CROSS [$DATE] WARNING: RAM at ${RAM}% (threshold: ${THRESHOLD}%)&quot; | tee -a &quot;$LOG_FILE&quot;
else
  echo &quot;$CHECK RAM OK: ${RAM}%&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runs in under a second. Zero dependencies. Works on Ubuntu, Debian, CentOS, RHEL, Arch — anything with &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;free&lt;/code&gt;, which is everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-the-cpu-check-actually-works&quot;&gt;How the CPU Check Actually Works&lt;/h3&gt;
&lt;p&gt;The CPU line looks intimidating, so let me walk through it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;top -bn1 | grep &quot;Cpu(s)&quot; | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1 | xargs printf &quot;%.0f&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;top -bn1&lt;/code&gt;&lt;/strong&gt; — runs &lt;code&gt;top&lt;/code&gt; in batch mode (&lt;code&gt;-b&lt;/code&gt;) for exactly one iteration (&lt;code&gt;-n1&lt;/code&gt;). Batch mode dumps the full output to stdout instead of opening the interactive TUI. This is the only way to use &lt;code&gt;top&lt;/code&gt; in a script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;grep &quot;Cpu(s)&quot;&lt;/code&gt;&lt;/strong&gt; — grabs the line that shows aggregate CPU stats.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;awk '{print $2}'&lt;/code&gt;&lt;/strong&gt; — pulls the user CPU percentage (the second field).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cut&lt;/code&gt; and &lt;code&gt;xargs printf&lt;/code&gt;&lt;/strong&gt; — strips the percent sign and any comma decimal separator, then rounds to an integer. You can't do integer comparison in bash with &lt;code&gt;2.5&lt;/code&gt; — it needs a clean number like &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The RAM check is simpler: &lt;code&gt;free&lt;/code&gt; shows total and used memory, and &lt;code&gt;awk&lt;/code&gt; divides used by total and multiplies by 100.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-thing-about-tee--a&quot;&gt;The Thing About &lt;code&gt;tee -a&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;You'll notice the script uses &lt;code&gt;echo ... | tee -a &quot;$LOG_FILE&quot;&lt;/code&gt; for warnings but plain &lt;code&gt;echo&lt;/code&gt; for healthy checks. This is intentional.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tee -a&lt;/code&gt; writes to the terminal AND appends to the log file simultaneously. When everything is fine, there's nothing to log — you don't want a log file full of &quot;CPU OK&quot; lines every hour for three years. You only want entries when something is actually wrong. So the log file becomes a clean history of every resource spike your server has had, with timestamps.&lt;/p&gt;
&lt;p&gt;When something breaks at 2 AM and you're debugging at 9 AM, you can &lt;code&gt;cat /var/log/resource-monitor.log&lt;/code&gt; and see exactly when resources started climbing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;schedule-it&quot;&gt;Schedule It&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hourly checks (what I use for most servers):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 * * * * /home/user/monitor.sh &amp;gt;&amp;gt; /var/log/monitor-cron.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every 5 minutes (for production servers where you need tighter visibility):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;*/5 * * * * /home/user/monitor.sh &amp;gt;&amp;gt; /var/log/monitor-cron.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not sure about the cron syntax? I have a &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-job-builder.html&quot;&gt;cron job builder tool&lt;/a&gt; that generates the line visually.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;variations-that-are-worth-adding&quot;&gt;Variations That Are Worth Adding&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Add an email alert when thresholds are breached:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;if [ &quot;$CPU&quot; -gt &quot;$THRESHOLD&quot; ]; then
  MSG=&quot;$CROSS WARNING: CPU at ${CPU}% on $(hostname) at $DATE&quot;
  echo &quot;$MSG&quot; | tee -a &quot;$LOG_FILE&quot;
  echo &quot;$MSG&quot; | mail -s &quot;[ALERT] High CPU on $(hostname)&quot; *Emails are not allowed*
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have a full &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-send-email-alert.html&quot;&gt;email alert script&lt;/a&gt; that covers the &lt;code&gt;mail&lt;/code&gt; setup if you haven't configured it before.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check disk space in the same script:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ &quot;$DISK&quot; -gt &quot;$THRESHOLD&quot; ]; then
  echo &quot;$CROSS [$DATE] WARNING: Disk at ${DISK}%&quot; | tee -a &quot;$LOG_FILE&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you've got CPU, RAM, and disk in one pass. I keep disk in a &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/disk-space-warning.html&quot;&gt;separate script&lt;/a&gt; because I use a different threshold for it (90% vs 80%), but combining them works fine if you want fewer cron entries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Log to a CSV for trending:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;$DATE,$CPU,$RAM&quot; &amp;gt;&amp;gt; /var/log/resource-history.csv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this for a week and you'll see patterns. Maybe your app spikes every day at 2 PM when a batch job runs. Maybe RAM creeps up 1% per day, which means you have a memory leak that'll hit the wall in three months. You can't see these patterns without historical data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-this-isn-t-enough&quot;&gt;When This Isn't Enough&lt;/h3&gt;
&lt;p&gt;This script is a notification system, not a monitoring platform. It tells you &quot;something is wrong right now&quot; but doesn't give you graphs, dashboards, or historical trending out of the box.&lt;/p&gt;
&lt;p&gt;If you need that level of visibility, tools like Netdata (free, runs locally) or Grafana + Prometheus are the next step. But for a single VPS or a handful of servers, a cron script that logs warnings and optionally emails you is 90% of what you need — and it takes 2 minutes to deploy instead of 2 hours.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Full script, line-by-line breakdown, cron setup, and more variations:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/monitor-cpu-ram-usage.html&quot;&gt;bashsnippets.xyz/snippets/monitor-cpu-ram-usage.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every script on the site is free, copy-paste ready, no signup required: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz&quot;&gt;bashsnippets.xyz&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/18494/your-server-is-at-97-cpu-right-now-would-you-know</guid>
<pubDate>Fri, 22 May 2026 11:00:00 +0000</pubDate>
</item>
<item>
<title>Changelog #8: Quota accuracy and deeper lead analyses</title>
<link>https://coderlegion.com/18007/changelog-8-quota-accuracy-and-deeper-lead-analyses</link>
<description>&lt;p&gt;Late April was mostly about counting tests the way your charts already do, giving sysadmin leads a denser board and a proper read-only analysis page, and wiring related tags so thin blog archives still send readers somewhere useful. We also lined up marketing anchors and small UI fixes so the path from a &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features&quot;&gt;feature page&lt;/a&gt; into signup or the waitlist behaves the same everywhere.&lt;/p&gt;&lt;h2&gt;Monthly quota now follows scored PageSpeed runs&lt;/h2&gt;&lt;p&gt;Your plan limit is PageSpeed tests per month. We only decrement the allowance when a run finishes with a Lighthouse performance score (the 0–100 headline number you see on site tests). HTTP failures, timeouts, or responses with no score no longer eat quota. That matches how we store a site test as worth charting only when there is a score to plot.&lt;/p&gt;&lt;h3&gt;Customer dashboard numbers&lt;/h3&gt;&lt;p&gt;On normal customer accounts (not sysadmin), the API usage strip at the top of the dashboard still shows three figures: successful runs today, this calendar month (we abbreviate thousands as “2.4k”), and errors today. Successful uses the same scored rows as quota; errors groups failed attempts so you can tell whether the lab or the URL is flaking without guessing from gaps in history. The strip still links to the day-by-day API usage table. The same ideas are spelled out for prospects on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/api-access-and-quota-visibility&quot;&gt;API access and quota visibility&lt;/a&gt; feature page.&lt;/p&gt;&lt;h3&gt;Pricing FAQ and product behaviour&lt;/h3&gt;&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;pricing&lt;/a&gt; page FAQ already described which runs count toward quota. Counters in the app now match that wording: success means a scored result you can act on, not a bare HTTP 200 with empty metrics.&lt;/p&gt;&lt;h3&gt;When a scheduled test has no performance score&lt;/h3&gt;&lt;p&gt;If automation gets a payload that cannot produce a score, we treat it as failed for quota and record an explicit failure instead of a row that looks healthy with no number. You spend less time hunting empty rows when PageSpeed Insights is intermittent or the URL blocks the lab.&lt;/p&gt;&lt;h2&gt;Leads board and full-page analysis&lt;/h2&gt;&lt;p&gt;The admin &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/leads-management-and-prospecting&quot;&gt;leads management and prospecting&lt;/a&gt; area is still in development with a small beta group. It is where agencies will run prospecting against sites that are not yet on a customer organisation: discovery jobs, scheduled lab tests, and the public report link you might send before someone becomes a paying tenant. The marketing page describes the intended workflow; the April changes already change how you work the queue inside the app.&lt;/p&gt;&lt;h3&gt;Index table: scan before you open a lead&lt;/h3&gt;&lt;p&gt;The index now shows enough signal on the first screen that you can sort and skim without opening every lead. Extra columns are on by default:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Pages discovered tells you how much of the site the crawler actually mapped (see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/automated-page-discovery&quot;&gt;automated page discovery&lt;/a&gt; for how that fits the wider product). A low number next to a large marketing site is often the first hint that auth walls, aggressive bot blocking, or a shallow sitemap will skew later scores.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Completed tests and pending tests compare what we have already run on mobile and desktop with what discovery says we should expect. Pending stays visible when a device lane has not caught up yet, so you are not guessing from a single “last tested” date in the row title.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Report shows the date of the latest public report plus a link that opens in a new tab, so you can sanity-check the customer-facing page before you paste it into an email.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Mobile and desktop render the latest analysis performance score as colour-coded badges: green from 90, amber from 50, red below, and “N/A” when we never got a scored run for that device.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Platform or CMS and priority remain in the column manager. Turn them on when you need taxonomy or routing metadata; leave them off when you want a wide table that still wraps long headers instead of crushing column widths.&lt;/p&gt;&lt;h3&gt;Lead analysis: same report layout as monitored sites&lt;/h3&gt;&lt;p&gt;Choosing View on a lead analysis opens a full-page, read-only record headed with the lead name and the URL we tested. Layout and sections match what you already know from monitored site tests: status and device, timestamps, a full-page or viewport screenshot when the stored response includes image data, and the headline 0–100 performance score.&lt;/p&gt;&lt;p&gt;Under that you get the same lab detail we surface for production monitoring: core timings (LCP, INP, CLS, and the rest of the Lighthouse set), optional Chrome UX Report field data when Google returns it, then diagnostics and optimisation opportunities, passed audits, and resource summaries. Nothing on the page is editable on purpose. When you are reconciling a PDF or a prospect email against what the crawler saw, you want a frozen snapshot of our store, not a second editor that could drift from the run you quoted.&lt;/p&gt;&lt;p&gt;If you already rely on the monitored-site test view for client conversations (the same framing as on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/core-web-vitals-monitoring&quot;&gt;Core Web Vitals monitoring&lt;/a&gt; page), you should not have to learn a parallel “lead-only” dialect. Same sections, same ordering, different object in the database.&lt;/p&gt;&lt;h2&gt;Blog archives and marketing pages&lt;/h2&gt;&lt;p&gt;Thin &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog&quot;&gt;blog&lt;/a&gt; tag pages can show related posts at the bottom when a tag only lists a few articles, so you still have onward reading. Across the public site, we cleaned up internal sign-up links, so you are less likely to land in the wrong place when you move from a post or &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features&quot;&gt;feature page&lt;/a&gt; into the waitlist or account flow.&lt;/p&gt;&lt;h2&gt;What to do next&lt;/h2&gt;&lt;p&gt;If you are on a paid plan (&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/pricing&quot;&gt;pricing&lt;/a&gt;), run your next batch of tests and watch the API usage strip: successful totals should move with the scores you see on site tests, and error spikes should show up the same day. If you work leads in admin, sort by pages discovered or completed tests before opening a single row. For the public overview of that workflow, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/features/leads-management-and-prospecting&quot;&gt;leads management and prospecting&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The next changelog will cover the first half of May.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/18007/changelog-8-quota-accuracy-and-deeper-lead-analyses</guid>
<pubDate>Fri, 15 May 2026 21:29:05 +0000</pubDate>
</item>
<item>
<title>LLM Observability tool</title>
<link>https://coderlegion.com/17391/llm-observability-tool</link>
<description>&lt;p&gt;I just created an open source LLM observability named &quot;TraceLM&quot; that can track the following activities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token cost&lt;/li&gt;
&lt;li&gt;token length&lt;/li&gt;
&lt;li&gt;latency information etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following tool can be found here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/unnivm/tracelm-java-agent&quot;&gt;https://github.com/unnivm/tracelm-java-agent&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This tools offers a very cool dashboard to track the above information. The user is required to login to access this dashboard.&lt;/p&gt;
&lt;p&gt;This tool currently supports only Java based applications as it is basically working based on instrumentation technique. But, this tool will be extended ti support other programming languages as well.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kteoibfghsc0qzzw1o3u.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;This project is under development and am going to add more features to to so that the idea is to make it an enterprise ready product.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17391/llm-observability-tool</guid>
<pubDate>Mon, 11 May 2026 06:50:41 +0000</pubDate>
</item>
<item>
<title>Watch your CrewAI agents in real-time with PiQrypt Vigil</title>
<link>https://coderlegion.com/15388/watch-your-crewai-agents-in-real-time-with-piqrypt-vigil</link>
<description>&lt;p&gt;You've added cryptographic audit trails to your crew (&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/piqrypt/how-i-added-cryptographic-audit-trails-to-any-crewai-crew-in-3-lines-2n6m&quot;&gt;part 1&lt;/a&gt;) &lt;br&gt;
and co-signed the handoffs between agents (&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/piqrypt/multi-agent-accountability-who-co-signs-the-handoff-between-your-crewai-agents-gi4&quot;&gt;part 2&lt;/a&gt;).   &lt;/p&gt;
&lt;p&gt;You now have tamper-evident history. But that's retrospective — you find out &lt;br&gt;
something went wrong after the fact, when you run &lt;code&gt;verify_chain()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Vigil is the real-time layer. It watches your agents while they run and raises &lt;br&gt;
alerts the moment behavioural anomalies appear in the chain.&lt;/p&gt;
&lt;h3 id=&quot;what-vigil-is&quot;&gt;What Vigil is&lt;/h3&gt;
&lt;p&gt;Vigil is a local HTTP dashboard that launches on &lt;code&gt;http://localhost:8421&lt;/code&gt;. &lt;br&gt;
No external dependency. No data leaves your machine. It reads the same &lt;br&gt;
AISS event chains your agents are writing and computes a VRS &lt;br&gt;
(Verifiable Risk Score) in real time.&lt;/p&gt;
&lt;p&gt;One command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install piqrypt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vigil is included. To start it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from vigil import start_vigil
start_vigil()
# Dashboard running at http://localhost:8421
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or from the CLI:  &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;piqrypt vigil start
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-vrs-measures&quot;&gt;What VRS measures&lt;/h3&gt;
&lt;p&gt;VRS is a composite risk score — a number between 0 and 1, where 1 means &lt;br&gt;
the chain is healthy and 0 means it's compromised. It's computed across &lt;br&gt;
five cryptographic dimensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integrity&lt;/strong&gt; — are all hashes in the chain consistent?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verified interactions&lt;/strong&gt; — are A2A handoffs co-signed as expected?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diversity&lt;/strong&gt; — is the event type distribution normal, or is the agent &lt;br&gt;
doing something anomalous?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finalization&lt;/strong&gt; — are events being properly closed?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotation health&lt;/strong&gt; — if the agent rotated its key, is the PCP continuity &lt;br&gt;
intact?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Critically: VRS is deterministic. No ML, no probabilistic scoring, no model &lt;br&gt;
that could drift. Given the same chain, any third party running the same &lt;br&gt;
verification gets the same score.&lt;/p&gt;
&lt;h3 id=&quot;a-full-crewai-setup-with-vigil&quot;&gt;A full CrewAI setup with Vigil&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from crewai import Crew, Task
from piqrypt.bridges.crewai import AuditedAgent as Agent
from vigil import start_vigil

# Start Vigil before the crew runs
start_vigil()  # → http://localhost:8421

researcher = Agent(
    role=&quot;Researcher&quot;,
    goal=&quot;Find competitive pricing data&quot;,
    backstory=&quot;Expert at finding and analyzing market data.&quot;,
    agent_name=&quot;researcher_01&quot;
)

writer = Agent(
    role=&quot;Writer&quot;,
    goal=&quot;Produce a pricing analysis report&quot;,
    backstory=&quot;Turns raw data into clear executive summaries.&quot;,
    agent_name=&quot;writer_01&quot;
)

research_task = Task(
    description=&quot;Research competitor pricing for product X&quot;,
    agent=researcher
)

write_task = Task(
    description=&quot;Write a pricing analysis based on the research&quot;,
    agent=writer
)

crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, write_task]
)

result = crew.kickoff()
# While the crew runs, Vigil shows VRS and chain health per agent in real time   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:8421&lt;/code&gt;. You'll see a dashboard per agent — event count, &lt;br&gt;
VRS, chain health, and a SOC-style timeline of events as they're stamped.&lt;/p&gt;
&lt;h3 id=&quot;what-a-critical-alert-looks-like&quot;&gt;What a CRITICAL alert looks like&lt;/h3&gt;
&lt;p&gt;If &lt;code&gt;researcher_01&lt;/code&gt;'s VRS drops below the critical threshold, Vigil raises an &lt;br&gt;
alert:&lt;br&gt;
[CRITICAL] researcher_01 — chain anomaly detected&lt;br&gt;
VRS: 0.21 (threshold: 0.30)&lt;br&gt;
Event #34: previous_hash mismatch&lt;br&gt;
Affected events: 34 → end of chain&lt;/p&gt;
&lt;p&gt;This means: someone (or something) modified event 34 after it was written. &lt;br&gt;
Every subsequent hash is now invalid. The chain is compromised from that &lt;br&gt;
point forward.&lt;/p&gt;
&lt;p&gt;Vigil alerts. It never acts. The human decides what to do — that's the &lt;br&gt;
design principle. Automated blocking without human decision is not in scope.&lt;/p&gt;
&lt;h3 id=&quot;a-real-example-the-pixelflow-agency&quot;&gt;A real example: the PixelFlow agency&lt;/h3&gt;
&lt;p&gt;The PiQrypt repo includes a runnable demo with a digital content agency —&lt;br&gt;
three CrewAI agents running on Claude Haiku, all monitored live in Vigil.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# From repo root
.\demos\start_families.ps1 pixelflow
# Opens Vigil automatically at http://localhost:8421
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three agents, three distinct behavioural profiles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pixelflow_content&lt;/code&gt; (Content Creator) — &lt;code&gt;watch&lt;/code&gt; profile, TSI drift signal.&lt;br&gt;
Runs 3 content sessions per day. Vigil tracks the drift in event diversity&lt;br&gt;
over time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pixelflow_seo&lt;/code&gt; (SEO Analyst) — &lt;code&gt;safe&lt;/code&gt; profile, A2C concentration signal.&lt;br&gt;
88% of its interactions go to &lt;code&gt;pixelflow_content&lt;/code&gt;. Vigil flags this&lt;br&gt;
concentration pattern.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pixelflow_scheduler&lt;/code&gt; (Social Scheduler) — &lt;code&gt;alert&lt;/code&gt; profile, temporal sync&lt;br&gt;
signal. Shares the same Claude Haiku backend as the other two — Vigil&lt;br&gt;
detects the timing correlation as a CRITICAL anomaly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three interact with external peers: &lt;code&gt;anthropic_api&lt;/code&gt;, &lt;code&gt;google_search_console&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;instagram_api&lt;/code&gt;, &lt;code&gt;twitter_api&lt;/code&gt;, &lt;code&gt;analytics_ga4&lt;/code&gt;. Every interaction is&lt;br&gt;
hash-chained. Every A2A handoff between agents carries a co-signed&lt;br&gt;
&lt;code&gt;interaction_hash&lt;/code&gt; in both chains.&lt;/p&gt;
&lt;p&gt;The demo runs in loop mode, injecting events every 5 seconds. Vigil&lt;br&gt;
recalculates VRS in real time. You can watch the &lt;code&gt;pixelflow_scheduler&lt;/code&gt;&lt;br&gt;
trust score degrade as the temporal sync pattern accumulates — without&lt;br&gt;
touching a single line of your crew definition.&lt;/p&gt;
&lt;p&gt;Full demo code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/piqrypt/piqrypt/demos&quot;&gt;github.com/piqrypt/piqrypt/demos&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;checking-chain-integrity-programmatically&quot;&gt;Checking chain integrity programmatically&lt;/h3&gt;
&lt;p&gt;You don't have to use the dashboard. You can verify a chain directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import piqrypt as aiss
from aiss.exceptions import InvalidChainError

events = aiss.load_events(&quot;researcher_01&quot;)

try:
    aiss.verify_chain(events)
    print(f&quot;Chain intact — {len(events)} events verified&quot;)
except InvalidChainError as e:
    print(f&quot;Chain compromised: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;verify_chain()&lt;/code&gt; is binary — it returns cleanly if the chain is intact,&lt;br&gt;
or raises &lt;code&gt;InvalidChainError&lt;/code&gt; the moment a broken link is found. No score,&lt;br&gt;
no partial result. The failure message tells you which link broke and why.&lt;/p&gt;
&lt;p&gt;The VRS score with thresholds (CRITICAL below 0.30, healthy above 0.70)&lt;br&gt;
is what Vigil computes continuously from the live chain and displays in&lt;br&gt;
the dashboard. These are two different tools for two different moments:&lt;br&gt;
&lt;code&gt;verify_chain()&lt;/code&gt; is a point-in-time integrity check you call explicitly;&lt;br&gt;
Vigil is the continuous watch that alerts you before you think to check.&lt;/p&gt;
&lt;h3 id=&quot;standard-vs-pro&quot;&gt;Standard vs Pro&lt;/h3&gt;
&lt;p&gt;The Vigil included in &lt;code&gt;pip install piqrypt&lt;/code&gt; (standard tier) gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full dashboard UI&lt;/li&gt;
&lt;li&gt;VRS with 7-day history&lt;/li&gt;
&lt;li&gt;CRITICAL alerts&lt;/li&gt;
&lt;li&gt;Up to 2 bridge types monitored&lt;/li&gt;
&lt;li&gt;Chain health per agent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vigil Pro adds 90-day VRS history, all alert levels (WARNING, INFO, DEBUG), &lt;br&gt;
unlimited bridge types, and TrustGate integration — the governance layer &lt;br&gt;
that intercepts agent actions before execution and routes to a human when &lt;br&gt;
policy requires it.&lt;/p&gt;
&lt;p&gt;For most development and staging use cases, the standard tier is enough.&lt;/p&gt;
&lt;h3 id=&quot;the-full-picture&quot;&gt;The full picture&lt;/h3&gt;
&lt;p&gt;Three articles, one integration pattern:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; — drop-in bridge, 3 lines, every agent action is now &lt;br&gt;
Ed25519-signed and hash-chained.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; — A2A co-signing, the handoffs between agents carry &lt;br&gt;
cryptographic proof that neither side can deny.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; — Vigil, real-time monitoring of chain health and behavioural &lt;br&gt;
anomalies while the crew runs.&lt;/p&gt;
&lt;p&gt;Together: your CrewAI crew has tamper-evident history, non-repudiable &lt;br&gt;
inter-agent accountability, and live anomaly detection. None of it requires &lt;br&gt;
a server, an account, or sending data anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Part 1:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/piqrypt/how-i-added-cryptographic-audit-trails-to-any-crewai-crew-in-3-lines-2n6m&quot;&gt;How I added cryptographic audit trails to any CrewAI crew in 3 lines&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Part 2:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/piqrypt/multi-agent-accountability-who-co-signs-the-handoff-between-your-crewai-agents-gi4&quot;&gt;Multi-agent accountability: who co-signs the handoff between your CrewAI agents?&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/piqrypt/piqrypt&quot;&gt;github.com/piqrypt/piqrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pip install piqrypt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Protocol spec (MIT): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/piqrypt/aiss-standard&quot;&gt;github.com/piqrypt/aiss-standard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15388/watch-your-crewai-agents-in-real-time-with-piqrypt-vigil</guid>
<pubDate>Tue, 21 Apr 2026 11:45:45 +0000</pubDate>
</item>
<item>
<title>Your API is down - now what? Capturing failure context in Node.js</title>
<link>https://coderlegion.com/15152/your-api-is-down-now-what-capturing-failure-context-in-node-js</link>
<description>&lt;h3 id=&quot;most-monitoring-tools-tell-you-one-thing&quot;&gt;Most monitoring tools tell you one thing&lt;/h3&gt;
&lt;p&gt;&quot;Your API is down.&quot;&lt;/p&gt;
&lt;p&gt;That's useful. But only partially.&lt;/p&gt;
&lt;p&gt;I wanted to go beyond &quot;it's down&quot; and understand &lt;strong&gt;why&lt;/strong&gt; — specifically, &lt;strong&gt;where in the request lifecycle things actually broke&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-frustrating-part-of-debugging-failures&quot;&gt;The frustrating part of debugging failures&lt;/h3&gt;
&lt;p&gt;The first time this really hit me, I spent almost an hour digging through logs after a 3am alert — only to realize the issue had already disappeared. No trace of what went wrong. Just a gap in the metrics and a resolved status.&lt;/p&gt;
&lt;p&gt;The typical workflow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You get an alert&lt;/li&gt;
&lt;li&gt;You SSH into your server&lt;/li&gt;
&lt;li&gt;You check logs&lt;/li&gt;
&lt;li&gt;You try to reproduce the issue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And in many cases… the issue is already gone.&lt;/p&gt;
&lt;p&gt;The failure might have lasted only a few seconds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A DNS resolution issue&lt;/li&gt;
&lt;li&gt;A TLS handshake problem&lt;/li&gt;
&lt;li&gt;A temporary upstream timeout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the time you investigate, there's no trace left.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;logs-don-t-always-tell-the-full-story&quot;&gt;Logs don't always tell the full story&lt;/h3&gt;
&lt;p&gt;Logs are helpful, but they have real limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They only capture what your application explicitly logs&lt;/li&gt;
&lt;li&gt;They often miss network-level issues entirely&lt;/li&gt;
&lt;li&gt;They don't show how a request failed step by step&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I kept missing was the &lt;strong&gt;network-level picture&lt;/strong&gt; — not what the app logged, but what actually happened between the client and the server at each phase of the request.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;breaking-down-an-http-request&quot;&gt;Breaking down an HTTP request&lt;/h3&gt;
&lt;p&gt;When you make a request to an API, a lot happens under the hood:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DNS lookup&lt;/strong&gt; — translating the domain name into an IP address&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP connection&lt;/strong&gt; — establishing a connection to the server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS handshake&lt;/strong&gt; — negotiating encryption (for HTTPS)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server processing&lt;/strong&gt; — the server does its work and prepares a response&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First byte received&lt;/strong&gt; — also called TTFB (Time To First Byte), this is the total time from sending the request to getting the first byte back. It's not a separate step — it's a cumulative measurement across all the steps above.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If any one of these steps fails or slows down, the entire request is affected.&lt;/p&gt;
&lt;p&gt;But most monitoring tools treat the request as a black box. You get a status code. Sometimes a timeout. Rarely anything more.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-idea-capture-the-failure-at-the-moment-it-happens&quot;&gt;The idea: capture the failure at the moment it happens&lt;/h3&gt;
&lt;p&gt;Instead of trying to debug after the failure has resolved itself...&lt;/p&gt;
&lt;p&gt;What if we could capture useful diagnostic signals &lt;strong&gt;during&lt;/strong&gt; the failure?&lt;/p&gt;
&lt;p&gt;So I started experimenting with capturing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS lookup timing&lt;/li&gt;
&lt;li&gt;TLS handshake duration&lt;/li&gt;
&lt;li&gt;Time to first byte&lt;/li&gt;
&lt;li&gt;The first part of the response body&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the exact moment a request fails.&lt;/p&gt;
&lt;p&gt;I ended up building a small tool around this idea.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-actually-changes&quot;&gt;What this actually changes&lt;/h3&gt;
&lt;p&gt;To see why this matters, here's what a typical monitoring alert looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ALERT: API DOWN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
endpoint:  api.myapp.io/payments
status:    DOWN
started:   2026-03-26 09:05:25
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's everything you get. Now you start the investigation — SSH, logs, curl, check DNS, check TLS. By the time you look, the evidence is usually gone.&lt;/p&gt;
&lt;p&gt;Now here's what it looks like when you capture diagnostic data at each failure. The tool waits for three consecutive failures before alerting (more on why below), and captures a snapshot at each one silently:&lt;/p&gt;
&lt;h4 id=&quot;example-a-server-degradation-into-timeout&quot;&gt;Example A — Server degradation into timeout&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;--- SNAPSHOT #1 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  dns_lookup       1ms                       (~estimated)
  tls_handshake    5ms                   (SSL negotiation)
  ttfb             4,007ms                     ↑ critical
                   server is very slow or overloaded
  total_time       4,312ms
───────────────────────────────────────────────────────────

--- SNAPSHOT #2 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  dns_lookup       2ms                       (~estimated)
  tls_handshake    4ms                   (SSL negotiation)
  ttfb             10,002ms                    ↑ critical
                   server is very slow or overloaded
  total_time       10,374ms                      ↑ worse
───────────────────────────────────────────────────────────

--- SNAPSHOT #3 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  error            TIMEOUT
                   Your server did not respond within
                   the allowed time. May be overloaded,
                   crashed, or unreachable.
  total_time       10,000ms
───────────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;TTFB went from 4,007ms to 10,002ms to a full timeout. The server was degrading under load until it stopped responding entirely. &lt;strong&gt;You can see the failure developing across three snapshots.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;example-b-upstream-dependency-failure&quot;&gt;Example B — Upstream dependency failure&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;--- SNAPSHOT #1 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  dns_lookup       3ms                       (~estimated)
  tls_handshake    12ms                  (SSL negotiation)
  ttfb             1,847ms                        ↑ high
  status_code      502
                   Bad Gateway — upstream service
                   returned an invalid response
  response_body    {&quot;error&quot;:&quot;upstream_timeout&quot;,
                    &quot;service&quot;:&quot;payment-gateway&quot;}
───────────────────────────────────────────────────────────

--- SNAPSHOT #2 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  ttfb             1,923ms                        ↑ high
  status_code      502
  response_body    {&quot;error&quot;:&quot;upstream_timeout&quot;,
                    &quot;service&quot;:&quot;payment-gateway&quot;}
───────────────────────────────────────────────────────────

--- SNAPSHOT #3 ---                         ap-southeast-1
───────────────────────────────────────────────────────────
  ttfb             2,104ms                        ↑ high
  status_code      502
  response_body    {&quot;error&quot;:&quot;upstream_timeout&quot;,
                    &quot;service&quot;:&quot;payment-gateway&quot;}

  ┌─────────────────────────────────────────────────────┐
  │  DNS ✓  TLS ✓  Server responded — upstream failed.  │
  │  Payment gateway timed out. Your server is fine.     │
  └─────────────────────────────────────────────────────┘
───────────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;DNS fine. TLS fine. Your server responded — but the payment gateway behind it timed out. &lt;strong&gt;No SSH session required.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The difference: instead of starting an investigation after the alert, the evidence is already there when the alert arrives.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-tricky-part-getting-this-data-in-node-js&quot;&gt;The tricky part: getting this data in Node.js&lt;/h3&gt;
&lt;p&gt;This turned out to be harder than expected.&lt;/p&gt;
&lt;p&gt;Most high-level APIs like &lt;code&gt;fetch&lt;/code&gt; don't expose DNS timing or TLS handshake duration at all. They give you a response, but no visibility into the individual phases of how that response was obtained.&lt;/p&gt;
&lt;p&gt;To get this data, I ended up using &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nodejs/undici&quot;&gt;&lt;code&gt;undici&lt;/code&gt;&lt;/a&gt; (the HTTP client that powers Node.js &lt;code&gt;fetch&lt;/code&gt; under the hood) combined with Node.js &lt;code&gt;diagnostics_channel&lt;/code&gt;. This lets you subscribe to events that fire at specific moments during a request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import diagnostics_channel from 'node:diagnostics_channel'

const requestStartMs = Date.now()
let connectStartMs = null
let connectEndMs = null
let headersMs = null

diagnostics_channel.subscribe('undici:client:beforeConnect', () =&amp;gt; {
  connectStartMs = Date.now()
})

diagnostics_channel.subscribe('undici:client:connected', () =&amp;gt; {
  connectEndMs = Date.now()  // TCP + TLS handshake complete
})

diagnostics_channel.subscribe('undici:request:headers', () =&amp;gt; {
  headersMs = Date.now()  // First byte received
})

// After the request completes:
// connectEndMs - connectStartMs = real TCP + TLS handshake time
// headersMs - requestStartMs     = real TTFB (total time to first byte)
// Remember to unsubscribe after the request completes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each metric actually gives you — and whether it's a real measurement or an estimate:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Source &lt;/th&gt;&lt;th&gt;Real or estimated?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;connect_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;undici:client:connected&lt;/code&gt; - &lt;code&gt;beforeConnect&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Real (TCP + TLS)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ttfb_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;undici:request:headers&lt;/code&gt; - request start &lt;/td&gt;&lt;td&gt;Real&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;total_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Date.now()&lt;/code&gt; delta &lt;/td&gt;&lt;td&gt;Real&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;tls_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Rough split of connect_ms (HTTPS only) &lt;/td&gt;&lt;td&gt;Estimated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;tcp_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Rough split of connect_ms &lt;/td&gt;&lt;td&gt;Estimated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;dns_ms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Derived heuristic &lt;/td&gt;&lt;td&gt;Estimated&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A few things worth noting honestly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DNS timing is estimated, not exact.&lt;/strong&gt; Node.js doesn't expose real DNS resolution timing through &lt;code&gt;diagnostics_channel&lt;/code&gt;. DNS resolution happens before the &lt;code&gt;beforeConnect&lt;/code&gt; event fires, so there's no direct event to hook into. The values I capture are heuristic-based — useful for spotting anomalies, but not precise measurements. That's why the examples above show &lt;code&gt;(~estimated)&lt;/code&gt; next to DNS values.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;diagnostics_channel&lt;/code&gt; events are semi-internal.&lt;/strong&gt; They work well today, but undici doesn't guarantee these channel names will stay stable across Node.js versions. It's a tradeoff — you get real timing data, but you're relying on an API that could change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The capture is a separate request.&lt;/strong&gt; When a check fails, the tool makes a second HTTP request using &lt;code&gt;undici&lt;/code&gt; to capture the timing breakdown. This means the snapshot might not match the original failure exactly — the server could have recovered, or the error could be different. It's a real tradeoff I'm still thinking about. The alternative would be to instrument the original request, but that adds complexity to the main check path.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted real timing data, even if it meant accepting these tradeoffs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;one-more-design-decision-don-t-alert-on-the-first-failure&quot;&gt;One more design decision: don't alert on the first failure&lt;/h3&gt;
&lt;p&gt;As you saw in the examples above, the tool waits for &lt;strong&gt;three consecutive failures&lt;/strong&gt; before sending an alert — and captures a snapshot at each one silently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Check 1 fails → snapshot #1 captured silently
Check 2 fails → snapshot #2 captured silently
Check 3 fails → snapshot #3 captured + alert sent with all three
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why three? It's enough to filter out transient blips (a single DNS hiccup, a momentary network glitch), but short enough to catch real outages within minutes at typical check intervals.&lt;/p&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fewer false alarms&lt;/strong&gt; — transient issues don't wake you up at 3am&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A progression timeline&lt;/strong&gt; — you can see the failure developing across three snapshots (like the degradation in Example A above)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evidence arrives with the alert&lt;/strong&gt; — all three snapshots are attached before you even open it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recovery is handled too&lt;/strong&gt; — two consecutive successes resolve the incident and send a recovery notification with the total downtime duration&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-small-shift-in-thinking&quot;&gt;A small shift in thinking&lt;/h3&gt;
&lt;p&gt;What changed for me was this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Don't just detect failures — capture context when they happen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That one shift made debugging much faster and less frustrating. By the time the alert arrives, the evidence is already there.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;still-exploring&quot;&gt;Still exploring&lt;/h3&gt;
&lt;p&gt;There are still open questions here — especially around DNS timing accuracy and how closely the snapshot matches the original failure when it's captured as a separate request.&lt;/p&gt;
&lt;p&gt;I'm curious how others handle this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you usually debug short-lived or transient API failures?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Do you rely on logs, APM tools, distributed tracing, or something else entirely? Have you found a good way to capture context at the moment of failure rather than investigating after the fact?&lt;/p&gt;
&lt;p&gt;Would love to hear what works for you.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15152/your-api-is-down-now-what-capturing-failure-context-in-node-js</guid>
<pubDate>Sun, 19 Apr 2026 05:55:20 +0000</pubDate>
</item>
<item>
<title>Your Website Looks Fine. This Free Health Scanner Disagrees.</title>
<link>https://coderlegion.com/15041/your-website-looks-fine-this-free-health-scanner-disagrees</link>
<description>&lt;p&gt;Most websites have problems their owners don't know about. Not the dramatic kind that crashes a server. The quiet kind — a broken script, an expiring SSL certificate, missing security headers, images that silently fail to load.&lt;/p&gt;
&lt;p&gt;These issues don't trigger uptime alerts. They just make the experience worse for visitors while every dashboard says &quot;all green.&quot;&lt;/p&gt;
&lt;p&gt;We built a free scanner that finds them in 20 seconds: &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://getsitewatch.com/scan?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=scanner&quot;&gt;getsitewatch.com/scan&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No signup. No email. Just paste a URL and get a full health report.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-checks&quot;&gt;What It Checks&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Broken Assets&lt;/strong&gt; — Every image, script, and stylesheet your page references. Do they actually load, or do they return 404? A missing JavaScript file can break your entire page while the server happily returns 200 OK.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSL Certificates&lt;/strong&gt; — Is your cert valid? When does it expire? Is the chain complete? An incomplete chain means your site works in Chrome but throws warnings in Safari and on mobile. 88% of companies have had an outage from an expired cert in the past two years.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mixed Content&lt;/strong&gt; — Does your HTTPS page load anything over HTTP? Browsers block this silently. No error. Images just don't render. Common on WordPress sites after an SSL migration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security Headers&lt;/strong&gt; — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy. Most sites are missing at least two. These are basic protections against XSS and clickjacking that take 15 minutes to set up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schema &amp;amp; SEO&lt;/strong&gt; — JSON-LD validation, Open Graph tags, canonical URLs. If these are broken, your links look bare when shared and Google can't generate rich snippets for your pages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discoverability&lt;/strong&gt; — Does your robots.txt exist? Is there a sitemap? Surprisingly often the answer is no, especially on SPA and Jamstack sites.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-typically-find&quot;&gt;What We Typically Find&lt;/h3&gt;
&lt;p&gt;We've run this on hundreds of sites. The pattern is consistent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Almost every site is missing security headers&lt;/li&gt;
&lt;li&gt;Roughly 3 out of 10 have at least one broken asset&lt;/li&gt;
&lt;li&gt;SSL issues are more common than anyone expects&lt;/li&gt;
&lt;li&gt;SEO metadata is frequently broken or missing after CMS updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of these sites look perfectly fine when you open them in a browser. The problems are invisible until you actually check.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;The scanner fetches your page, enumerates every subresource, validates each one returns a successful response, inspects headers for security policies, checks SSL certificate validity and chain completeness, and validates structured data and SEO metadata.&lt;/p&gt;
&lt;p&gt;Results come back as a prioritized report: critical issues first, info-level findings last. Every finding includes a plain-language explanation of what it means and how to fix it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-1&quot;&gt;Try It&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://getsitewatch.com/scan?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=scanner&quot;&gt;getsitewatch.com/scan&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;20 seconds. Completely free. &lt;/p&gt;
&lt;p&gt;If you want these checks to run continuously and alert you when something breaks, that's what &lt;a rel=&quot;nofollow&quot; href=&quot;https://getsitewatch.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=scanner&quot;&gt;Sitewatch&lt;/a&gt; does — website monitoring that catches what uptime tools miss. But the one-time scan is yours with no strings attached.&lt;/p&gt;
&lt;p&gt;Curious what your site's health rating looks like.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15041/your-website-looks-fine-this-free-health-scanner-disagrees</guid>
<pubDate>Fri, 17 Apr 2026 07:06:19 +0000</pubDate>
</item>
</channel>
</rss>