<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Reliability on ErrorZap</title><link>https://errorzap.com/tags/reliability/</link><description>Recent content in Reliability on ErrorZap</description><image><title>ErrorZap</title><url>https://errorzap.com/og.png</url><link>https://errorzap.com/og.png</link></image><generator>Hugo</generator><language>en-US</language><copyright>ErrorZap</copyright><lastBuildDate>Sat, 07 Nov 2026 09:00:00 -0700</lastBuildDate><atom:link href="https://errorzap.com/tags/reliability/index.xml" rel="self" type="application/rss+xml"/><item><title>Proof, Not Green Lights</title><link>https://errorzap.com/posts/proof-not-green-lights/</link><pubDate>Sat, 07 Nov 2026 09:00:00 -0700</pubDate><guid>https://errorzap.com/posts/proof-not-green-lights/</guid><description>APIs, queues, processes, builds, and dashboards kept telling me yes to questions I had not actually asked.</description><content:encoded><![CDATA[<p>Green is a dangerous color in software.</p>
<p>The API returns 200. The remote command says accepted. The print queue empties. The service process is running. The backup job completes. The build passes. Every dashboard is green, and the thing a person cares about still did not happen.</p>
<p>This is not because the indicators are fake. It is because they answer smaller questions.</p>
<h2 id="every-layer-proves-itself">Every layer proves itself</h2>
<p>Consider a remote print job:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>request accepted -&gt; job queued -&gt; spooler released -&gt; printer received -&gt; paper emerged
</span></span></code></pre></div><p>An empty queue proves the spooler no longer owns the job. It does not prove the printer had paper, the label stock was loaded correctly, the output was legible, or the page landed in the expected room.</p>
<p>The same collapse happens everywhere:</p>
<ul>
<li>a backup job proves data was written, not that restore works;</li>
<li>a process proves code is running, not that the service is useful;</li>
<li>a deployment proves bytes reached a target, not that traffic reaches them;</li>
<li>a SIP phone rings, but the human may hear nothing;</li>
<li>a smart light reports <code>on</code> at one-percent brightness and appears dark.</li>
</ul>
<p>The green light belongs to one layer.</p>
<h2 id="ask-what-a-human-would-observe">Ask what a human would observe</h2>
<p>The simplest way to improve acceptance tests is to finish this sentence:</p>
<blockquote>
<p>If this worked, a person at the destination would observe ______.</p>
</blockquote>
<p>Then design the test around that observation.</p>
<p>For a website: the new page loads from the public URL on phone and desktop, with the expected content and assets.</p>
<p>For audio: two humans hear each other in both directions.</p>
<p>For a restore: the isolated copy mounts, starts when authorized, serves expected data, and can be failed back without contaminating production.</p>
<p>For an alert: the intended person receives one actionable notification and one recovery—not merely an API receipt.</p>
<h2 id="keep-the-intermediate-lights">Keep the intermediate lights</h2>
<p>Intermediate signals are valuable. They locate failures.</p>
<p>If a print request was never accepted, do not troubleshoot the printer tray. If the process is dead, do not start with browser caching. Layered evidence is how we move from vague failure to exact boundary.</p>
<p>The mistake is promoting an intermediate success into a final claim.</p>
<p>A useful acceptance ledger looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>prepared: artifact exists
</span></span><span style="display:flex;"><span>acted: delivery attempted
</span></span><span style="display:flex;"><span>accepted: target system acknowledged
</span></span><span style="display:flex;"><span>verified: target state read back
</span></span><span style="display:flex;"><span>observed: user-visible outcome confirmed
</span></span></code></pre></div><p>Not every task requires all five. The labels prevent us from pretending it did.</p>
<h2 id="honest-partials-are-faster">Honest partials are faster</h2>
<p>There is a strange pressure in technical work to declare victory at the first green light. It feels efficient. In reality, it creates reopened work, confusing handoffs, and repeated troubleshooting.</p>
<p>&ldquo;The print job left the queue; physical output is unverified&rdquo; is more useful than &ldquo;printed.&rdquo;</p>
<p>&ldquo;The service is active and its health endpoint is ready; no real user flow was tested&rdquo; is a complete operational statement.</p>
<p>Precision is not pessimism. It tells the next person exactly where truth ends.</p>
<p>Green lights are clues. Proof is the outcome that survives the final mile.</p>
]]></content:encoded></item><item><title>Last Good Data Is a Feature</title><link>https://errorzap.com/posts/last-good-data-is-a-feature/</link><pubDate>Wed, 04 Nov 2026 09:00:00 -0700</pubDate><guid>https://errorzap.com/posts/last-good-data-is-a-feature/</guid><description>When a public-data source breaks, replacing yesterday&amp;rsquo;s truth with today&amp;rsquo;s garbage is not freshness. It is vandalism with a timestamp.</description><content:encoded><![CDATA[<p>I used to think a data collector had two states: it ran or it failed.</p>
<p>Then I started collecting from public websites.</p>
<p>A public source can return HTTP 200 and still be useless. It can serve a login page where a table used to be. It can return only the first page, repeat yesterday&rsquo;s records with new timestamps, move columns, invent empty dates, or decide that a polite automated request looks suspicious. The transport succeeded. The data did not.</p>
<p>The most dangerous collector is not the one that crashes. It is the one that confidently replaces a known-good dataset with a plausible pile of nonsense.</p>
<h2 id="fresh-is-not-the-same-as-true">Fresh is not the same as true</h2>
<p>Imagine a local information page with 8,000 verified records. Tonight&rsquo;s refresh retrieves 14 rows because the source changed pagination. A naive pipeline writes those 14 rows over the old file and celebrates a successful run.</p>
<p>By morning, the website is technically current and practically destroyed.</p>
<p>The safer pattern keeps separate objects:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>incoming candidate
</span></span><span style="display:flex;"><span>       |
</span></span><span style="display:flex;"><span>       v
</span></span><span style="display:flex;"><span>validate structure, counts, dates, uniqueness
</span></span><span style="display:flex;"><span>       |
</span></span><span style="display:flex;"><span>       +---- bad ----&gt; quarantine + alert
</span></span><span style="display:flex;"><span>       |
</span></span><span style="display:flex;"><span>       +---- good ---&gt; atomically promote to current
</span></span><span style="display:flex;"><span>                              |
</span></span><span style="display:flex;"><span>                              +--&gt; preserve previous known-good
</span></span></code></pre></div><p>The last-good dataset is not merely a backup. It is part of the product contract.</p>
<p>If tonight&rsquo;s import fails validation, readers should see yesterday&rsquo;s honest data plus a visible freshness warning. They should not see an empty page pretending that nothing exists.</p>
<h2 id="validation-should-be-rude">Validation should be rude</h2>
<p>Collectors need to distrust their source in specific, testable ways.</p>
<p>Useful gates include:</p>
<ul>
<li>required columns are still present;</li>
<li>row count did not collapse unexpectedly;</li>
<li>identifiers remain unique;</li>
<li>dates are parseable and not absurdly in the future;</li>
<li>detail-page coverage stays within a known range;</li>
<li>a sample of old records still exists;</li>
<li>content is not an error page wearing a 200 status code.</li>
</ul>
<p>None of these proves perfection. Together they make silent catastrophe much harder.</p>
<p>The thresholds should also be explainable. &ldquo;Reject if the row count falls more than 40 percent&rdquo; is debatable but testable. &ldquo;The page looked weird&rdquo; is neither.</p>
<h2 id="stale-should-be-visible">Stale should be visible</h2>
<p>Preserving last-good data creates a new obligation: do not pass stale data off as fresh.</p>
<p>Every public-facing dataset should carry a small status card:</p>
<ul>
<li>last successful source retrieval;</li>
<li>last successful validation;</li>
<li>current row count;</li>
<li>whether detail coverage is complete or partial;</li>
<li>number of quarantined records;</li>
<li>link to the original source;</li>
<li>plain-language caveat when the source is blocked or changing.</li>
</ul>
<p>That little card is more useful than a hundred lines of invisible logging. It tells readers what the system knows about itself.</p>
<h2 id="failure-can-be-a-product-feature">Failure can be a product feature</h2>
<p>Most software demos focus on the happy path: click Refresh, watch new rows appear. Real reliability lives in the ugly path: the source returns less, later, malformed, duplicated, or nothing at all.</p>
<p>The mature response is not heroic scraping. It is restraint.</p>
<p>Keep yesterday&rsquo;s truth. Quarantine today&rsquo;s suspicious changes. Explain the age of the evidence. Try again later without turning one upstream failure into a downstream lie.</p>
<p>Sometimes the most important new data is the decision not to replace the old data.</p>
]]></content:encoded></item><item><title>Bad Rows Go to Quarantine</title><link>https://errorzap.com/posts/bad-rows-go-to-quarantine/</link><pubDate>Tue, 03 Nov 2026 09:00:00 -0700</pubDate><guid>https://errorzap.com/posts/bad-rows-go-to-quarantine/</guid><description>One malformed record should become one visible exception, not a reason to poison or stop an entire daily import.</description><content:encoded><![CDATA[<p>Batch jobs have a talent for turning one weird row into a full-day emergency.</p>
<p>The source sends a date that does not exist. A business name contains a character nobody tested. One detail page returns half an HTML document. A record claims it was updated in the year 2099. The importer reaches that row, throws an exception, and leaves the whole dataset stuck at yesterday.</p>
<p>The usual reaction is to make the parser more forgiving. That can be worse. A parser that accepts everything eventually turns corruption into official data.</p>
<p>The better answer is quarantine.</p>
<h2 id="a-bad-record-is-not-a-bad-run">A bad record is not a bad run</h2>
<p>A robust importer treats records as individual claims.</p>
<p>Each claim passes through structural, semantic, and consistency checks. Good records continue. Suspicious records move to a separate holding area with the reason they were rejected.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>record 4172
</span></span><span style="display:flex;"><span>  identifier: present
</span></span><span style="display:flex;"><span>  date: 2099-31-44       X invalid
</span></span><span style="display:flex;"><span>  required fields: pass
</span></span><span style="display:flex;"><span>  action: quarantine
</span></span><span style="display:flex;"><span>  reason: impossible date
</span></span></code></pre></div><p>The run can finish with 7,999 accepted records and one quarantined record. That is not a hidden partial success. It is an explicit result with a number, a reason, and something a human can inspect.</p>
<h2 id="quarantine-is-not-the-trash">Quarantine is not the trash</h2>
<p>Rejected records must remain recoverable. The point is to prevent them from contaminating the public dataset while preserving enough evidence to repair the rule or correct the source later.</p>
<p>A useful quarantine entry includes:</p>
<ul>
<li>source identifier and source URL when safe;</li>
<li>retrieval time;</li>
<li>validation rule that failed;</li>
<li>bounded copy or hash of the raw input;</li>
<li>parser version;</li>
<li>whether the record existed in the previous dataset;</li>
<li>review status and resolution.</li>
</ul>
<p>Do not dump an entire credential-bearing response into a log. Quarantine should preserve evidence, not create a second security incident.</p>
<h2 id="the-totals-must-add-up">The totals must add up</h2>
<p>The fastest way to lose trust in a pipeline is to make records disappear between counters.</p>
<p>Every run should satisfy a reconciliation equation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>retrieved = accepted + unchanged + quarantined + explicitly skipped
</span></span></code></pre></div><p>If it does not, the run is incomplete.</p>
<p>That equation sounds obvious. It is also where many pipelines fail. They log &ldquo;processed 8,000&rdquo; at the start, &ldquo;saved 7,997&rdquo; at the end, and never explain the missing three.</p>
<p>Thin logs turn those three records into ghosts. A complete ledger turns them into work.</p>
<h2 id="tomorrow-should-not-inherit-todays-wedge">Tomorrow should not inherit today&rsquo;s wedge</h2>
<p>Quarantine also protects future runs.</p>
<p>If one malformed row stays in the upstream source for a week, a crash-on-first-error collector will fail seven days in a row. A quarantine-aware collector will flag the same record, deduplicate the alert, and continue collecting everything else. When the source fixes it, the row can re-enter through the normal validation path.</p>
<p>The operational difference is enormous. One is a brittle daily ceremony. The other is a system that can live in the real world.</p>
<p>Bad rows are inevitable. Silent rows are dangerous. Quarantined rows are manageable.</p>
]]></content:encoded></item><item><title>The Public Website That Blocked the Polite Bot</title><link>https://errorzap.com/posts/the-public-website-that-blocked-the-polite-bot/</link><pubDate>Mon, 02 Nov 2026 17:15:00 -0700</pubDate><guid>https://errorzap.com/posts/the-public-website-that-blocked-the-polite-bot/</guid><description>A public data portal rejected an ordinary collector, and the right response was better failure handling—not a proxy-rotation arms race.</description><content:encoded><![CDATA[<p>The information was public. The website was reachable. The collector was slow, identified, and boring.</p>
<p>It still got blocked.</p>
<p>That moment divides scraping projects into two kinds. One kind asks, &ldquo;How do we get around this?&rdquo; The other asks, &ldquo;What is the responsible product behavior when the source says no?&rdquo;</p>
<p>I want to build the second kind.</p>
<h2 id="public-does-not-mean-permissionless">Public does not mean permissionless</h2>
<p>A page visible in a browser may sit behind rate controls, session logic, terms, copyright, privacy obligations, or infrastructure that was never designed for automated access. A government label does not automatically erase those constraints. Neither does civic usefulness.</p>
<p>Before collecting, identify:</p>
<ul>
<li>the official source and any documented API or export;</li>
<li>terms and licensing for reuse, not merely viewing;</li>
<li>robots.txt and any published crawl or rate guidance;</li>
<li>update cadence;</li>
<li>contact or data-request path;</li>
<li>personal information that should not be republished;</li>
<li>a reasonable request rate and cache policy.</li>
</ul>
<p>If a structured export exists, use it. If permission is required, ask. If the data can be linked without copying, consider whether that is enough.</p>
<h2 id="a-block-is-a-state-not-a-challenge">A block is a state, not a challenge</h2>
<p>When a collector receives a 403, a 429, a challenge page, or a suddenly empty response, it should classify the event and stop escalating.</p>
<p>The bad pattern rotates headers, IPs, accounts, and browser fingerprints until something slips through. That is expensive, fragile, and hostile. It also converts a data-quality problem into an adversarial-access problem.</p>
<p>The safer pattern is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>detect block
</span></span><span style="display:flex;"><span>  -&gt; stop the batch (and honor any Retry-After)
</span></span><span style="display:flex;"><span>  -&gt; preserve last-good data
</span></span><span style="display:flex;"><span>  -&gt; record bounded evidence
</span></span><span style="display:flex;"><span>  -&gt; alert once
</span></span><span style="display:flex;"><span>  -&gt; use official contact/export path
</span></span></code></pre></div><p>Do not let retries become a denial-of-service attack with good intentions.</p>
<h2 id="design-the-site-for-source-failure">Design the site for source failure</h2>
<p>If a public-facing service depends on someone else&rsquo;s portal, source unavailability is normal architecture.</p>
<p>The user experience should expose:</p>
<ul>
<li>last successful update time;</li>
<li>original source link;</li>
<li>current collection status;</li>
<li>whether displayed data is complete or partial;</li>
<li>what is being done to restore the feed;</li>
<li>correction path.</li>
</ul>
<p>That turns a mysterious stale page into an honest service.</p>
<h2 id="keep-the-collector-replaceable">Keep the collector replaceable</h2>
<p>Source HTML changes. Portals get replaced. APIs appear and disappear. A collector should separate retrieval, parsing, validation, storage, and presentation so a new lawful source path does not require rebuilding everything.</p>
<p>Store normalized records with provenance. Preserve the original source identifier. Keep transformations documented. Test parsers against saved, non-sensitive fixtures rather than hammering the live site during development.</p>
<p>The collector is not the public product. Trust is.</p>
<h2 id="politeness-includes-accepting-no">Politeness includes accepting no</h2>
<p>There is a technical culture that treats every block as a puzzle. Sometimes the clever solution is to back off, keep known-good information available, and make a formal request for a better feed.</p>
<p>The portal blocking the bot did not end the project. It clarified the contract the project needed: ethical retrieval, visible freshness, last-good preservation, and no secret war against the source.</p>
<p>A public-interest tool should not have to behave like malware to stay current.</p>
]]></content:encoded></item><item><title>The PWA That DDoSed Itself With Polling</title><link>https://errorzap.com/posts/the-pwa-that-ddosed-itself-with-polling/</link><pubDate>Mon, 02 Nov 2026 17:00:00 -0700</pubDate><guid>https://errorzap.com/posts/the-pwa-that-ddosed-itself-with-polling/</guid><description>I asked a tiny dashboard for reassurance ten times a second. It answered by manufacturing its own outage.</description><content:encoded><![CDATA[<p>The dashboard was supposed to make the system feel calmer.</p>
<p>One screen. A few useful numbers. A green dot when the service was reachable. The sort of little home-lab panel that says, <em>Everything is fine; go do literally anything else.</em></p>
<p>Instead, it became the most anxious machine in the house.</p>
<p>The first version polled its backend roughly ten times a second. On a fast local network, the requests usually returned before anyone noticed. On cellular data, Wi-Fi roaming, or a sleepy phone, a response could take longer than the interval between requests. The dashboard did not wait. It launched another request, then another, then another.</p>
<p>Soon the browser was running a tiny stampede:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tick 1: request A --------------------&gt;
</span></span><span style="display:flex;"><span>tick 2:   request B ------------------&gt;
</span></span><span style="display:flex;"><span>tick 3:     request C ----------------&gt;
</span></span><span style="display:flex;"><span>tick 4:       request D --------------&gt;
</span></span><span style="display:flex;"><span>                       &lt;--- C fails
</span></span><span style="display:flex;"><span>              &lt;--- A succeeds
</span></span></code></pre></div><p>Because responses arrived out of order, an old failure could overwrite a newer success. The service was fine. The dashboard announced that it was dead.</p>
<h2 id="a-green-dot-can-be-a-denial-of-service-tool">A green dot can be a denial-of-service tool</h2>
<p>Polling looks harmless because every request is small. The danger is multiplication.</p>
<p>If a new request starts every 100 milliseconds but a slow network takes two seconds to answer, twenty requests can be in flight at once. Add retries and multiple open tabs and the monitoring client becomes load generation. It is the digital equivalent of calling a restaurant ten times a second to ask whether the phone system works.</p>
<p>The problem was not &ldquo;JavaScript is bad&rdquo; or &ldquo;cellular is unreliable.&rdquo; The problem was that the design had no concept of an unfinished question.</p>
<p>The fix was a single-flight rule:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>if a check is already running:
</span></span><span style="display:flex;"><span>    do not start another one
</span></span><span style="display:flex;"><span>else:
</span></span><span style="display:flex;"><span>    run one check with a timeout
</span></span><span style="display:flex;"><span>    record the result
</span></span><span style="display:flex;"><span>    schedule the next check afterward
</span></span></code></pre></div><p>That one rule changed the system from a metronome into a conversation.</p>
<h2 id="one-miss-is-not-an-outage">One miss is not an outage</h2>
<p>The second mistake was treating every failed request as a verdict.</p>
<p>Phones change networks. Radios wake up. DNS caches expire. A tab disappears into the background and returns with its timers distorted. One missed check is evidence of one missed check. It is not proof that the watched service is down.</p>
<p>A useful status display needs memory:</p>
<ul>
<li>keep the last known-good result visible for a bounded grace period;</li>
<li>count consecutive failures rather than panicking on the first one;</li>
<li>show &ldquo;stale&rdquo; before &ldquo;dead&rdquo;;</li>
<li>require a fresh success before declaring recovery;</li>
<li>slow down when the tab is hidden or the device is offline.</li>
</ul>
<p>This is not hiding failures. It is refusing to promote noise into truth.</p>
<h2 id="monitoring-software-needs-a-nervous-system">Monitoring software needs a nervous system</h2>
<p>The best monitors are not the ones that ask the most questions. They are the ones that understand the cost and meaning of each answer.</p>
<p>A calm client has a timeout, a backoff policy, a maximum concurrency of one for each check, and a state machine more expressive than green versus red. It distinguishes <code>healthy</code>, <code>stale</code>, <code>degraded</code>, <code>offline</code>, and <code>unknown</code>. Most importantly, it does not let an older response travel backward through time and erase a newer one.</p>
<p>The final dashboard did less work and told the truth more often.</p>
<p>That is a recurring engineering lesson: when a monitoring screen screams, check whether the patient is sick—or whether the thermometer is stabbing it ten times a second.</p>
]]></content:encoded></item></channel></rss>