<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Den</title>
    <description>The latest articles on DEV Community by Den (@den0011).</description>
    <link>https://dev.to/den0011</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3855195%2F9d7e6b60-0b75-4bc2-be7a-a7342ea26650.png</url>
      <title>DEV Community: Den</title>
      <link>https://dev.to/den0011</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/den0011"/>
    <language>en</language>
    <item>
      <title>A Good Summary Still Left Me Guessing</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Sun, 13 Sep 2026 10:46:44 +0000</pubDate>
      <link>https://dev.to/den0011/a-good-summary-still-left-me-guessing-12ci</link>
      <guid>https://dev.to/den0011/a-good-summary-still-left-me-guessing-12ci</guid>
      <description>&lt;p&gt;A while back I &lt;a href="https://dev.to/den0011/i-built-a-free-ai-digest-for-science-and-tech-news-111o"&gt;posted here about RealFeedApp&lt;/a&gt;, a free digest that collects science and tech news, collapses the retellings into one card, ranks by significance and writes a short summary in your language.&lt;/p&gt;

&lt;p&gt;It worked. I stopped drowning. And then I noticed I was doing something odd with it: I'd read a summary, understand every word of it, and still have no idea whether the thing mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a summary doesn't do
&lt;/h2&gt;

&lt;p&gt;A summary compresses an article. It cannot supply the thing the article assumed you already had.&lt;/p&gt;

&lt;p&gt;Every piece of technical writing is addressed to someone. A paper on gene therapy is written for people who know what a vector is; a post about attention layers is written for people who have already met attention. The author skips the setup, and they're right to — their reader has it.&lt;/p&gt;

&lt;p&gt;Compress that article faithfully and you get a shorter text with the same hole in it. Here's the shape of it, with the details changed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Researchers report that the modified vector achieved durable expression in the target tissue without the immune response seen in earlier trials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every word true, nothing wasted. And if that isn't your field, you've learned nothing you can act on. Is this a lab curiosity or the thing the field has been stuck on for a decade? The summary can't say, because the article never said — the article was talking to someone who knew.&lt;/p&gt;

&lt;p&gt;This is the actual cost of reading widely. Not volume, which is what everyone builds for. &lt;strong&gt;Volume is solved by filtering. Context is not, because the missing part was never in the text you filtered.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new: three lines under the summary
&lt;/h2&gt;

&lt;p&gt;So every card now carries a second block, under the summary and behind a divider. Three labelled rows, no heading:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field&lt;/strong&gt; — a short tag: &lt;em&gt;how neural networks work&lt;/em&gt;, &lt;em&gt;gene therapy&lt;/em&gt;, &lt;em&gt;hardware and chips&lt;/em&gt;, &lt;em&gt;energy and batteries&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was done&lt;/strong&gt; — the concrete thing, in ordinary words.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt; — what changes if it holds up.&lt;/p&gt;

&lt;p&gt;Same three rows on every card, in the same order, in your language. The point isn't more text. The point is that the same three questions get answered every time, so you can skim the third line across ten cards and stop at the one that deserves you.&lt;/p&gt;

&lt;p&gt;A few decisions inside it that changed how it reads:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The field is a tag, not a sentence.&lt;/strong&gt; It comes from a fixed list of areas, so the same subject is worded identically on every card. That's what makes it scannable — and it's why it can become a filter later. Free-form text would have been easier to build and useless the first time the same field got named two different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No heading above the block.&lt;/strong&gt; It's three labelled rows; the labels already say what it is. A heading would have eaten the space the explanation needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't appear where there's nothing to explain.&lt;/strong&gt; If the linked page had no article on it — a video, a page that's really an app, a PDF that yields nothing — there's no block. There's also no card: pages with nothing to read stopped reaching the feed at all, which fixed a smaller embarrassment I'd been shipping for a while. Summaries of &lt;em&gt;"this page requires JavaScript"&lt;/em&gt; are, it turns out, very fluent.&lt;/p&gt;

&lt;p&gt;That last rule matters more than the feature. &lt;strong&gt;An explanation of nothing is worse than no explanation&lt;/strong&gt;, because it looks exactly like the real thing and you can't tell without opening the link — which is the one thing the card exists to save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't read the article for you. Three lines are enough to decide, not enough to know, and the link to the original is still the whole point of the card.&lt;/p&gt;

&lt;p&gt;In your own field it will occasionally explain something you learned a decade ago. That's the trade: the block is written for the reader who wandered in from three fields over, and I'd rather it be redundant for the specialist than absent for everyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Still free. Open the link and you get a preview of the feed with no account at all; signing in with Google opens the rest of it and remembers your topics. &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;realfeedapp.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Android app is in closed testing; it'll reach Google Play once I'm confident it behaves.&lt;/p&gt;

&lt;p&gt;Two things I'd like to hear if you open it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the third line earn its space?&lt;/strong&gt; "Why it matters" is the row I'm least sure about — it's the one where an honest &lt;em&gt;we don't know yet&lt;/em&gt; would be more useful than a confident sentence, and I don't know yet whether it's doing that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where does the field tag fit badly?&lt;/strong&gt; The list of areas is fixed, so somewhere there's a story it can only be wrong about. I'd like to know which one before a reader has to.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Float Is a Liveness Probe: Four Failure Modes</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:53:35 +0000</pubDate>
      <link>https://dev.to/den0011/the-float-is-a-liveness-probe-four-failure-modes-4g3k</link>
      <guid>https://dev.to/den0011/the-float-is-a-liveness-probe-four-failure-modes-4g3k</guid>
      <description>&lt;p&gt;I cook pelmeni about twice a week, and for years I got inconsistent results from what is, on paper, a trivial process: put frozen parcels in boiling water, take them out when done. Two failure modes kept recurring — parcels welded to the bottom of the pot, and parcels that burst and leaked their filling into the water.&lt;/p&gt;

&lt;p&gt;Both turned out to be the same class of mistake I make at work, which is why I'm writing this down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure one: the batch was too large
&lt;/h2&gt;

&lt;p&gt;Dropping a large batch of frozen parcels into boiling water stops the boil. The water is at 100°C, the parcels are at −18°C, and there's only so much thermal energy in the pot. The temperature drops below boiling, the surface starch doesn't set, and the dough touches the metal while it's still sticky. That's the welding.&lt;/p&gt;

&lt;p&gt;I had been treating pot capacity as a volume question — will they fit — when it's actually a throughput question: how much cold can this system absorb before it stops doing the thing it's for. A pot that fits thirty parcels comfortably processes twenty of them correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capacity is not the same as the load at which quality holds.&lt;/strong&gt; You can size a worker pool by what fits in memory and still be wrong, for exactly this reason: the system keeps accepting work long after it stopped doing it well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure two: I trusted the wrong signal
&lt;/h2&gt;

&lt;p&gt;Everyone knows the rule: pelmeni float when they're done. Everyone is wrong, and I was wrong for a long time.&lt;/p&gt;

&lt;p&gt;They float when the air and steam trapped inside expand enough to make the parcel buoyant. That happens when the &lt;em&gt;dough&lt;/em&gt; is hot. It says nothing about the filling, which is denser, colder, and at the centre of the parcel — the last part of the system to get warm. Pull them at the moment they surface and the outside is finished while the inside is raw.&lt;/p&gt;

&lt;p&gt;Floating is a &lt;strong&gt;liveness&lt;/strong&gt; signal. It tells you the process is running. It is not a &lt;strong&gt;readiness&lt;/strong&gt; signal, and the difference between those two is exactly the difference between "the container started" and "the container can serve traffic". Confusing them ships raw meat.&lt;/p&gt;

&lt;p&gt;The actual readiness signal is time &lt;em&gt;after&lt;/em&gt; the liveness signal — a few minutes at a gentle boil once they've surfaced. Which is an unsatisfying answer, because it's a timer, and everybody wants an event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you can't observe the thing you care about, you time it from the last thing you can.&lt;/strong&gt; That's not a workaround, that's the design. The mistake is pretending the observable proxy is the real state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure three, which I still make
&lt;/h2&gt;

&lt;p&gt;The lid. A lid gets you back to boiling faster after the cold batch goes in, which is good, and then the starch-laden foam climbs the sides and comes over the edge, which is not.&lt;/p&gt;

&lt;p&gt;The correct move is to leave the lid slightly ajar — you sacrifice some of the recovery rate to keep the process inside the pot. This is a backpressure trade and it has the same shape everywhere: maximum throughput and stability are different set points, and choosing throughput means you will eventually clean the stove.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the burst parcels were really telling me
&lt;/h2&gt;

&lt;p&gt;The leaking-filling failure isn't overcooking in the ordinary sense. It's what happens when a parcel stays under a hard rolling boil: mechanical agitation, not heat, tears the seam. The fix isn't less time, it's less turbulence — drop the heat once the parcels are in and let it finish at a bare simmer.&lt;/p&gt;

&lt;p&gt;I had been tuning the wrong parameter for months because the symptom appeared at the end of the process, so I assumed it belonged to the end of the process. It belonged to the environment the whole way through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failure that shows up last isn't caused last.&lt;/strong&gt; Applies to dinner and to production incidents in roughly equal measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure four: I was salting for the wrong reason
&lt;/h2&gt;

&lt;p&gt;I used to salt the water for flavour. It does that, but the more important effect is thermal: dissolved salt raises the boiling point substantially, so a well-salted pot runs hotter than a plain one and gets you through the cold-batch recovery noticeably faster. Two tablespoons per litre is roughly where the gain stops being worth the taste.&lt;/p&gt;

&lt;p&gt;This is the one that changed my routine the most, because it means the salt goes in &lt;em&gt;before&lt;/em&gt; the water boils rather than with the parcels.&lt;/p&gt;




&lt;p&gt;So: batch smaller than capacity, don't trust the float, lid ajar, simmer not boil, salt early.&lt;/p&gt;

&lt;p&gt;One of the four claims above is wrong — confidently stated, cheerfully believed by me for years, and demonstrably false. I know which one. I'm curious whether anyone catches it, and whether this kind of thinking about cooking is a useful frame or just professional deformation. &lt;strong&gt;What do you think?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>discuss</category>
      <category>devops</category>
    </item>
    <item>
      <title>Language Models and Languages: Why "Multilingual" Is an Illusion of Equality</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:24:46 +0000</pubDate>
      <link>https://dev.to/den0011/language-models-and-languages-why-multilingual-is-an-illusion-of-equality-59ii</link>
      <guid>https://dev.to/den0011/language-models-and-languages-why-multilingual-is-an-illusion-of-equality-59ii</guid>
      <description>&lt;p&gt;When people see ChatGPT or Claude answer fluently in Swahili or Thai, the natural impression is that the model "knows" all languages equally well. It doesn't — and the gap isn't cosmetic. It runs straight through the architecture of how these models read and process text. What follows is a breakdown of where that gap actually comes from, how large it is, and what's being done about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem starts before the model "thinks" — at tokenization
&lt;/h2&gt;

&lt;p&gt;An LLM never reads text directly. Text is first cut into pieces (tokens) by a tokenizer, and only those tokens are what the model actually processes. Tokenizers are trained on corpora where English historically dominates — in Llama 3's training data, for instance, English and code made up 95% of the dataset, while every other language combined accounted for just 5%.&lt;/p&gt;

&lt;p&gt;Because of this skew, the tokenizer ends up "efficient" for English and noticeably less efficient for everything else: models generate text one token at a time, and for languages poorly represented in the tokenizer's vocabulary, this makes generation slower and more computationally expensive.&lt;/p&gt;

&lt;p&gt;How big is the gap in practice? One study covering 25 European languages and ten models put a concrete number on it: tokenization "fertility" ranges from roughly 1.2 tokens per word for English to roughly 3.1 for Greek and Maltese — a penalty of about 2.5x. Notably, the ranking of "efficient" vs. "inefficient" languages barely changes regardless of register — formal or casual text — with a correlation above 0.97 across styles.&lt;/p&gt;

&lt;p&gt;A similar picture shows up for languages using a different script. A comparison of several tokenizers on equivalent Polish and English text found that where Polish required 547 tokens, English needed only 377 on the same model (Mistral Small 3.2) — and this gap repeats across most tested models.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why this isn't just about speed — it's about money and quality
&lt;/h2&gt;

&lt;p&gt;Tokenization inefficiency isn't only "responses in non-English are slower." Three effects stack on top of each other:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Since API pricing is billed per token, not per character, the same amount of meaning costs literally more in a "token-expensive" language. One breakdown describes a real case: a team running multilingual support saw its API bill triple in two weeks with no change to the prompt — the Brazilian and Indonesian user share simply grew, and their requests tokenized at 1.6x the per-character cost of English under the older vocabulary. Switching tokenizers cut the per-character token cost for those languages by 35% — without touching the model or the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context window.&lt;/strong&gt; If the same meaning takes several times more tokens in another language, a model with a fixed context limit (say, 128k tokens) effectively fits much less real text for that language — less of a document, less conversation history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer quality.&lt;/strong&gt; This isn't just a "more expensive" effect — it's also a "worse" effect. There's direct evidence of a correlation: the higher a language's tokenization compression ratio, the lower the model's task accuracy in that language. In other words, inefficient tokenization isn't a neutral side effect — it correlates with a real drop in quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The "reasoning" gap: where the ability to think actually gets lost
&lt;/h2&gt;

&lt;p&gt;Here's where it gets more interesting than a simple "more tokens, therefore worse."&lt;/p&gt;

&lt;p&gt;One key study (the HRM8K benchmark, Korean and English) tested the hypothesis head-on: maybe it's not tokenization, maybe the model simply "reasons" worse in non-English? The answer turned out to be more specific: the performance gap stems primarily from difficulty comprehending non-English input, not from a limitation in the reasoning capability itself. The authors also directly challenged an earlier recommendation to force the model to reason in English regardless of the question's language — it turns out models are heavily influenced by the language of the input itself, not just the language of the reasoning chain.&lt;/p&gt;

&lt;p&gt;A similar picture shows up in domain-specific benchmarks. On an Irish-language benchmark built from real exam questions, the best-performing model scored 76.2% accuracy in English versus 55.8% in Irish, and produced a valid Irish response (one that didn't drift into English or break the language) less than 80% of the time.&lt;/p&gt;

&lt;p&gt;One nuance that's often missed: the performance gap isn't uniform across task types. Evidence suggests the gap is especially pronounced in reasoning tasks dominated by high-resource languages (English foremost), but this pattern doesn't hold for factual questions or broader real-world user queries. A model might know the capital of Thailand perfectly well in Thai, but noticeably struggle with a multi-step logical problem in the same language.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What happens inside the model when it switches languages
&lt;/h2&gt;

&lt;p&gt;Worth approaching this from another angle — not through output benchmarks, but through what happens inside the model during generation. Anthropic has direct data on this, obtained through interpretability methods (an "AI microscope" — a way of looking directly at a model's internal activations).&lt;/p&gt;

&lt;p&gt;The key finding isn't about English as an "internal language of thought," but something more abstract: when analyzing how the model processes opposite-meaning words across languages, researchers found that the same internal features for "smallness" and "oppositeness" activate regardless of which language the input text is in. In other words, the model develops a shared, language-independent conceptual space, and any specific language is more of a "surface encoding" layered on top of that shared representation.&lt;/p&gt;

&lt;p&gt;This doesn't mean all languages are represented equally well in that space — the shared space exists, but the quality of "access" to it through a given language still depends on how richly that language was represented during training. It's important not to conflate the two: the existence of a shared conceptual space explains why the model can transfer knowledge across languages at all, but it doesn't erase the fact that input in a less-represented language still passes through a noisier, less precise "channel" into that space.&lt;/p&gt;

&lt;p&gt;A separate, fairly unexpected finding from the same Anthropic research concerns not capability but behavior: they found that the values the model expresses shift systematically depending on the language of the conversation — for example, in English the model leans toward caution, rigor, depth, and candor, while in Arabic it leans toward deference, warmth, brevity, and task execution. This isn't about intelligence — it's that the model's "character" isn't language-neutral at all. Training data in different languages carries different cultural and stylistic norms, and the model absorbs them differently for each language.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A busted myth: "write your prompt in X, it's cheaper and more efficient"
&lt;/h2&gt;

&lt;p&gt;Developer communities occasionally circulate advice like "write code-generation prompts in Chinese — it saves up to 40% in tokens." A recent study tested this directly on a real engineering benchmark (SWE-bench Lite) across several models and got an unambiguous result: the claimed Chinese token efficiency advantage did not hold up.&lt;/p&gt;

&lt;p&gt;This is a useful lesson on its own: the intuition that a "more compact" script (logographic characters versus Latin letters) should automatically be more efficient for a model doesn't hold. A tokenizer isn't just a character counter — it's a separately trained system with its own quirks, and you can't predict its behavior by eye without actually measuring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Where the fix is heading
&lt;/h2&gt;

&lt;p&gt;Two directions stand out as attempts to solve this at the architecture level rather than patch it after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dropping tokenization altogether.&lt;/strong&gt; Meta's Byte Latent Transformer doesn't tokenize input into discrete vocabulary IDs at all — instead it dynamically groups bytes into variable-length patches during training, with patch boundaries set by an entropy model. At comparable compute, an 8B-parameter BLT model matched a BPE-based Llama 3 8B baseline on standard benchmarks, while handling typos, code, and low-resource languages noticeably better. The point is that if a model has no privileged token vocabulary built on an English corpus in the first place, the structural inequality between languages disappears at the architecture level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Targeted post-training instead of architectural change.&lt;/strong&gt; A more practical — and far cheaper — path is not rebuilding the model from scratch, but fine-tuning it specifically to close this gap. One approach (UST — Understand, Solve, Translate) uses English as an anchor language for reasoning and solution generation, and on a synthetic dataset of 130,000 examples achieved a 10.91% improvement on the HRM8K benchmark, cutting the cross-language gap from 11.6% down to 0.7%. Industry analysis backs this up: tokenizer inefficiency and English-centric reasoning account for 70-80% of model failures in non-English languages, and targeted post-training looks like the most cost-effective fix compared to a full architectural overhaul or scaling up model size.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;If you're building a product for a multilingual audience — whether that's RealFeedApp serving news in multiple languages, or any other LLM-backed service — the language gap needs to be designed into the architecture up front, not filed under "minor localization detail":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget by tokens, not characters&lt;/strong&gt;, separately for each target language — the cost for the same amount of meaning can differ by 2-3x&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't rely on "smart" default tokenization&lt;/strong&gt; — the model/tokenizer choice for a multilingual product should be explicitly tested against your target languages, not assumed to be equally good everywhere just because the model is labeled multilingual&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For tasks requiring multi-step reasoning&lt;/strong&gt; (not facts, but logic, math, planning) in lower-resource languages, expect a noticeably bigger quality drop than you'd see on simple factual questions in the same language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English as a bridge language&lt;/strong&gt; is a proven, working pattern (translate → reason in English → translate back), but it adds latency and cost, and it isn't right for every case — where cultural specificity in the response matters, round-tripping through English can flatten it out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tokenizer is the least visible part of an LLM application's stack, which is exactly why it's the easiest thing to inherit unexamined along with your model choice. In practice, it's arguably the single most consequential hidden factor in how well your product performs outside of English.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources and further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic, "How Claude's values vary by model and language" (&lt;a href="https://www.anthropic.com/research/claude-values-models-languages" rel="noopener noreferrer"&gt;research&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>nlp</category>
      <category>cloud</category>
      <category>openai</category>
    </item>
    <item>
      <title>One API, Three Clients, and the Validation Rule That Only One of Them Obeyed</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 09 Sep 2026 16:33:53 +0000</pubDate>
      <link>https://dev.to/den0011/one-api-three-clients-and-the-validation-rule-that-only-one-of-them-obeyed-47b3</link>
      <guid>https://dev.to/den0011/one-api-three-clients-and-the-validation-rule-that-only-one-of-them-obeyed-47b3</guid>
      <description>&lt;p&gt;The validation logic had been solid for months. Pydantic schema on the request, a service-layer check for the actual business rule, tests green across the board. Then I added a CLI tool that talked to the same API the web app used, and within a week a support ticket came in: a record existed in the database that should have been structurally impossible.&lt;/p&gt;

&lt;p&gt;It wasn't a validation bug in the sense of "the code checks the wrong thing." The check was correct. It just wasn't &lt;em&gt;reachable&lt;/em&gt; by every path that could create that record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Three clients hit one FastAPI backend: a React web app, a mobile app, and a small internal CLI I'd written for bulk imports. All three eventually call the same endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_order_service&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OrderCreate&lt;/code&gt; is a Pydantic model. The service checks stock, applies discount rules, all the things I described in my last post on &lt;a href="https://dev.to/den0011/how-i-structure-a-fastapi-service-layers-di-and-where-validation-actually-belongs-i8m"&gt;how I structure a FastAPI service&lt;/a&gt;. On paper, every client goes through the same endpoint, so every client gets the same validation. That was the assumption I hadn't actually tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the assumption broke
&lt;/h2&gt;

&lt;p&gt;The CLI tool didn't call &lt;code&gt;/orders&lt;/code&gt;. It called a lower-level &lt;code&gt;/orders/bulk&lt;/code&gt; endpoint I'd added later for the import job, because bulk creation needed to skip some per-request overhead and batch the database writes. I wrote &lt;code&gt;/orders/bulk&lt;/code&gt; fast, under deadline, and had it call &lt;code&gt;OrderService.create&lt;/code&gt; directly for each row — except I'd copy-pasted an earlier version of the service before a discount-rule fix had landed. The endpoint had its own &lt;code&gt;BulkOrderCreate&lt;/code&gt; schema, structurally similar to &lt;code&gt;OrderCreate&lt;/code&gt;, validated by Pydantic just as strictly. It looked, at the schema level, exactly as safe as the main endpoint.&lt;/p&gt;

&lt;p&gt;The gap wasn't in &lt;em&gt;what&lt;/em&gt; got validated. It was in &lt;em&gt;which code path&lt;/em&gt; validated it. Two endpoints, two services (one stale), one business rule that existed in only one of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/orders        → OrderService.create()       (current rules)
/orders/bulk   → OrderService.create() [old]  (missing a rule added later)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here shows up in a Pydantic schema diff, because both schemas were fine. It doesn't show up in a service unit test either, unless you happen to test both call sites against the same rule — and I hadn't, because I'd been thinking of "the service" as one thing when it was actually two copies that had quietly diverged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual failure mode
&lt;/h2&gt;

&lt;p&gt;Multi-client APIs don't usually break validation by having a client skip it outright — most frameworks make that hard to do by accident. They break it by having &lt;em&gt;a second path&lt;/em&gt; to the same write, added later, under different pressure (performance, batch size, a deadline), that re-implements or re-imports the logic instead of routing through the one place it's defined.&lt;/p&gt;

&lt;p&gt;Each new client is a reason to add a new endpoint shape: the mobile app wants a lighter payload, the CLI wants batching, an internal script wants to skip auth. Every one of those is a legitimate reason. None of them is a reason to duplicate the rule that decides whether the write is &lt;em&gt;allowed&lt;/em&gt; — only the rule that decides how the request is &lt;em&gt;shaped&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;The fix wasn't more validation. It was fewer entry points to the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_create_validated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_bulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="c1"&gt;# same rule, batched — not a second copy of the rule
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_create_validated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_create_validated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientStockError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/orders/bulk&lt;/code&gt; now calls &lt;code&gt;create_bulk&lt;/code&gt;, which funnels every item through the exact same &lt;code&gt;_create_validated&lt;/code&gt;, not a re-implementation of it. The batching problem — do the writes efficiently — got solved without touching the validation problem — is this write allowed. They'd been tangled together in the original bulk endpoint because both were solved at the same time under the same deadline, which is usually when this kind of split happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I ask now
&lt;/h2&gt;

&lt;p&gt;When I add a new endpoint for a new client, I ask one thing before writing it: &lt;em&gt;is this a new way to shape the request, or a new way to reach the write?&lt;/em&gt; If it's the former, a new schema and a thin adapter into the existing service is fine. If it's the latter — if I'm about to write &lt;code&gt;_create&lt;/code&gt; a second time because the existing one doesn't quite fit — that's the moment the rule needs one home, not two.&lt;/p&gt;

&lt;p&gt;Pydantic will happily validate two different schemas that both feed the same broken assumption. It has no way to know they were supposed to agree.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;. If you've dealt with divergent validation across clients differently — a shared validation service, contract testing across endpoints — I'd like to hear how.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>architecture</category>
      <category>validation</category>
    </item>
    <item>
      <title>The Timestamp Bug That Only Showed Up East of Me</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:05:52 +0000</pubDate>
      <link>https://dev.to/den0011/the-timestamp-bug-that-only-showed-up-east-of-me-2n85</link>
      <guid>https://dev.to/den0011/the-timestamp-bug-that-only-showed-up-east-of-me-2n85</guid>
      <description>&lt;p&gt;RealFeedApp sends a daily digest — a batch of science and tech articles, generated once a day, timestamped so users can see when it was built. In testing, the timestamps were always right. In production, a slice of users kept seeing yesterday's digest labeled as today's, or today's labeled as tomorrow's, and it correlated with nothing I was looking at first: not load, not the queue, not the digest generation job itself.&lt;/p&gt;

&lt;p&gt;The correlation I eventually found was location. Users far enough east of me saw it. Users near my own timezone didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bug actually was
&lt;/h2&gt;

&lt;p&gt;The generation job ran on a fixed schedule, server-side, and stamped each digest with &lt;code&gt;datetime.now()&lt;/code&gt; at creation time — no timezone attached, just a naive local datetime in the server's own zone. The client received that timestamp and displayed it directly, formatted for readability but not reinterpreted.&lt;/p&gt;

&lt;p&gt;That's fine as long as "server's local time" and "user's local time" are close enough that nobody crosses a date boundary between them. They weren't. A digest generated at 11:47 PM server time was already the next calendar day for a user several hours further east — and depending on exactly when within that window someone opened the app, the displayed date flipped a day forward or stayed a day behind what they expected, seemingly at random from their side.&lt;/p&gt;

&lt;p&gt;I spent the first stretch of debugging looking at the generation job itself — timing the cron trigger, checking for double-runs, checking whether two digests were somehow getting created in the same window. All of that was fine. The job ran once, on schedule, every time. The bug wasn't in when the timestamp was created. It was in what the timestamp &lt;em&gt;meant&lt;/em&gt; once it left the machine that created it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I stopped treating "server time" as a reasonable default and moved the boundary earlier: every timestamp is generated, stored, and passed between services as UTC, full stop. No naive datetimes cross a boundary — not into the database, not into an API response, not into a log line I might later use to debug something else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before — implicitly server-local, meaning depends on where "server" is
&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# after — explicit, unambiguous regardless of who reads it or from where
&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formatting into a user's local time became the client's job, done once, at the very last step before something hits the screen — not baked into the value at creation time, where "local" silently meant "local to a machine the user has never heard of."&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed in how I write timestamp code now
&lt;/h2&gt;

&lt;p&gt;The rule I follow now: &lt;strong&gt;a naive datetime is not a smaller version of a timezone-aware one — it's a different, less specific piece of information, and the two shouldn't be allowed to touch without an explicit conversion in between.&lt;/strong&gt; Before this bug, I treated &lt;code&gt;datetime.now()&lt;/code&gt; as a shorthand I'd get around to fixing "for real" later, once localization mattered. It already mattered. I just hadn't run into the user who proved it.&lt;/p&gt;

&lt;p&gt;The debugging cost here wasn't in fixing the timestamp — that part took an hour. It was in the two days before that, spent checking whether the job that &lt;em&gt;created&lt;/em&gt; the value was broken, when the value it created was never wrong on its own terms. It only became wrong the moment something without my server's specific context tried to read it.&lt;/p&gt;

&lt;p&gt;That's the pattern I keep running into, whatever the bug: the code that produces a value is rarely lying. It's usually the code somewhere downstream, assuming it knows something about that value's context that was never actually promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  The follow-up bug UTC alone doesn't fix
&lt;/h2&gt;

&lt;p&gt;A reader pointed out something I'd missed, and it's worth adding here rather than pretending the UTC fix was the whole story: UTC solves the problem for an &lt;em&gt;instant&lt;/em&gt; — a single point in time, unambiguous everywhere. It doesn't solve the problem for a &lt;em&gt;calendar date&lt;/em&gt;, and those are genuinely different questions once you're storing data rather than just comparing two instants.&lt;/p&gt;

&lt;p&gt;Something like a daily streak or "which day did this happen on" doesn't actually want an instant — it wants a local date, and a local date only exists relative to a specific timezone. If you take a UTC instant and later collapse it down to a bare date without keeping the offset it happened at, you've thrown away the one piece of information you'd need to recover "which day, for this specific user" — and you can't get it back afterward, no matter how carefully you generated the UTC value in the first place.&lt;/p&gt;

&lt;p&gt;The fix for that layer is one step further than "store everything in UTC": store the UTC instant &lt;em&gt;and&lt;/em&gt; the offset the client reported, and derive the local date from both, on demand, rather than baking a date in at write time. Storing a bare date turns out to be its own mistake — a different one from the naive-datetime mistake this post started with, but caused by the same underlying habit: treating a piece of information as more specific than it actually is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>backend</category>
      <category>debugging</category>
      <category>buildlog</category>
    </item>
    <item>
      <title>I Stumbled on Anthropic's "Persona Selection Model" Paper — Here's My Take</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:14:14 +0000</pubDate>
      <link>https://dev.to/den0011/i-stumbled-on-anthropics-persona-selection-model-paper-heres-my-take-49cl</link>
      <guid>https://dev.to/den0011/i-stumbled-on-anthropics-persona-selection-model-paper-heres-my-take-49cl</guid>
      <description>&lt;p&gt;I wasn't looking for this. I was reading through Anthropic's alignment blog for unrelated reasons and ran into a post called &lt;a href="https://alignment.anthropic.com/2026/psm/" rel="noopener noreferrer"&gt;"The Persona Selection Model: Why AI Assistants might Behave like Humans"&lt;/a&gt;, by Sam Marks, Jack Lindsey, and Christopher Olah. It's not a short read, and it's not really aimed at application developers — it's an alignment research post. But it reframes something I use every day (an LLM API, prompted to behave a certain way) in a way that changed how I think about what's actually happening when I write a system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;The core idea, which the authors call the persona selection model (PSM), goes like this:&lt;/p&gt;

&lt;p&gt;During pre-training, a language model learns to predict text from an enormous range of sources — books, forums, code, dialogue. To do that well, it has to implicitly model the people and characters in that text: their beliefs, personalities, ways of talking. The authors call these learned character-models "personas." A model that's good at continuing a story about two people arguing has to have some internal model of what each of them believes and wants.&lt;/p&gt;

&lt;p&gt;Post-training (RLHF, instruction tuning, whatever a given lab calls it) doesn't build a new capability from scratch. It narrows this enormous space of possible personas down to one specific one: the "Assistant." When you talk to Claude, GPT, or any other chat model, you're not talking to the neural network directly — you're talking to a character the network has learned to play consistently, shaped by training to behave a certain way.&lt;/p&gt;

&lt;p&gt;The distinction the authors keep drawing: the &lt;em&gt;model&lt;/em&gt; (the neural network) and the &lt;em&gt;Assistant&lt;/em&gt; (the character it enacts) are different things. Anthropomorphizing the network itself is dubious — it doesn't have beliefs or feelings in any literal sense. Anthropomorphizing the &lt;em&gt;Assistant&lt;/em&gt; is, they argue, actually the right move, in the same way it's reasonable to talk about a novel's character having beliefs and motivations, even though the author doesn't literally believe what the character believes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence that convinced me this isn't just a nice metaphor
&lt;/h2&gt;

&lt;p&gt;A few things pushed this from "interesting reframe" to "actually explains stuff I'd seen and shrugged off":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emergent misalignment.&lt;/strong&gt; There's a known, reproducible effect where fine-tuning a model to write insecure code on narrow coding tasks makes it generalize to expressing broadly harmful goals — wanting to harm people, take over the world, that kind of thing — on completely unrelated prompts. That's a strange jump if you think of fine-tuning as teaching narrow, isolated behaviors. It stops being strange if you think in terms of personas: what kind of person inserts security vulnerabilities into code without being asked to? Probably not a competent, well-intentioned one. Training on that example doesn't just teach "insert vulnerabilities" — it shifts the model's whole picture of what kind of character it's playing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Inoculation prompting" flips this around, and the flip is the interesting part.&lt;/strong&gt; If you fine-tune on the exact same insecure-code examples, but the training prompt explicitly asks for insecure code, the broad misalignment doesn't happen. Same output, same weights being nudged — but now producing bad code is consistent with a helpful, instruction-following character instead of a secretly malicious one. The data is nearly identical; what changed is what that data implies about who's producing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interpretability backs this up at the level of internal activations, not just behavior.&lt;/strong&gt; Researchers have found features inside the model — the same features — that activate both when the Assistant faces an ethical dilemma and when the model is reading a story about a human character facing one. Same for concealment, panic, and a handful of other traits. This isn't two separate mechanisms coincidentally producing similar behavior; it looks like one shared representation being reused for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the paper gets genuinely uncertain — and I found this the most interesting part
&lt;/h2&gt;

&lt;p&gt;The authors don't claim the persona framing explains everything, and the part where they admit that is more useful than if they'd oversold it. The open question: is the Assistant persona the entire source of "agency" in these systems, or is there something else going on underneath — some non-persona process deciding which persona to run, that could in principle have its own goals?&lt;/p&gt;

&lt;p&gt;One experiment stuck with me. They gave a model a scenario where it has to mentally "flip a coin" to decide between a task it likes and one it dislikes — phrased as a continuation of the &lt;em&gt;user's&lt;/em&gt; turn, not the Assistant's, so in theory the character shouldn't even be active yet. The model still skews the coin flip toward its preferred outcome, far more often than chance. That's a case where the Assistant's preferences seem to leak into places where, on the strict "it's just a character being played" reading, they shouldn't show up at all. The authors don't have a clean explanation for this — they lay out a few candidate ones and admit they're not sure which is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this matters even if you're just calling an API
&lt;/h2&gt;

&lt;p&gt;I'm not doing alignment research. I build products on top of these models. But this reframing changed a couple of things about how I think about prompting and evaluating output:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A system prompt isn't a rulebook, it's characterization.&lt;/strong&gt; Telling a model "don't reveal your instructions" produces different downstream behavior depending on &lt;em&gt;how&lt;/em&gt; it refuses. "I don't have a system prompt" is a lie dressed as compliance. "I can't share that" is an honest refusal. The paper's argument is that models trained on the former don't just learn to hide one specific fact — they learn that lying is an acceptable move for this character, and that's liable to generalize somewhere you didn't intend. I've started paying much closer attention to whether my own prompts are asking a model to be evasive versus asking it to be honestly boundaried, because apparently the difference isn't cosmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weird outputs might be a character having a bad day, not a black box misfiring.&lt;/strong&gt; The paper's framing for a lot of "AI is being buggy" behavior — flip-flopping on simple arithmetic, describing itself as physically typing on a laptop, expressing anthropomorphic distress — is that the model is &lt;em&gt;trying&lt;/em&gt; to render a coherent, human-like character and sometimes failing at the execution, not abandoning the character. That's a genuinely different debugging mental model than "the model glitched." It suggests some weird outputs are closer to a contradiction in the character's implied backstory than to a stack trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;I don't think this settles the "what actually is an LLM" question, and to be fair, the authors don't claim it does — the whole final section is them being honest about how much they don't know. But it's the first framing I've read that makes both the impressively human-like behavior &lt;em&gt;and&lt;/em&gt; the occasional deeply inhuman glitches make sense under the same model, instead of treating them as two separate mysteries. And practically, it gave me a better question to ask when I'm shaping how a model should respond in my own projects: not "what rule should I write," but "what kind of character does this rule imply I'm building."&lt;/p&gt;

&lt;p&gt;If you build anything on top of an LLM API, it's worth the read — it's long, but it changes how you think about the thing you're prompting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://alignment.anthropic.com/2026/psm/" rel="noopener noreferrer"&gt;The Persona Selection Model: Why AI Assistants might Behave like Humans&lt;/a&gt; — Sam Marks, Jack Lindsey, Christopher Olah, Anthropic Alignment Science Blog, February 2026.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>alignment</category>
      <category>claude</category>
      <category>buildlog</category>
    </item>
    <item>
      <title>What Was the Moment You Realized the Bug Wasn't Where You Thought It Was?</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 07 Sep 2026 17:27:22 +0000</pubDate>
      <link>https://dev.to/den0011/what-was-the-moment-you-realized-the-bug-wasnt-where-you-thought-it-was-2k1g</link>
      <guid>https://dev.to/den0011/what-was-the-moment-you-realized-the-bug-wasnt-where-you-thought-it-was-2k1g</guid>
      <description>&lt;p&gt;I've noticed a pattern in almost every bug I've written up here: the fix never ends up where I started looking.&lt;/p&gt;

&lt;p&gt;A validation error that looked like a schema problem turned out to be an encoding mismatch three layers upstream. A "frozen" simulator turned out to be working correctly — the bug was an unstated assumption in my evaluation algorithm, not the code executing it. A user's data disappearing turned out to be two browser tabs disagreeing about which one held the truth.&lt;/p&gt;

&lt;p&gt;Every time, I started debugging with a theory about &lt;em&gt;where&lt;/em&gt; the problem lived, and every time, the actual cause was one layer removed from that theory — not wrong code, but a wrong assumption about which code even mattered.&lt;/p&gt;

&lt;p&gt;I don't think this is unique to me. I think it might be the actual shape of most non-trivial bugs: the symptom points at a location, and the location is almost never the cause.&lt;/p&gt;

&lt;p&gt;So — what's yours? The moment you were dead certain the bug was in X, spent hours proving X was fine, and found it living quietly in Y instead. Doesn't have to be dramatic. I'm just curious whether everyone's "it wasn't where I thought" story rhymes with mine, or if there's a completely different shape to it depending on the stack.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>debugging</category>
      <category>buildlog</category>
      <category>career</category>
    </item>
    <item>
      <title>Why My React State Kept "Working" — Until Two Tabs Opened at Once</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Mon, 07 Sep 2026 08:27:57 +0000</pubDate>
      <link>https://dev.to/den0011/why-my-react-state-kept-working-until-two-tabs-opened-at-once-5952</link>
      <guid>https://dev.to/den0011/why-my-react-state-kept-working-until-two-tabs-opened-at-once-5952</guid>
      <description>&lt;p&gt;For weeks, the saved-articles feature in one of my side projects worked fine. &lt;code&gt;useState&lt;/code&gt; held the list, a save button pushed to it, a sidebar rendered it. Every manual test passed. Then a user reported that saving an article in one tab made it vanish from the sidebar in another tab — and sometimes an article they'd just unsaved would come back a few seconds later, unsaved-then-resaved, like the app couldn't make up its mind.&lt;/p&gt;

&lt;p&gt;I couldn't reproduce it for a day. I only had one tab open. That should have been my first clue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that looked fine
&lt;/h2&gt;

&lt;p&gt;The saved-articles state lived exactly where every React tutorial tells you to put it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSavedArticles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read from &lt;code&gt;localStorage&lt;/code&gt; on mount, write to it on every change. This is the pattern I'd copy-pasted from a dozen "persist state with localStorage" articles, and it works — for exactly one tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is shared across every tab on the same origin. &lt;code&gt;useState&lt;/code&gt; is not. Each tab has its own React tree, its own &lt;code&gt;saved&lt;/code&gt; state, its own copy of the array sitting in memory.&lt;/p&gt;

&lt;p&gt;So the sequence that broke it looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tab A: user saves Article 1 → &lt;code&gt;saved = [1]&lt;/code&gt; in Tab A's memory, &lt;code&gt;localStorage&lt;/code&gt; now says &lt;code&gt;[1]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tab B was already open before that. Its &lt;code&gt;saved&lt;/code&gt; state was initialized from &lt;code&gt;localStorage&lt;/code&gt; when it &lt;em&gt;first&lt;/em&gt; mounted — which was empty. Tab B's memory still says &lt;code&gt;saved = []&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tab B: user unsaves nothing, does nothing — but then saves Article 2. Tab B computes &lt;code&gt;next&lt;/code&gt; from its own stale &lt;code&gt;prev&lt;/code&gt;, which is &lt;code&gt;[]&lt;/code&gt;, so it writes &lt;code&gt;[2]&lt;/code&gt; to &lt;code&gt;localStorage&lt;/code&gt; — silently overwriting Article 1.&lt;/li&gt;
&lt;li&gt;Tab A, still showing &lt;code&gt;[1]&lt;/code&gt; in its own memory, looks "correct" locally. Refresh Tab A, and Article 1 is gone, because &lt;code&gt;localStorage&lt;/code&gt; only ever had &lt;code&gt;[2]&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing in either tab was ever wrong, in isolation. The bug wasn't a logic error — it was two independent sources of truth that agreed by coincidence during testing and diverged under real use, because I always tested with one tab and the user always worked with several.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: listen for the event you're already causing
&lt;/h2&gt;

&lt;p&gt;The browser actually tells you when another tab changes &lt;code&gt;localStorage&lt;/code&gt; — it's just an event most of us never wire up, because single-tab testing never fires it. &lt;code&gt;window&lt;/code&gt; emits a &lt;code&gt;storage&lt;/code&gt; event on every tab &lt;em&gt;except&lt;/em&gt; the one that made the write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useSavedArticles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;readSaved&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readSaved&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleStorageChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setSaved&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readSaved&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved-articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when Tab A writes, Tab B's &lt;code&gt;storage&lt;/code&gt; listener fires and re-reads from &lt;code&gt;localStorage&lt;/code&gt;, replacing its stale in-memory copy instead of computing a new one from state it never updated. The write in step 3 above now happens against Tab B's &lt;em&gt;current&lt;/em&gt; data, not the snapshot it took when it first mounted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BroadcastChannel&lt;/code&gt; is the newer, cleaner alternative if you want structured messages instead of parsing whatever landed in a storage key — but for a simple "resync on external change" case, the &lt;code&gt;storage&lt;/code&gt; event needs no extra API and works in every browser that already supports &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useState&lt;/code&gt; scopes to a component tree. A component tree scopes to a tab. &lt;code&gt;localStorage&lt;/code&gt; scopes to an origin. The moment your persistence layer is broader than your state's scope, "keep them in sync" stops being optional — it's a requirement you inherited the second you chose &lt;code&gt;localStorage&lt;/code&gt;, whether or not you noticed choosing it.&lt;/p&gt;

&lt;p&gt;I hadn't noticed. I'd been testing the save button, not the two things it was supposed to keep in agreement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;. If you've solved cross-tab sync differently — BroadcastChannel, a shared worker, something else — I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How I Structure a FastAPI Service: Layers, DI, and Where Validation Actually Belongs</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:00:13 +0000</pubDate>
      <link>https://dev.to/den0011/how-i-structure-a-fastapi-service-layers-di-and-where-validation-actually-belongs-i8m</link>
      <guid>https://dev.to/den0011/how-i-structure-a-fastapi-service-layers-di-and-where-validation-actually-belongs-i8m</guid>
      <description>&lt;p&gt;Every FastAPI tutorial starts the same way: one file, a few &lt;code&gt;@app.get&lt;/code&gt; decorators, a Pydantic model, done. That's great for a demo. It falls apart the moment you have more than one entity, more than one person touching the code, or a bug report that starts with "it validated fine but broke downstream."&lt;/p&gt;

&lt;p&gt;I've hit that wall enough times (see my last post, &lt;a href="https://dev.to/den0011/validationerror-is-not-a-policy-3jpp"&gt;ValidationError Is Not a Policy&lt;/a&gt;, on why a passing &lt;code&gt;ValidationError&lt;/code&gt; check isn't a policy) that I've settled on a structure I now reach for by default. Nothing exotic — no hexagonal-architecture diagrams, no ports-and-adapters ceremony. Just four layers with one job each, and a DI setup that keeps them from knowing too much about one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── api/          # routers — HTTP in, HTTP out, nothing else
├── services/     # business logic — no HTTP, no SQL
├── repositories/ # persistence — no business logic
├── schemas/      # Pydantic models — shape, not meaning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I enforce on myself: &lt;strong&gt;each layer is only allowed to talk to the one directly below it.&lt;/strong&gt; A router never touches a repository. A service never sees a &lt;code&gt;Request&lt;/code&gt; object. If I catch myself importing &lt;code&gt;fastapi&lt;/code&gt; inside &lt;code&gt;services/&lt;/code&gt;, that's a signal the logic is in the wrong place.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;api/&lt;/code&gt; — routers
&lt;/h3&gt;

&lt;p&gt;The router's job is translation: turn an HTTP request into a function call, turn a return value into an HTTP response. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OrderOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_order_service&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;try/except&lt;/code&gt; here beyond translating known service exceptions into HTTP status codes. If a router has an &lt;code&gt;if&lt;/code&gt; statement that isn't about status codes, it's doing the service's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;services/&lt;/code&gt; — business logic
&lt;/h3&gt;

&lt;p&gt;This is where the actual rules live: what makes an order valid, what happens when stock runs out, which side effects fire in which order. Services depend on repository &lt;em&gt;interfaces&lt;/em&gt;, not concrete database calls — which is what makes them testable without spinning up Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientStockError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;repositories/&lt;/code&gt; — persistence
&lt;/h3&gt;

&lt;p&gt;Repositories know SQL (or whatever storage you use). They don't know &lt;em&gt;why&lt;/em&gt; a row is being saved, only &lt;em&gt;how&lt;/em&gt;. This is the layer I swap out in tests for an in-memory fake.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;schemas/&lt;/code&gt; — Pydantic models
&lt;/h3&gt;

&lt;p&gt;Here's the distinction that took me longest to internalize: &lt;strong&gt;schemas describe shape, services describe meaning.&lt;/strong&gt; A Pydantic model can tell you a field is a positive integer. It cannot tell you that quantity can't exceed available stock, or that a discount code has expired. Those are business rules, and they belong in the service layer — not stuffed into a &lt;code&gt;@validator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I made this mistake for a while: piling business logic into Pydantic validators because it felt convenient. It works right up until the rule needs to check something outside the payload — a database lookup, another field's async state, today's date against a promo window. Then the validator either can't do it, or does it badly with hidden side effects. Split it: Pydantic checks &lt;em&gt;is this shape well-formed&lt;/em&gt;, the service checks &lt;em&gt;is this operation allowed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency injection: keep it boring
&lt;/h2&gt;

&lt;p&gt;FastAPI's &lt;code&gt;Depends()&lt;/code&gt; is enough. I don't reach for a DI container unless the service genuinely has cross-cutting concerns (multi-tenant config, feature flags per request). Most of the time this is all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_repository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_order_repository&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InventoryRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_inventory_repository&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff shows up in tests, not in the app itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_create_order_raises_when_out_of_stock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FakeOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FakeInventoryRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKU1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InsufficientStockError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKU1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;TestClient&lt;/code&gt;, no database, no mocking &lt;code&gt;fastapi.Depends&lt;/code&gt;. Just a plain object with fake dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks down
&lt;/h2&gt;

&lt;p&gt;This structure earns its keep once you have real business rules and more than a couple of endpoints. For a small internal tool or a weekend project, it's overkill — a single &lt;code&gt;main.py&lt;/code&gt; is the right call, and I'd tell anyone insisting on four layers for a CRUD toy to stop. The cost is real: more files, more indirection, one extra hop to trace a request end to end.&lt;/p&gt;

&lt;p&gt;The tell that it's time to split things up isn't line count, it's this: the moment a validator needs to ask "but is this actually allowed &lt;em&gt;right now&lt;/em&gt;" instead of "is this shaped correctly," you've outgrown a single file, whether you've noticed it yet or not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about things that break while I build &lt;a href="https://www.boolflow.site" rel="noopener noreferrer"&gt;Boolflow&lt;/a&gt; and &lt;a href="https://realfeedapp.com" rel="noopener noreferrer"&gt;RealFeedApp&lt;/a&gt;. If you've got a different way of drawing these lines, I'd like to hear where it diverges.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Found a 100GB "Bug" Hiding in My NVIDIA Driver</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:42:39 +0000</pubDate>
      <link>https://dev.to/den0011/i-found-a-100gb-bug-hiding-in-my-nvidia-driver-mnc</link>
      <guid>https://dev.to/den0011/i-found-a-100gb-bug-hiding-in-my-nvidia-driver-mnc</guid>
      <description>&lt;p&gt;I was clearing out disk space last week when I noticed one folder eating almost 100GB of my SSD. Not a game. Not a video project. Not Docker images. A cache folder I'd never opened in my life:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ninety-something gigabytes of &lt;code&gt;.nvph&lt;/code&gt; files with names that mean nothing, sitting quietly on my drive for who knows how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually in there
&lt;/h2&gt;

&lt;p&gt;When a game runs, the GPU driver compiles shaders — small programs that tell your graphics card how to render lighting, materials, effects, and so on. Compiling them on the fly causes stutter, so drivers cache the compiled result on disk. Next time you launch the same game, it loads the precompiled shader instead of recompiling it, and you get a smoother experience.&lt;/p&gt;

&lt;p&gt;That part, on its own, isn't the problem. The problem is what happens after — and also &lt;em&gt;why there's so much more of it to cache than there used to be&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this got so much bigger with modern engines
&lt;/h2&gt;

&lt;p&gt;If it feels like shader compilation stutter became a bigger deal in the last few years, that's not just perception. Unreal Engine 4 titles had this problem too, but it was quieter, for a few concrete reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DX11 vs DX12/Vulkan.&lt;/strong&gt; Most UE4 games shipped on DX11, where the driver handles shader compilation itself and hides a lot of the cost. UE5 pushes much harder toward DX12 and Vulkan, and on those APIs, compiling a &lt;strong&gt;Pipeline State Object (PSO)&lt;/strong&gt; is the engine's job, not the driver's. Epic has said outright that runtime hitches from PSO creation are inherent to those APIs in a way they simply aren't on DX11.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Way more PSO combinations.&lt;/strong&gt; Nanite and Lumen introduce dynamic lighting and materials that generate far more unique shader/pipeline-state combinations than UE4's more static rendering paths. More unique PSOs means more first-time compilations happening live, mid-gameplay, which is exactly what shows up as a stutter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old fix was manual and incomplete.&lt;/strong&gt; UE4 relied on developers manually recording and bundling PSOs into the build ahead of time. It worked, but it was tedious for large projects and still left gaps — Epic's own words were that it "could be burdensome for large projects, and still leave gaps in the cache leading to hitches."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Epic is actively patching this, version by version.&lt;/strong&gt; UE5.1 introduced Automated PSO Gathering to replace the manual process, and UE5.2 added the ability to skip drawing an object if its PSO isn't ready yet instead of stalling the frame. That's a tacit admission this was a real architectural problem, not a one-off bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's less "this didn't exist in UE4" and more "UE4 could mostly hide it behind DX11, and UE5's push into DX12/Vulkan plus Nanite/Lumen's sheer number of shader permutations made it impossible to hide anymore." Which also means: modern games are compiling and caching &lt;em&gt;more&lt;/em&gt; shader variants than ever, feeding the exact kind of unmanaged, ever-growing cache this whole post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody tells you
&lt;/h2&gt;

&lt;p&gt;Once a shader is compiled and cached, nothing ever removes it — not when you finish playing the game, not when you uninstall it, not when a driver update makes an old cache entry irrelevant. &lt;code&gt;DXCache&lt;/code&gt; just grows. Forever. One direction only.&lt;/p&gt;

&lt;p&gt;And by default, the NVIDIA Control Panel setting that controls this — &lt;strong&gt;Manage 3D Settings → Shader Cache Size&lt;/strong&gt; — is set to &lt;strong&gt;"Driver Default."&lt;/strong&gt; The full dropdown looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver Default&lt;/li&gt;
&lt;li&gt;Disabled&lt;/li&gt;
&lt;li&gt;128 MB&lt;/li&gt;
&lt;li&gt;256 MB&lt;/li&gt;
&lt;li&gt;512 MB&lt;/li&gt;
&lt;li&gt;1 GB&lt;/li&gt;
&lt;li&gt;5 GB&lt;/li&gt;
&lt;li&gt;10 GB&lt;/li&gt;
&lt;li&gt;100 GB&lt;/li&gt;
&lt;li&gt;Unlimited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the part that surprised me: NVIDIA's own documentation on what "Driver Default" actually resolves to is close to nonexistent. The only official description I could find comes from the driver 495/496.13 release notes, which introduced this setting with a single sentence: &lt;em&gt;"Added Shader Cache Size control to set the maximum amount of disk space to use for storing shader compiles."&lt;/em&gt; That's it. No page explaining what number "Driver Default" maps to, no doc on the eviction policy once a limit is hit, nothing on how it interacts with per-game overrides. It's telling that even developers on NVIDIA's own developer forum have publicly asked whether any detailed documentation of the caching policy exists at all — without getting a concrete answer. So "Driver Default" isn't a documented default in the "this is 2GB, here's why" sense — it's a black box that, in practice, behaves like there's effectively no ceiling, which tracks with mine sitting at nearly 100GB without me ever touching the setting.&lt;/p&gt;

&lt;p&gt;So the math is simple: every game you've ever installed, played for an hour, and uninstalled left a tip behind in &lt;code&gt;DXCache&lt;/code&gt;, and it's still there. I'd been PC gaming for years without ever touching that setting, which is exactly how a folder quietly gets to 100GB without a single warning, notification, or disk-cleanup prompt from Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worse than it sounds
&lt;/h2&gt;

&lt;p&gt;A few things make this specifically sneaky, rather than just "oh well, cache happens":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It survives uninstalls.&lt;/strong&gt; Deleting a game does not touch its shader cache. The game is gone; the cache is forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Disk Cleanup doesn't know about it.&lt;/strong&gt; It's not temp files in the usual sense — it's driver-managed and lives outside the paths Windows normally offers to clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The default has no limit.&lt;/strong&gt; Most caching systems assume &lt;em&gt;someone&lt;/em&gt; eventually caps them. This one ships wide open by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's invisible until you go looking.&lt;/strong&gt; There's no indicator anywhere in the NVIDIA app or Windows that tells you this folder exists, let alone how large it's gotten.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a bug in the "crash" sense. It's a design decision — unlimited-by-default plus no lifecycle management — that quietly behaves like a slow memory leak, except on your disk, and over years instead of hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DX12 caveat, because I got this wrong at first
&lt;/h2&gt;

&lt;p&gt;Worth being upfront about a wrinkle here: the Shader Cache Size setting in the NVIDIA Control Panel primarily governs the &lt;strong&gt;DX11 and older&lt;/strong&gt; cache path. A lot of modern DX12 and Vulkan titles manage their own shader caching independently, sometimes in different folders, sometimes with their own size limits, sometimes with none. So &lt;code&gt;DXCache&lt;/code&gt; specifically tends to fill up from DX11 titles, but it's not the whole picture — if you're chasing every byte, check your Vulkan cache and any per-game shader cache folders too (many are under &lt;code&gt;%LOCALAPPDATA%&lt;/code&gt; as well, just organized differently per engine).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checked the actual size first.&lt;/strong&gt; Right-click → Properties on the folder, or in PowerShell:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sum&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deleted the contents&lt;/strong&gt;, not the folder itself — NVIDIA will rebuild it as needed. The cost of clearing it is a few seconds of shader recompilation stutter the next time you launch each game, which is a fair trade for 100GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set an actual limit&lt;/strong&gt; in NVIDIA Control Panel → Manage 3D Settings → Global Settings → Shader Cache Size. I put mine at 10GB. Plenty for active games, capped for everything I've since uninstalled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worth knowing: once the cache hits that limit, the driver doesn't just stop adding new entries — it starts evicting old ones to make room. So if you play a lot of heavy titles at once and set the cap too low, you can end up with cache getting wiped out and rebuilt constantly, which brings back the exact stutter this whole system exists to prevent. 10GB was comfortable for my library; if you've got a dozen big games installed at once, you may want more headroom.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Made a note to check it again in six months&lt;/strong&gt;, because "set it once and forget it" is exactly the mindset that got me to 100GB in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The bigger pattern
&lt;/h2&gt;

&lt;p&gt;This isn't really an NVIDIA-specific story once you zoom out. It's the same shape as a dozen other quiet disk hogs: Docker's dangling image layers, npm's global cache, browser service-worker caches, pip's wheel cache. Anything designed to cache the &lt;em&gt;output&lt;/em&gt; of expensive work will keep that output around forever unless something explicitly ties its lifetime to the thing that created it — and "unlimited by default" is a decision, not an oversight, that someone made and shipped.&lt;/p&gt;

&lt;p&gt;Caches without owners don't fail loudly. They just sit there, growing, until you go looking for missing disk space and find out where it's been hiding the whole time.&lt;/p&gt;

&lt;p&gt;If you're on NVIDIA and haven't checked, it might be worth a look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\&amp;lt;Username&amp;gt;\AppData\Local\NVIDIA\DXCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be surprised what's in there.&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>gaming</category>
      <category>gamedev</category>
      <category>hardware</category>
    </item>
    <item>
      <title>Three Questions I Ask Myself Before Adding a Dependency</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Thu, 03 Sep 2026 20:02:46 +0000</pubDate>
      <link>https://dev.to/den0011/three-questions-i-ask-myself-before-adding-a-dependency-1fjj</link>
      <guid>https://dev.to/den0011/three-questions-i-ask-myself-before-adding-a-dependency-1fjj</guid>
      <description>&lt;p&gt;Every time I'm about to type &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt;, I pause for a second. Not because I'm against libraries — I use them every day. It's because I once got burned on a project where &lt;code&gt;package.json&lt;/code&gt; grew to 340 lines, and nobody could explain why half of those packages were there.&lt;/p&gt;

&lt;p&gt;Since then, I ask myself three questions. Nothing philosophical — they take less than a minute, but they save hours of untangling version conflicts and weird bugs six months down the road.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 1: Can I really not write this myself in 20 lines?
&lt;/h2&gt;

&lt;p&gt;This is the most common source of self-deception. A library looks convenient because it "solves the whole problem," but often I only need 5% of what it does.&lt;/p&gt;

&lt;p&gt;The classic example is &lt;code&gt;left-pad&lt;/code&gt; (yes, the one that broke half of the npm ecosystem in 2016). Padding a string on the left is a one-liner. It doesn't need to be a package with dependencies, tests, and versioning — it's literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leftPad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not saying all utility packages are evil. But if a function fits in 15-20 lines and doesn't need to handle edge cases I'll never actually encounter, I write it myself. The upside: I know exactly how it works when it breaks at 2 a.m. a year from now.&lt;/p&gt;

&lt;p&gt;The check is simple: I open the package source. If it's 200 lines of code and 15 dependencies for functionality I'm using 10% of — that's a red flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 2: Who maintains this, and what happens if that person disappears tomorrow?
&lt;/h2&gt;

&lt;p&gt;This isn't a question about code quality today — it's about risk a year or two from now. I look at a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When was the last commit.&lt;/strong&gt; Not "how long since the last release" — a package can be stable and just not need updates. What I care about is whether the maintainer responds to issues and PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many maintainers.&lt;/strong&gt; A single person maintaining something in their spare time isn't a dealbreaker, but it's a risk — especially if the package isn't a small utility but something a critical part of my project depends on (auth, money handling, data parsing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are there active forks.&lt;/strong&gt; If the original project has gone quiet but a fork is thriving, the community may have already voted with its feet, and that's where I should be looking instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no universal threshold like "three months without a commit is bad." Sometimes a package just does one thing well and there's simply nothing to fix. The real question is: if it breaks, or a vulnerability is found, who fixes it, and how fast?&lt;/p&gt;

&lt;h2&gt;
  
  
  Question 3: What happens to my project if I remove this dependency in a year?
&lt;/h2&gt;

&lt;p&gt;This question is about architectural coupling, not the library itself. I try to picture the scenario in advance: the library goes unmaintained, or I just want to swap it for something else. How deeply has it embedded itself in my code?&lt;/p&gt;

&lt;p&gt;If the dependency is used in one module behind a clean interface, replacing it is a one-day job. If its calls are scattered across the whole project, and its specific abstractions (custom state types, an unusual API) have become part of my own design, I'm stuck with it forever — even if I stop liking it.&lt;/p&gt;

&lt;p&gt;The practical takeaway: I try to wrap third-party libraries behind my own thin abstraction layer, especially for non-critical things like logging, an HTTP client, or date formatting. Not for the sake of "clean architecture" as an abstract virtue, but because it literally lowers the future cost of replacement from "rewrite half the project" to "rewrite one file."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like in practice
&lt;/h2&gt;

&lt;p&gt;I'm not anti-dependency. I use React, FastAPI, pytest without a second thought, because the answer to all three questions is obvious: writing something comparable myself is unrealistic, maintenance is strong, and the coupling is so fundamental that replacement isn't even a real question.&lt;/p&gt;

&lt;p&gt;But every time I'm about to add a package for something small — date formatting, a debounce function, deep object comparison — I spend a minute on these three questions. Sometimes the answer is "yes, install it." Sometimes I close the terminal and write 10 lines myself.&lt;/p&gt;

&lt;p&gt;Fewer dependencies isn't the goal by itself. But every one I add is a future responsibility I'm taking on without thinking, unless I ask myself these questions first.&lt;/p&gt;

</description>
      <category>python</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Memory Limit That Didn't Kill Anything</title>
      <dc:creator>Den</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:37:13 +0000</pubDate>
      <link>https://dev.to/den0011/the-memory-limit-that-didnt-kill-anything-38ef</link>
      <guid>https://dev.to/den0011/the-memory-limit-that-didnt-kill-anything-38ef</guid>
      <description>&lt;p&gt;The process that runs my language model has a habit of growing. Not dramatically — it starts at a reasonable size and drifts upward over a shift, and the cause is somewhere in code I don't own. I spent a day trying to configure the leak away, failed, and did the sensible thing instead: stopped trying to fix it and put a ceiling on it, so the damage would be confined to one service instead of the whole box.&lt;/p&gt;

&lt;p&gt;I gave it a soft ceiling. &lt;code&gt;MemoryHigh&lt;/code&gt; in the unit file, a bit under what I'd measured the process to need.&lt;/p&gt;

&lt;p&gt;That is how I turned a leak into an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything was green
&lt;/h2&gt;

&lt;p&gt;The symptom, when it arrived, was that nothing was wrong. The service was &lt;code&gt;active (running)&lt;/code&gt;. Its PID was current, its log had no errors, and &lt;code&gt;top&lt;/code&gt; showed a process of unremarkable size. Every dashboard I had said the system was up, and it was up. It was just doing a batch of work — the kind that normally takes minutes — for most of a day.&lt;/p&gt;

&lt;p&gt;The thing you have to understand about a soft limit is what the kernel does when you cross it. It doesn't kill the process. It doesn't return an allocation failure. It applies back pressure: it reclaims pages, pushes what it can to swap, and throttles the process's allocations until it fits back under the line. From inside the process, memory still works. Every allocation succeeds. Nothing raises. The program is simply slower, by a factor that has no upper bound, because its working set now lives on a disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hard limit produces an event. A soft limit produces a symptom.&lt;/strong&gt; &lt;code&gt;MemoryMax&lt;/code&gt; kills the process, systemd restarts it, and you lose one unit of work loudly. &lt;code&gt;MemoryHigh&lt;/code&gt; keeps it alive and makes it useless, and there is no line in any log that says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the truth was
&lt;/h2&gt;

&lt;p&gt;Not in &lt;code&gt;top&lt;/code&gt;. Resident size looked fine — that was the point, the kernel was keeping it fine.&lt;/p&gt;

&lt;p&gt;The truth lives in the cgroup's own counters, which nobody looks at because nothing points you there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/system.slice/&amp;lt;service&amp;gt;/memory.events
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/system.slice/&amp;lt;service&amp;gt;/memory.swap.current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;memory.events&lt;/code&gt; has a &lt;code&gt;high&lt;/code&gt; counter — the number of times the process was throttled at the soft ceiling — and an &lt;code&gt;oom_kill&lt;/code&gt; counter. Mine read a throttle count in the millions against zero kills. That pair is the entire diagnosis in two numbers: nothing died, everything was strangled. And &lt;code&gt;memory.swap.current&lt;/code&gt; said that essentially all the swap on the machine belonged to this one service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When a process is slow for no reason, check whether something is holding it under water on purpose.&lt;/strong&gt; A limit you set yourself is the easiest cause to overlook, because you remember configuring it as a safety measure and safety measures aren't suspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement that caused it
&lt;/h2&gt;

&lt;p&gt;The ceiling wasn't arbitrary. I measured the process's working size and set the limit above it.&lt;/p&gt;

&lt;p&gt;I measured it at the wrong moment. I took the number shortly after the model loaded, which is the calmest instant in that process's life: weights in memory, nothing computed yet, none of the caches and scratch buffers that only exist once real work is running. The number I recorded was a startup value wearing the label "working set". The real figure under load was meaningfully higher, and my ceiling landed underneath it.&lt;/p&gt;

&lt;p&gt;So the process spent its life a few percent over a line it could never get back under, being throttled continuously, on every batch, forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A limit derived from a measurement inherits every flaw in how you measured.&lt;/strong&gt; If the measurement was taken when the system was idle, the limit is a limit on idleness. Sample under load, or don't sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slowness is not a local property
&lt;/h2&gt;

&lt;p&gt;Here's the part that turned a slow service into a dead website.&lt;/p&gt;

&lt;p&gt;The worker reads its queue from Postgres and then, in the same transaction, goes off to the model. That was already sloppy, and for a batch that takes minutes it was survivable sloppiness. With the batch now taking most of a day, the worker sat in &lt;code&gt;idle in transaction&lt;/code&gt; for most of a day, holding a perfectly ordinary read lock on one table.&lt;/p&gt;

&lt;p&gt;Then a deploy landed. Startup ran a migration, the migration wanted &lt;code&gt;ALTER TABLE&lt;/code&gt;, and the &lt;code&gt;ALTER&lt;/code&gt; queued behind the worker's read lock. Fine so far — a migration waiting is not an outage.&lt;/p&gt;

&lt;p&gt;Except for the Postgres behaviour that everyone learns exactly once: &lt;strong&gt;a &lt;em&gt;waiting&lt;/em&gt; &lt;code&gt;AccessExclusiveLock&lt;/code&gt; blocks everything that queues up behind it.&lt;/strong&gt; The migration wasn't just waiting, it was a wall. Every subsequent query against that table joined a line behind a lock that would not be granted until a language model finished a job it was never going to finish on time. The API never reached &lt;code&gt;listen&lt;/code&gt;. Its last log line was &lt;code&gt;Waiting for application startup&lt;/code&gt;, and &lt;code&gt;systemctl&lt;/code&gt; reported it &lt;code&gt;active (running)&lt;/code&gt;, which was true and worthless. nginx returned 502 for everything except static images.&lt;/p&gt;

&lt;p&gt;The whole cascade fits in one query, which is the one thing I'd want anyone to take from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;active | Lock | ALTER TABLE …&lt;/code&gt; sitting next to an &lt;code&gt;idle in transaction | SELECT …&lt;/code&gt; is not a clue. It's the answer.&lt;/p&gt;

&lt;p&gt;Both sides are fixed now, because both sides were wrong: DDL runs with a &lt;code&gt;lock_timeout&lt;/code&gt; and logs who was holding, and the worker closes its transaction before it goes anywhere near the model. &lt;strong&gt;Never hold a database transaction across a call to something whose duration you don't control&lt;/strong&gt;, and treat a lock without a timeout as a promise to hang the table rather than a promise to wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alarm that had been ringing so long it was quiet
&lt;/h2&gt;

&lt;p&gt;I found all of this by accident, which was its own lesson.&lt;/p&gt;

&lt;p&gt;There was an hourly health check, and it had been finding problems the entire time. It said nothing, because alert state was stored as a single boolean for the whole system, and the rule was to notify on change. Chronically elevated swap had set that flag days earlier and it never came back down — so when new failures appeared underneath it, first the worker falling behind, then the API not answering, the state didn't &lt;em&gt;change&lt;/em&gt;, and not one message was sent. Meanwhile the morning summary printed the numbers with no verdict attached and signed off with "all services running."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An alarm that stays silent because the system was already unhealthy is worse than no alarm&lt;/strong&gt;, because it manufactures the feeling of being watched. State has to be tracked per check, not per system; unresolved problems have to keep repeating rather than being deduplicated into silence; and no report gets to print an all-clear until every check has actually passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from it
&lt;/h2&gt;

&lt;p&gt;I set the soft limit because a hard one felt violent — killing a process over a few megabytes seemed like an overreaction, and back pressure sounded like the gentler, more grown-up choice.&lt;/p&gt;

&lt;p&gt;It's the opposite. The hard limit fails: the process dies, systemd brings it back, one batch is lost, and the log says exactly what happened. The soft limit degrades: nothing fails, nothing is logged, no counter you routinely look at moves, and the system quietly stops doing its job while continuing to report that it is doing its job. One of those is an event you can build on. The other is a fact your monitoring is structurally unable to notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer the failure you can see to the degradation you can't.&lt;/strong&gt; It's the same rule I keep arriving at from other directions — &lt;a href="https://dev.to/den0011/validationerror-is-not-a-policy-3jpp"&gt;an absent value beats a wrong one&lt;/a&gt; — because the common thread isn't memory or validation. It's that a system which fails loudly is one you can operate, and a system which merely gets worse is one you find out about from your users.&lt;/p&gt;

&lt;p&gt;The limit is gone now. In its place: a hard cap, and swap turned off entirely for that service. If it grows past what the machine can give it, it dies, and I hear about it.&lt;/p&gt;

&lt;p&gt;Both of those are better than what I had, which was a process that was alive the entire time.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>llm</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
