<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:syndication="https://joctatorres.dev/ns/syndication">
<channel>
  <title>joc-thoughts</title>
  <link>https://joc-thoughts.dev/</link>
  <description>Editorial-tech essays by Jocta Torres on distributed systems, ML &amp; AI, and the philosophy of computer science.</description>
  <language>en-us</language>
  <atom:link href="https://joc-thoughts.dev/feed.xml" rel="self" type="application/rss+xml" />
  <item>
    <title>The Shape of a Good Incident Postmortem</title>
    <link>https://joc-thoughts.dev/blog/the-shape-of-a-good-incident-postmortem</link>
    <guid isPermaLink="true">https://joc-thoughts.dev/blog/the-shape-of-a-good-incident-postmortem</guid>
    <pubDate>Sat, 26 Apr 2025 00:00:00 GMT</pubDate>
    <description>Most postmortems fail not at the writing but at the framing. The shape of the document is the shape of the lesson — and most documents have the wrong shape.</description>
    <dc:creator><![CDATA[Jocta Torres]]></dc:creator>
    <content:encoded><![CDATA[<p>Most engineering organizations write postmortems the same way. A timeline at the top. Root cause halfway down. Action items at the bottom. A bullet list of contributing factors that, on close reading, contributes very little. The document is signed off, filed in a wiki nobody re-reads, and the same incident — or a close cousin of it — happens again six months later. The ritual was performed. The lesson was not learned.</p>
<p>I have been writing and reviewing postmortems for ten years now, across three companies and four sets of conventions. The pattern I keep hitting is that the <em>shape</em> of the document is the shape of the lesson, and most documents have the wrong shape. They optimize for legibility to the executive who skims them once, not for the engineer who has to absorb them. They emphasize the bug that triggered the outage and underweight the systemic conditions that allowed a single bug to take everything down. And they almost always end before the most important question has been asked.</p>
<h2>The bad shape: timeline-first</h2>
<p>Here is the pattern I see most often. The postmortem leads with a minute-by-minute timeline:</p>
<pre><code>14:02 — alert fires on api-prod-write-latency
14:04 — on-call acknowledges; investigates database
14:11 — escalates to db-team
14:18 — db-team identifies stuck transaction
14:22 — manual rollback issued
14:27 — latency returns to baseline
</code></pre>
<p>This timeline is necessary for a particular kind of audience — the person reconstructing the incident weeks later for a regulatory report, or the SRE writing a runbook patch. It is not, however, the right <em>opening</em>. The reader&#39;s first contact with the document is a narrow procedural log; everything that matters about the incident — the surprise, the model failure, the assumption that turned out to be false — is buried under timestamps.</p>
<p>Timeline-first is the default template in most incident-management tools. It&#39;s the easy thing to generate and the wrong thing to lead with.</p>
<p>The result is that a reader who is not the original responder gets a recap of what <em>happened</em> without ever encountering what was <em>believed</em> before the incident. Postmortems are interesting precisely when they reveal the gap between belief and reality. A timeline rarely shows that gap; it shows the actions that closed it.</p>
<h2>The better shape: belief-first</h2>
<p>Here is the shape I have come to prefer:</p>
<ol>
<li><strong>What we believed before the incident.</strong> Three to five sentences. The mental model the team carried into the day — about capacity, about failure modes, about which dependency could and could not break. Specific. Quotable. Wrong somewhere.</li>
<li><strong>What actually happened.</strong> A short narrative — no timestamps yet, just the story. Two to four paragraphs.</li>
<li><strong>Where the belief and the reality diverged.</strong> This is the load-bearing section. Name the <em>specific</em> assumption that turned out to be wrong. Quote it from a runbook or a design doc if you can.</li>
<li><strong>Timeline.</strong> Now we have earned it. The reader knows the stakes.</li>
<li><strong>Contributing factors.</strong> Ranked, not bulleted. The top one matters; the bottom three are filler unless someone defends each one.</li>
<li><strong>What would have prevented this.</strong> Not &quot;what did we do&quot; — that&#39;s tactical. What change in the <em>system that allowed this</em> would have prevented it? Code change, process change, alerting change, organizational change.</li>
<li><strong>What we are doing.</strong> Owners and dates. Three or fewer items. If you have ten items, you have ten future postmortems.</li>
</ol>
<p>The discipline of the belief-first shape forces the writer to surface the <em>prior</em> — the model the team was operating under. Once the prior is written down, the question of whether the prior was reasonable becomes answerable. Sometimes the prior was reasonable and the incident was a genuine surprise; that&#39;s a different lesson than &quot;we missed the obvious thing.&quot; A timeline-first postmortem cannot make this distinction.</p>
<p>You don&#39;t really understand a system until you have observed how it fails — and the observation has to make it back into the model.</p>
<h2>The two failure modes of postmortems</h2>
<p>Once you start writing in this shape, you notice two characteristic failure modes in the postmortems other people write.</p>
<p><strong>Failure mode one: the mechanism is correct but the lesson is shallow.</strong> &quot;We had a stuck transaction; we rolled it back; we will add an alert for stuck transactions.&quot; Mechanically accurate, operationally improved, but the <em>system</em> that allowed a single stuck transaction to cause a 25-minute outage is unchanged. Why was the database not isolated? Why was the on-call surprised? Why did the alert come from latency rather than from the transaction itself? The shallow postmortem stops at the first <em>because</em> and treats the answer as the cause.</p>
<p><strong>Failure mode two: the lesson is deep but unactionable.</strong> &quot;Our culture of optimism around schema changes is the underlying issue.&quot; Maybe true. Almost certainly not something the next reader can act on. Deep lessons need to land somewhere — a checklist, a code review template, a hiring criterion, a quarterly goal. If the deep lesson does not become a <em>concrete change in how the team operates next week</em>, it is decoration, no matter how true it is.</p>
<p>A good postmortem walks the narrow path between these. It identifies a systemic condition, then proposes a change small enough to make this quarter and concrete enough to verify next quarter.</p>
<h2>A worked example</h2>
<p>Suppose a team runs a payments service that emits an outbox event for every successful charge. Downstream consumers — analytics, fraud, customer email — read from the outbox table via change-data-capture. One day, the outbox-publisher process gets stuck. Events back up. After two hours, somebody notices because the daily revenue dashboard is suspiciously empty. The team restarts the publisher; events flow again; everything looks fine.</p>
<p>The shallow postmortem says: &quot;Add a metric for outbox-publisher lag; alert if lag &gt; 5 minutes.&quot; This is good and the team should do it. It is not enough.</p>
<p>The deeper question: <em>the team did not notice for two hours.</em> Why? Because there was no integrator-of-last-resort whose job it was to ask &quot;is the data flowing?&quot; The dashboards showed application metrics — the payments service itself was healthy — and downstream dashboards were owned by other teams. The systemic condition was: <strong>no single team owned the end-to-end pipeline health, so when the pipeline broke between teams, nobody noticed.</strong> Underneath that is a second condition the team had not <a href="/blog/distributed-systems-sample">named the deadline</a> — there was no agreed answer to <em>how long is too long?</em>, so two hours of silence registered as nothing.</p>
<p>The deepest lessons from incidents almost always live in the seams between teams. The team that owns the upstream service notices upstream. The team that owns the downstream service notices downstream. The seam is where the alarm should be — and is usually where it is missing.</p>
<p>The actionable change is not &quot;add a metric.&quot; It is &quot;designate a pipeline-health owner&quot; — a rotation, a named team, a dashboard that reports the <em>gap</em> between upstream production and downstream consumption. The metric falls out of that ownership. Without the ownership, the metric exists but nobody is on the hook for watching it.</p>
<p> HealthReport:
    upstream_count = payments.events_emitted_in(last_hour=True)
    downstream_count = analytics.events_consumed_in(last_hour=True)
    lag = upstream_count - downstream_count
    if lag &gt; LAG_THRESHOLD:
        page(&quot;pipeline-owner&quot;, reason=f&quot;outbox lag={lag}&quot;)
    return HealthReport(upstream=upstream_count,
                        downstream=downstream_count,
                        lag=lag)`}</p>
<blockquote>
</blockquote>
<pre><code class="language-python"># Conceptual sketch: a single end-to-end health check
# owned by one team, asserted in one place.
def assert_pipeline_health() -&gt; HealthReport:
    upstream_count = payments.events_emitted_in(last_hour=True)
    downstream_count = analytics.events_consumed_in(last_hour=True)
    lag = upstream_count - downstream_count
    if lag &gt; LAG_THRESHOLD:
        page(&quot;pipeline-owner&quot;, reason=f&quot;outbox lag={lag}&quot;)
    return HealthReport(upstream=upstream_count,
                        downstream=downstream_count,
                        lag=lag)
</code></pre>
<p>The code is trivial. The hard part — the part that the postmortem must produce — is the answer to &quot;who runs this, and who gets paged when it fails?&quot; That answer is organizational, not technical, and a timeline-first postmortem will almost never reach it.</p>
<h2>Postmortems as compounding artifacts</h2>
<p>There is a second-order benefit to writing postmortems in the belief-first shape: they accumulate into a useful body of <a href="/topics/distributed-systems">organizational knowledge about distributed systems</a> over time. A pile of timeline-first postmortems is not a knowledge base; it is a chronological archive. Reading ten of them in sequence is exhausting and yields little because each one starts over with new timestamps and new components.</p>
<p>A pile of belief-first postmortems, by contrast, can be re-read for patterns: <em>which beliefs, across the company, have been wrong most often?</em> If you have twelve postmortems and seven of them turn on a wrong belief about backpressure, you have not had seven incidents — you have had one organizational gap manifesting seven times. The pattern is invisible in timeline-first writing because the timelines do not align. It is obvious in belief-first writing because the priors do.</p>
<p>This is why I push teams to write the <em>prior</em> first, even when it feels embarrassing. The embarrassment is the point. The prior is what was wrong, and writing it out makes the wrongness legible. Once it is legible, it becomes correctable — first in this team&#39;s understanding, then in the next team&#39;s via the documents that survive.</p>
<h2>What about blame?</h2>
<p>The standard answer is &quot;postmortems are blameless.&quot; This is correct, and it is also insufficient on its own. <em>Blameless</em> means the postmortem does not assign fault to an individual; it does not mean the postmortem refrains from naming what was wrong. A postmortem that is so blameless it cannot identify the false belief is not blameless; it is <em>vague</em>, and vagueness is its own kind of harm because it makes the lesson unreachable.</p>
<p>The right move is to be specific about what was wrong while being generous about why a competent person could have believed it. &quot;The runbook said the database was the bottleneck; based on Q3 load tests, this was true. By Q1 of the next year, the bottleneck had quietly shifted to the message broker, and no one re-ran the load test.&quot; This is blameless and specific. It accuses no individual; it <a href="/blog/cs-philosophy-sample">names the precise belief</a> that drifted out of sync with reality and the artifact that should have caught the drift.</p>
<p>A postmortem that hits this register — specific about the gap, generous about the cause, concrete about the change — is rare. When you read one, save it. When you write one, share it. They are how teams get less surprised over time.</p>
<h2>A short checklist</h2>
<p>If I had to compress all of this into a checklist for the writer of the next postmortem:</p>
<ul>
<li>Did you write the <strong>prior</strong> down — what the team believed before the incident? Three sentences minimum.</li>
<li>Did you name the <strong>specific belief</strong> that turned out to be wrong, and quote it from a runbook or design doc?</li>
<li>Are your <strong>action items</strong> concrete enough that a stranger reading the document next quarter can ask &quot;did we do that?&quot;</li>
<li>Is there at most <strong>one organizational change</strong> in the action items? More than one means none of them will actually happen.</li>
<li>Does the document name an <strong>owner</strong> for each action item, not just a team?</li>
<li>If you re-read this postmortem in a year alongside ten others, will the <strong>pattern</strong> show?</li>
</ul>
<p>If the answer to all six is yes, the postmortem is doing its job. If not, it is — like most of the postmortems I have read — a record of an event rather than the seed of a lesson. The first kind is easy to produce and easy to forget. The second kind is harder to write and harder to forget. We need more of the second kind.</p>
]]></content:encoded>
    <category>Distributed Systems</category>
    <category>reliability</category>
    <category>operations</category>
    <category>writing</category>
    <category>postmortems</category>
  </item>
  <item>
    <title>Naming Things Is the Last Hard Problem in Computer Science</title>
    <link>https://joc-thoughts.dev/blog/cs-philosophy-sample</link>
    <guid isPermaLink="true">https://joc-thoughts.dev/blog/cs-philosophy-sample</guid>
    <pubDate>Fri, 18 Apr 2025 00:00:00 GMT</pubDate>
    <description>Cache invalidation got tooling. Off-by-one errors got tests. Naming is still mostly vibes, and that has cost us more than we admit.</description>
    <dc:creator><![CDATA[Jocta Torres]]></dc:creator>
    <content:encoded><![CDATA[<p>Phil Karlton&#39;s joke — &quot;there are only two hard things in computer science: cache invalidation and naming things&quot; — is repeated so often that the actual claim has been lost. The claim is that <strong>naming is hard at the same level cache invalidation is hard</strong>: not as a polish step, but as a load-bearing engineering activity.</p>
<p>We have made tremendous progress on cache invalidation. We have CDN purge APIs, vector clocks, content-addressed storage. We have not made remotely as much progress on naming. Naming is still, almost everywhere, a matter of taste, of habit, of whatever the original author happened to type at 2am.</p>
<h2>Why bad names cost so much</h2>
<p>A bad name is not a lexical error. It is a <strong>type error in the social system that maintains the code</strong> — one of <a href="/topics/cs-philosophy">the recurring themes of the philosophy of computer science</a>, and one we keep underestimating. A function named <code>processItem</code> invites every future caller to project their own theory of what the function does — and one of them will be wrong. A boolean called <code>flag</code> becomes a magnet for special cases. A class called <code>Manager</code> accretes responsibilities until it manages the heat death of the universe.</p>
<p>The cost compounds because naming determines <strong>who can change the code</strong>. A reader who has to derive intent from the body before they can edit safely is a slow reader. A reader whose grasp of the system depends on memorizing nicknames is a brittle reader. Names are the project&#39;s API for its own contributors.</p>
<pre><code class="language-ts">// Both compile. One is a debugging session.
function check(u: User, x: number): boolean { /* ... */ }
function userHasSufficientCredits(user: User, requiredCredits: number): boolean { /* ... */ }
</code></pre>
<p>You can argue the second is verbose. You cannot argue the second is unclear at the call site, six months later, <a href="/blog/the-shape-of-a-good-incident-postmortem">in a postmortem</a>.</p>
<h2>What &quot;good naming&quot; actually requires</h2>
<p>It requires three things, none of which are tooling:</p>
<ul>
<li><strong>A vocabulary that the team has agreed on.</strong> Glossaries are deeply unfashionable and almost always worth the half-day.</li>
<li><strong>A willingness to rename late.</strong> Most names are wrong on the first pass; the cost of fixing them goes up monotonically with time.</li>
<li><strong>A house style that resists cleverness.</strong> Cute names age badly. Boring names age into prose.</li>
</ul>
<h2>The remaining hard problem</h2>
<p>We can cache-invalidate at scale because we treated invalidation as engineering. We can mostly avoid off-by-one errors because we treated them as engineering. The same move is now overdue for <a href="/blog/ml-ai-sample">training-data provenance</a> — also currently shrugged off as taste, also paying the same compounding bill. Naming is the last large category of bug we have collectively decided to leave to &quot;taste&quot;. That decision is more expensive than it looks, and the bill keeps coming due in onboarding time, in ambiguous bug reports, in the slow compounding cost of code nobody quite understands.</p>
<p>We could decide otherwise. It would be cheap. We just don&#39;t, yet.</p>
]]></content:encoded>
    <category>CS &amp; Philosophy</category>
    <category>language</category>
    <category>design</category>
    <category>taste</category>
  </item>
  <item>
    <title>Training-Data Provenance Is the Real Alignment Problem</title>
    <link>https://joc-thoughts.dev/blog/ml-ai-sample</link>
    <guid isPermaLink="true">https://joc-thoughts.dev/blog/ml-ai-sample</guid>
    <pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate>
    <description>Before we argue about model behavior, we should be able to point to a row of data and explain how it got into the corpus. We mostly cannot.</description>
    <dc:creator><![CDATA[Jocta Torres]]></dc:creator>
    <content:encoded><![CDATA[<p>Most public conversation about AI alignment is downstream of a much simpler problem: we generally cannot point to a row of training data and explain, in concrete terms, how it ended up in the corpus. Not &quot;approximately&quot; — concretely. Which crawl, which heuristic, which dedupe pass, which licensing assumption.</p>
<h2>Provenance vs. lineage</h2>
<p>It helps to separate two ideas. <strong>Lineage</strong> is the story of how data flows through your pipeline once you have it: cleaning, tokenization, sharding, mixing. Lineage is mostly a tooling problem, and the field is getting better at it.</p>
<p><strong>Provenance</strong> is the story of where the data came from in the first place. That story tends to terminate in phrases like &quot;Common Crawl, snapshot 2024-09&quot; or &quot;a partner dataset&quot; or, worst, &quot;we don&#39;t remember exactly&quot;. Provenance is upstream of lineage, and it&#39;s where <a href="/topics/ml-ai">the genuinely hard ML/AI questions</a> live.</p>
<h2>A small, fixable problem</h2>
<p>The smallest version of this problem is reproducibility. If a behavior surfaces in eval at version <code>v37</code> of a model and is gone in <code>v38</code>, the obvious question is: what changed in the data? Today, for many teams, the honest answer is &quot;we changed eight things and re-ran&quot;. This is the same shape of failure as <a href="/blog/distributed-systems-sample">an unnamed deadline in a distributed system</a> — without the marker written down, the post-hoc reconstruction is the only debugging surface you have. A provenance graph would let you answer it in one query.</p>
<pre><code class="language-python"># What we want to be able to say:
diff = corpus_v38.provenance_set() - corpus_v37.provenance_set()
print(diff.sources_added)   # which upstreams entered the mix
print(diff.sources_dropped) # which were filtered out
</code></pre>
<p>That doesn&#39;t require a new framework. It requires that every source — every URL list, every partner dump, every synthetic generator — emits a stable identifier, and that the corpus retains that identifier per row.</p>
<h2>The bigger reason it matters</h2>
<p>The bigger reason is that <strong>regulation is not going to wait for us to figure this out</strong>. The EU AI Act, the various US state-level proposals, the inevitable consent-based opt-outs — every one of these requires the operator to answer a provenance question on demand. &quot;We trained on a public mix&quot; is not going to be enough. &quot;We trained on these 4,217 sources, here is the manifest, here is when each was last refreshed, here is the legal basis we relied on for each&quot; — that is going to be the bar.</p>
<p>Building toward that bar is mostly <strong>boring infrastructure</strong>: hashing, manifesting, signing, journaling. None of it is research-flavored. All of it pays off the first time someone asks a hard question and you can answer it with a query instead of a Slack thread.</p>
<p>The alignment debate is not going to make sense until we can have it grounded in concrete artifacts. Provenance is what makes the artifact concrete — and concreteness, as in <a href="/blog/cs-philosophy-sample">every other corner of engineering</a>, starts with a name we have agreed to.</p>
]]></content:encoded>
    <category>ML &amp; AI</category>
    <category>alignment</category>
    <category>data</category>
    <category>provenance</category>
  </item>
  <item>
    <title>What Consensus Protocols Teach Us About Deadlines</title>
    <link>https://joc-thoughts.dev/blog/distributed-systems-sample</link>
    <guid isPermaLink="true">https://joc-thoughts.dev/blog/distributed-systems-sample</guid>
    <pubDate>Fri, 04 Apr 2025 00:00:00 GMT</pubDate>
    <description>Raft, Paxos, and the surprising lesson that durable agreement is mostly about agreeing when to stop waiting.</description>
    <dc:creator><![CDATA[Jocta Torres]]></dc:creator>
    <content:encoded><![CDATA[<p>Consensus protocols are usually introduced as algorithms for agreeing on a value. Read enough Raft papers and a different framing emerges: they are primarily algorithms for agreeing <strong>when to stop waiting</strong>. The value is incidental; the deadline is the contract.</p>
<h2>The deadline is the contract</h2>
<p>In a single-node system, &quot;now&quot; is unambiguous. The instant you write to disk and <code>fsync</code> returns, the world has moved on. In a <a href="/topics/distributed-systems">distributed system</a>, &quot;now&quot; is a negotiation. Until a quorum acknowledges, the write is suspended in a kind of probabilistic superposition — durable enough to survive most failures, fragile enough that a partition can rewrite it. The whole engineering job is bounding that interval.</p>
<p>Raft&#39;s election timeout, Paxos&#39;s ballot numbering, ZAB&#39;s epoch counters — they are all variants of the same trick: <strong>make the deadline a first-class value that every participant can compare</strong>. Once a follower decides &quot;I have waited long enough&quot;, it doesn&#39;t ask permission. It promotes itself, and the protocol absorbs the disagreement.</p>
<pre><code class="language-ts">// Conceptually — pseudocode, not real Raft.
if (now() - lastHeartbeat &gt; electionTimeout) {
  becomeCandidate();
  requestVotesFrom(peers);
}
</code></pre>
<p>The interesting part isn&#39;t the <code>if</code>. It&#39;s that <code>electionTimeout</code> is <strong>randomized per node</strong> so two followers don&#39;t promote in lockstep. The protocol uses entropy as a coordination primitive.</p>
<h2>What the deadline buys you</h2>
<p>Three things, in the order they matter:</p>
<ol>
<li><strong>Liveness.</strong> A bounded interval after which a stuck cluster will pick a winner, even if the wrong one.</li>
<li><strong>Convergence.</strong> Subsequent rounds will rule out the bad winner and replace it. Liveness without convergence is just thrashing.</li>
<li><strong>A debugging surface.</strong> When something goes wrong, the timestamps tell you who decided what, and when. No deadline, no story.</li>
</ol>
<p>The third one is undervalued. A distributed system without explicit deadlines is a system whose failures can only be reasoned about post-hoc by reading logs and squinting. Once the deadline is in the protocol, it&#39;s also in the metrics, the traces, and the SLOs.</p>
<h2>The lesson generalizes</h2>
<p>Most &quot;distributed&quot; problems in application code aren&#39;t really about distribution. They are about deadlines we never named. The cron job that sometimes runs twice; the webhook that sometimes fires after the user has logged out; the cache that sometimes serves stale data after a write — these are all <a href="/blog/the-shape-of-a-good-incident-postmortem">consensus failures in miniature</a>, and the unnamed deadline is what turns each one into an incident the team has to write up later. Naming the deadline turns them into design problems instead of incidents.</p>
<p>That, I think, is the durable lesson the consensus literature has for the rest of us. Not the algorithms. The <a href="/blog/cs-philosophy-sample">discipline of writing the deadline down</a>.</p>
]]></content:encoded>
    <category>Distributed Systems</category>
    <category>consensus</category>
    <category>raft</category>
    <category>reliability</category>
  </item>
</channel>
</rss>
