<?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: 강해수</title>
    <description>The latest articles on DEV Community by 강해수 (@riversea).</description>
    <link>https://dev.to/riversea</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%2F3982443%2F067a59e7-d157-4b5a-896c-bb48e103f563.png</url>
      <title>DEV Community: 강해수</title>
      <link>https://dev.to/riversea</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/riversea"/>
    <language>en</language>
    <item>
      <title>1024-token RAG chunks cut my storage cost in half — and nearly doubled my Claude bill</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:29:34 +0000</pubDate>
      <link>https://dev.to/riversea/1024-token-rag-chunks-cut-my-storage-cost-in-half-and-nearly-doubled-my-claude-bill-11e</link>
      <guid>https://dev.to/riversea/1024-token-rag-chunks-cut-my-storage-cost-in-half-and-nearly-doubled-my-claude-bill-11e</guid>
      <description>&lt;p&gt;Switching from 512 to 1024-token chunks saved $1.20/month on Vectorize. It cost me $92 more on Claude Sonnet. I didn't see that coming until I did the math.&lt;/p&gt;

&lt;p&gt;I run an ad analytics SaaS with a daily agent flow that hits a RAG step on every cycle — about 400 runs a day. I'd left the chunk size at the default 512 tokens for three months before I got curious enough to actually measure it. So I indexed the same 100 ad reports three ways (256, 512, 1024 tokens), ran 20 fixed queries five times each, and tracked latency, citation accuracy, and estimated monthly cost.&lt;/p&gt;

&lt;p&gt;The summary table looked like 512 wins cleanly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chunk size&lt;/th&gt;
&lt;th&gt;Avg latency&lt;/th&gt;
&lt;th&gt;Citation accuracy&lt;/th&gt;
&lt;th&gt;Monthly vector cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;256 tokens&lt;/td&gt;
&lt;td&gt;43ms&lt;/td&gt;
&lt;td&gt;16.2 / 20&lt;/td&gt;
&lt;td&gt;~$4.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 tokens&lt;/td&gt;
&lt;td&gt;51ms&lt;/td&gt;
&lt;td&gt;17.8 / 20&lt;/td&gt;
&lt;td&gt;~$2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1024 tokens&lt;/td&gt;
&lt;td&gt;67ms&lt;/td&gt;
&lt;td&gt;15.1 / 20&lt;/td&gt;
&lt;td&gt;~$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But the vector storage number is a trap. With 1024-token chunks, each &lt;code&gt;top_k: 5&lt;/code&gt; retrieval pulls ~5,120 tokens into Claude's context instead of ~2,560. At $3/M input tokens, that's roughly 2,560 extra tokens × 12,000 monthly calls = 30.7M tokens = &lt;strong&gt;$92/month&lt;/strong&gt;. The $1.20 Vectorize saving doesn't touch it.&lt;/p&gt;

&lt;p&gt;1024-token chunks also produced what I'd call &lt;em&gt;dilution&lt;/em&gt; rather than hallucination — Claude wasn't making things up, it was including too much surrounding text and missing the actual point. One campaign's 60-day performance data would land as a single dense chunk, and the model would surface averages instead of the anomaly I was asking about. Smaller chunks hurt in the opposite direction: a campaign summary split across three 256-token pieces meant &lt;code&gt;top_k: 3&lt;/code&gt; often came back with an incomplete picture.&lt;/p&gt;

&lt;p&gt;What I run now is two indexes on the same data — a &lt;code&gt;report-512&lt;/code&gt; namespace for accuracy-first summary queries and a &lt;code&gt;live-256&lt;/code&gt; namespace for latency-sensitive ad-hoc questions:&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;summaryChunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vectorize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report-512&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liveChunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vectorize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;live-256&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Double the storage cost, but the Claude savings on the live path more than cover it at my call volume. Whether that math holds at lower volumes — it probably doesn't.&lt;/p&gt;

&lt;p&gt;I also hit a production incident during this experiment that had nothing to do with chunk size: swapping embedding models mid-index caused a dimension mismatch that killed the entire RAG step at 9am. That failure and the chunk overlap experiments I haven't run yet are in the full writeup.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the dimension mismatch incident and what I'd test next with dynamic chunking by document type — over on riversealab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riversealab.com/en/posts/rag-chunk-size-latency-quality-tradeoff/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>My Durable Object processed 4 req/s instead of 40 — the culprit wasn't storage</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:26:07 +0000</pubDate>
      <link>https://dev.to/riversea/my-durable-object-processed-4-reqs-instead-of-40-the-culprit-wasnt-storage-1h12</link>
      <guid>https://dev.to/riversea/my-durable-object-processed-4-reqs-instead-of-40-the-culprit-wasnt-storage-1h12</guid>
      <description>&lt;p&gt;A 200ms outbound webhook call was serializing every single request through my Durable Object, and I spent the first hour blaming the wrong thing.&lt;/p&gt;

&lt;p&gt;Durable Objects enforce a strict execution model: one &lt;code&gt;fetch&lt;/code&gt; handler runs at a time. If a second request arrives while the first is still awaiting &lt;em&gt;anything&lt;/em&gt; — storage, a network call, a sleep — it queues behind it. That's the consistency guarantee, and it's intentional. What I missed is that the queue doesn't care &lt;em&gt;what&lt;/em&gt; you're awaiting. A &lt;code&gt;storage.put()&lt;/code&gt; that takes 5ms and an outbound &lt;code&gt;fetch()&lt;/code&gt; that takes 300ms both hold the same lock. At 10 concurrent callers, you're not running 10 operations in parallel — you're running them in a single-file line, each one waiting for the full execution time of the one ahead of it.&lt;/p&gt;

&lt;p&gt;My DO was flushing a write buffer: save to storage, then POST to a webhook. Under load during a campaign spike (12K writes/minute), &lt;code&gt;wrangler tail&lt;/code&gt; started showing queue depth errors. I assumed KV back-pressure — I've hit the ~1,000 writes/second namespace cap before — so I rewrote the buffer to batch puts. Throughput improved maybe 15%. Still serialized. A quick timer log inside the handler told the real story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lastSeen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`storage.put: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 3–8ms&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://hooks.example.com/webhook&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&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="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`outbound fetch: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 180–420ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was architectural, not a micro-optimization. The DO should own state, not side effects. I moved the webhook call to a Queue binding — &lt;code&gt;env.WEBHOOK_QUEUE.send(body)&lt;/code&gt; runs in under 5ms and doesn't block on consumer acknowledgment. The DO drops the payload and moves on immediately. Lock held for single-digit milliseconds instead of 400.&lt;/p&gt;

&lt;p&gt;The second part of the fix — parallelizing read-only storage calls with &lt;code&gt;Promise.all()&lt;/code&gt; instead of sequential &lt;code&gt;await&lt;/code&gt; chains — shaved another chunk off p95 latency and is worth knowing about even if you never touch a webhook.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the &lt;code&gt;Promise.all()&lt;/code&gt; read pattern, what the input gate actually controls in the DO event loop, and how to test serialization behavior locally with &lt;code&gt;wrangler dev&lt;/code&gt; — over on dailymanuallab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailymanuallab.com/posts/durable-objects-concurrent-fetch-serialization/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Adding one field to Notion cost me 2.5 hours. The same change in Tana took 30 seconds.</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:24:04 +0000</pubDate>
      <link>https://dev.to/riversea/adding-one-field-to-notion-cost-me-25-hours-the-same-change-in-tana-took-30-seconds-40nm</link>
      <guid>https://dev.to/riversea/adding-one-field-to-notion-cost-me-25-hours-the-same-change-in-tana-took-30-seconds-40nm</guid>
      <description>&lt;p&gt;Adding a single property to a live Notion database with 160 rows isn't a five-minute job — it's a backfill session. I learned this the hard way in week seven of running Notion as production infrastructure for a content pipeline shipping 40 pieces a month.&lt;/p&gt;

&lt;p&gt;I added a "Distribution Channel" property mid-project because a client requirement shifted (they always do). Notion has no default inheritance for existing records. Every row showed blank in the rollup that referenced the new field until I manually touched it. Two and a half hours of cleanup for one schema change. And that cost resets every time the schema evolves — which, if your clients are real humans with drifting requirements, is constantly.&lt;/p&gt;

&lt;p&gt;The same change in Tana took about 30 seconds. I added a "Budget Flag" field to my Campaign supertag in month three. Every existing Campaign node inherited it immediately with a null value. No backfill required. That's not a minor UX difference — it's a fundamentally different data model. Tana's supertags propagate field definitions forward and backward across all tagged nodes. Notion's database columns are static per-row until you intervene.&lt;/p&gt;

&lt;p&gt;Here's the trade-off that actually matters after four months of tracking 41 friction events across both tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Notion&lt;/th&gt;
&lt;th&gt;Tana&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema stable, team needs access&lt;/td&gt;
&lt;td&gt;Holds up&lt;/td&gt;
&lt;td&gt;Breaks down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema evolves frequently&lt;/td&gt;
&lt;td&gt;Painful&lt;/td&gt;
&lt;td&gt;Fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tana's collaboration model was the wall I hit hard. Two contractors needed read access to brief statuses. Tana's sharing wasn't built for that workflow — at least not during the period I was running this. I ended up exporting pipeline status to a shared Notion page daily. Two tools doing one job, which is its own kind of friction.&lt;/p&gt;

&lt;p&gt;The honest framing isn't which tool is better. It's which failure mode is cheaper for your specific situation. Schema instability has a price in Notion that nobody in the "just plan ahead" crowd accounts for honestly.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including how the Zapier-to-Notion CRM sync held up at 300 entries a month, and the exact point where Tana's live search replaced three separate Notion databases — over on dailyfocusmag.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailyfocusmag.com/posts/tana-vs-notion-databases/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>workflow</category>
      <category>knowledgework</category>
    </item>
    <item>
      <title>My best-looking ROAS campaigns were quietly destroying subscription revenue</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:22:28 +0000</pubDate>
      <link>https://dev.to/riversea/my-best-looking-roas-campaigns-were-quietly-destroying-subscription-revenue-4agh</link>
      <guid>https://dev.to/riversea/my-best-looking-roas-campaigns-were-quietly-destroying-subscription-revenue-4agh</guid>
      <description>&lt;p&gt;Campaigns with the cleanest ROAS dashboards had collapsed subscription attach rates — from 40% down to under 12% — and nobody noticed for weeks.&lt;/p&gt;

&lt;p&gt;Here's what happened: subscription checkouts and one-off purchases were firing into the same &lt;code&gt;Purchase&lt;/code&gt; event, feeding a single tROAS target. The algorithm did exactly what it was told. It found conversions at the target ROAS, and the cheaper, more abundant one was the one-off buyer. Subscription LTV over 12 months in these accounts ran 3.5x–6x the one-off AOV for the same SKU. A ₩29,000/month skincare subscription is worth ₩348,000 in year one. The ₩38,000 one-off buyer is done. Blended into one ROAS target, the platform has no mechanism to weight them differently — so it doesn't.&lt;/p&gt;

&lt;p&gt;The fix is structural, not a settings tweak. Two separate conversion actions, two separate campaign containers. On Meta, splitting at the ad set level inside one campaign doesn't hold — the delivery system still blends optimization signal. The separation only works cleanly at the campaign level. On Google PMax, two separate campaigns with separate conversion goals assigned. I also tested value rules as a shortcut for six weeks: they adjust reported value but the underlying audience signal the model uses for prospecting still treats both buyer types as the same conversion. Subscription rate didn't move.&lt;/p&gt;

&lt;p&gt;The part that actually makes bid separation work — and where most implementations stop short — is what value you pass as the conversion signal. Checkout revenue is the wrong number for a subscription. I fire a separate &lt;code&gt;Subscribe_Complete&lt;/code&gt; event with a value parameter set to projected 6-month LTV, not the transaction amount, via Cloudflare Workers intercepting the post-purchase webhook within 2 seconds of checkout. That's what the tROAS target is actually bidding against.&lt;/p&gt;

&lt;p&gt;One caveat worth naming: this only makes sense when the LTV gap is real. If subscription LTV is only 1.5x one-off AOV, the volume fragmentation from separation probably costs more than the targeting precision gains. The threshold I use is 2.5x. Below that, consolidation may genuinely be the right call.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the exact 3-check decision flow I run on day 3 of every new campaign, and a simpler fallback for teams that can't build the webhook infra yet — over on themedilog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://themedilog.com/posts/subscription-vs-oneoff-bid-separation/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>d2c</category>
      <category>paidads</category>
      <category>analytics</category>
    </item>
    <item>
      <title>My agent dry-ran fine in staging 100 times — then wrecked production on the first real run</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 01:12:19 +0000</pubDate>
      <link>https://dev.to/riversea/my-agent-dry-ran-fine-in-staging-100-times-then-wrecked-production-on-the-first-real-run-10cc</link>
      <guid>https://dev.to/riversea/my-agent-dry-ran-fine-in-staging-100-times-then-wrecked-production-on-the-first-real-run-10cc</guid>
      <description>&lt;p&gt;A staging-to-production data bleed cost me 4 hours of rollback. That's what finally made dry-run a structural requirement, not an afterthought.&lt;/p&gt;

&lt;p&gt;The common advice is: test in staging, promote when green. The problem is environment drift. My D1 schema changes once or twice a week, and a solo operator can't keep staging perfectly synchronized. Worse, agents don't have fixed execution paths — the same input can produce a different tool call sequence on the next run. I ran a flow 100 times in staging and still hit a fresh path on the first production execution.&lt;/p&gt;

&lt;p&gt;The most surprising thing I learned after 6 months of running this: &lt;strong&gt;latency wasn't the problem I expected&lt;/strong&gt;. KV writes averaged 12ms — basically imperceptible. The real problem was that mock responses fool the agent into treating skipped writes as real successes. I'd dry-run an R2 &lt;code&gt;put&lt;/code&gt;, the agent would believe the file was uploaded, and then proceed to write metadata to D1 — which was &lt;em&gt;not&lt;/em&gt; in dry-run scope. Real write, orphaned record.&lt;/p&gt;

&lt;p&gt;The fix: once any write tool in a run hits dry-run, propagate a flag for that &lt;code&gt;runId&lt;/code&gt; that forces all subsequent writes in the same run to dry-run too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after intercepting first dry-run write&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`dryrun_active:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// every subsequent hook checks this flag&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDryRunActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`dryrun_active:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing that burned me: if the hook itself fails — say, KV goes temporarily unavailable — Claude Code's default behavior is fall-through. The tool call executes anyway, dry-run flag ignored. Last week a KV spike caused hook timeouts and 3 agents wrote directly to production. No data loss because those ops were idempotent, but it was luck. Hook failure needs its own alert, separate from agent failure.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the dry-run propagation edge cases, R2 + D1 orphan scenarios, and where this pattern completely falls apart (read-modify-write loops, APIs with side-effectful reads) — over on riversealab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riversealab.com/en/posts/agent-dry-run-mode-production-safety/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>`wrangler dev --remote` silently writes to your production KV namespace — here's the fix</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 01:08:50 +0000</pubDate>
      <link>https://dev.to/riversea/wrangler-dev-remote-silently-writes-to-your-production-kv-namespace-heres-the-fix-2c6p</link>
      <guid>https://dev.to/riversea/wrangler-dev-remote-silently-writes-to-your-production-kv-namespace-heres-the-fix-2c6p</guid>
      <description>&lt;p&gt;I lost production data on a Tuesday afternoon because &lt;code&gt;wrangler.toml&lt;/code&gt; had one missing field. Not a code bug. Not a logic error. A missing &lt;code&gt;preview_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;wrangler dev&lt;/code&gt; uses a local SQLite simulation — safe, isolated, zero real traffic. The moment you add &lt;code&gt;--remote&lt;/code&gt;, every KV read and write goes to the actual Cloudflare namespace over the API. If your &lt;code&gt;wrangler.toml&lt;/code&gt; only has the &lt;code&gt;id&lt;/code&gt; field pointing at your production namespace, those writes land in prod. No warning. No confirmation prompt. Just silent data mutation on the namespace your live users depend on.&lt;/p&gt;

&lt;p&gt;The fix is a single extra field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[kv_namespaces]]&lt;/span&gt;
&lt;span class="py"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MY_STORE"&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PROD_NAMESPACE_ID_HERE"&lt;/span&gt;
&lt;span class="py"&gt;preview_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DEV_NAMESPACE_ID_HERE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrangler automatically routes &lt;code&gt;--remote&lt;/code&gt; traffic through &lt;code&gt;preview_id&lt;/code&gt; instead of &lt;code&gt;id&lt;/code&gt;. Create a separate dev namespace with &lt;code&gt;wrangler kv namespace create "MY_STORE_dev"&lt;/code&gt;, drop its ID into &lt;code&gt;preview_id&lt;/code&gt;, and your production namespace is untouched. This should probably be in the quickstart docs. It isn't, at least not prominently.&lt;/p&gt;

&lt;p&gt;The second thing worth knowing: &lt;code&gt;--remote&lt;/code&gt; exposes a behavioral gap that local simulation hides entirely. Local KV is synchronous and in-process — a &lt;code&gt;put()&lt;/code&gt; followed by a &lt;code&gt;get()&lt;/code&gt; on the same key always returns the fresh value. Remote KV is eventually consistent. I had a rate-limiting worker that looked completely broken under &lt;code&gt;--remote&lt;/code&gt;: I'd write a counter, immediately read it back, and get the old value. The worker was correct. The local simulation had been lying to me about how production actually behaves. Switching to &lt;code&gt;--remote&lt;/code&gt; (against a dev namespace, not prod) surfaced the real race condition. That's uncomfortable, but it's accurate.&lt;/p&gt;

&lt;p&gt;There's also a write-rate ceiling worth knowing before you run any kind of seed script: hit roughly 1,000 writes/minute and you'll start seeing &lt;code&gt;429 Too Many Requests&lt;/code&gt; with error code &lt;code&gt;10013&lt;/code&gt;. A 70ms sleep between writes keeps you under the limit without dramatically slowing a seed operation down.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the &lt;code&gt;wrangler tail&lt;/code&gt; JSON truncation trap that cost me two hours, a shell script for seeding a dev namespace with representative data, and the exact &lt;code&gt;cacheTtl: 0&lt;/code&gt; pattern for honest read behavior — over on dailymanuallab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailymanuallab.com/posts/wrangler-local-dev-remote-kv-binding/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I audited 340 reading captures. Only 20% ever became knowledge I actually used.</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Wed, 01 Jul 2026 01:07:02 +0000</pubDate>
      <link>https://dev.to/riversea/i-audited-340-reading-captures-only-20-ever-became-knowledge-i-actually-used-3imp</link>
      <guid>https://dev.to/riversea/i-audited-340-reading-captures-only-20-ever-became-knowledge-i-actually-used-3imp</guid>
      <description>&lt;p&gt;Out of 340 captures over 90 days — Readwise highlights, Obsidian quick-captures, browser bookmarks — exactly 68 ever became a note I actually used. That's a 20% completion rate. The other 272 had a timestamp and nothing else.&lt;/p&gt;

&lt;p&gt;The uncomfortable part wasn't the number itself. It was what the data said about &lt;em&gt;where&lt;/em&gt; things died. I assumed the bottleneck was my weekly review — not thorough enough, not consistent enough. Wrong. I tagged every capture for three weeks as either "capture-only" or "annotated-at-source." Capture-only items had an 8% chance of becoming a usable note. Items where I spent 90 seconds writing a single sentence — in my own words, not a copied highlight — completed at around 55%. The weekly review wasn't failing because I was bad at reviews. It was failing because context decays faster than a week.&lt;/p&gt;

&lt;p&gt;By Sunday, I genuinely couldn't remember why I'd saved half the items. A highlighted paragraph looked important. The argument I was building when I saved it was gone. So I'd either re-read the source (expensive) or archive without processing (wasteful). The knowledge was perishable in a way that tasks simply aren't. What fixed it wasn't a better review template — it was a same-session annotation rule. I won't close a tab after reading something capture-worthy unless I've written one sentence into my Obsidian daily note first. The sentence doesn't have to be good. It has to be mine. Sixty to ninety seconds. That single constraint moved my annotation rate more than four months of Sunday review slots ever did.&lt;/p&gt;

&lt;p&gt;The other piece that made this stick was a &lt;code&gt;decay-date&lt;/code&gt; field in Obsidian Dataview — something I hadn't seen anyone write about before I built it. Every live annotation gets a date set 14 days out. A query surfaces anything expiring within 3 days. If it hasn't been promoted to a permanent note by then, a Templater script archives it automatically. Not deleted. But gone from the active workspace. The deadline is visible. The loss is real but low-stakes. It created a forcing function the inbox folder never could.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the harder completion metric I'm now using (a capture only "completes" when it gets cited in something I shipped, not when it becomes a note) and the exact Dataview query setup — over on dailyfocusmag.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailyfocusmag.com/posts/reading-to-notes-completion-rate/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>workflow</category>
      <category>knowledgework</category>
    </item>
    <item>
      <title>Adding more Claude subagents made my pipeline slower — here's the specific reason why</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:37:12 +0000</pubDate>
      <link>https://dev.to/riversea/adding-more-claude-subagents-made-my-pipeline-slower-heres-the-specific-reason-why-3n4l</link>
      <guid>https://dev.to/riversea/adding-more-claude-subagents-made-my-pipeline-slower-heres-the-specific-reason-why-3n4l</guid>
      <description>&lt;p&gt;Scaling from 4 to 8 Claude Code subagents pushed my error rate from 0.8% to 4.3%. The bottleneck wasn't the model.&lt;/p&gt;

&lt;p&gt;The culprit was a stateful MCP tool called &lt;code&gt;analytics_query&lt;/code&gt; that held pagination cursors, mid-aggregation values, and filter chains in instance memory between calls. Cloudflare Workers routes each request to whichever PoP instance is handy — no guarantees you land on the same one twice. At 4 subagents, collisions were rare enough that sessions accidentally stayed sticky. At 8, the distribution spread out and context misses went nonlinear. The error looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tool call failed — session context not found&lt;/span&gt;
  &lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sess_7f3a9b"&lt;/span&gt;
  &lt;span class="na"&gt;worker_instance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker-11"&lt;/span&gt;
  &lt;span class="na"&gt;expected_instance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worker-04"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session ID existed. The worker didn't match. State was gone.&lt;/p&gt;

&lt;p&gt;I ran two fixes side by side. KV-based session storage (serialize the whole context, read at call start, write at call end) solved the routing problem but created a new one: at 8 concurrent subagents, KV writes multiplied to ~16x my estimate. Under load, p99 latency jumped from 180ms to 620ms per tool call, and the write cost alone crossed $150/month at my volume.&lt;/p&gt;

&lt;p&gt;Durable Objects solved it cleanly. Route by session ID and you always hit the same DO instance — session affinity handled at the platform level, not in my code. Same load, p99 dropped to 38ms. Monthly cost settled around $40–60.&lt;/p&gt;

&lt;p&gt;The tradeoff nobody mentions upfront: DO instances get evicted on idle, and when that happens the in-memory state silently vanishes. The agent has no idea and keeps going. That failure mode is quieter and scarier than KV latency spikes, which at least show up in dashboards immediately.&lt;/p&gt;

&lt;p&gt;What I landed on after six months: DO memory for active sessions, DO Storage checkpoints at the end of each tool call (~$10/month extra), and KV only as a routing index — read-heavy, nearly free. Three layers, but each one has a distinct failure mode you can actually isolate.&lt;/p&gt;

&lt;p&gt;The 6-subagent mark was my inflection point. Below it, you might not see this problem at all. Above it, the session collision math gets ugly fast.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the checkpoint timing problem (DO idle eviction is less predictable than the docs suggest) and what happens when multiple subagents hit the same session simultaneously — over on riversealab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riversealab.com/en/posts/agent-session-affinity-stateful-tools/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>56% of my notes died before I ever had a chance to retrieve them</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:32:02 +0000</pubDate>
      <link>https://dev.to/riversea/56-of-my-notes-died-before-i-ever-had-a-chance-to-retrieve-them-1i17</link>
      <guid>https://dev.to/riversea/56-of-my-notes-died-before-i-ever-had-a-chance-to-retrieve-them-1i17</guid>
      <description>&lt;p&gt;41 out of 73 captures never left my inbox. Not because my retrieval system failed — because they never got processed in the first place.&lt;/p&gt;

&lt;p&gt;I spent three weeks logging every note I captured across 21 days and tracing exactly where each one stopped moving. Every PKM framework I'd read pointed at the same culprit: retrieval. Bad tagging, weak search, no backlinks. So I'd built for that — custom Obsidian templates, a graph view, a daily note that auto-pulls open tasks. None of it mattered, because the bottleneck was upstream. The notes were dying at capture, not at search.&lt;/p&gt;

&lt;p&gt;The finding that actually changed my setup: notes I captured with a single sentence of intent — &lt;em&gt;why this matters, what problem it connects to&lt;/em&gt; — had roughly an 80% retrieval rate when I went back for them two weeks later. Notes without that sentence: around 20%. That's not a tagging problem or a folder problem. It's a 45-second problem at the moment of capture. I'd been optimizing the wrong end of the pipeline entirely.&lt;/p&gt;

&lt;p&gt;The fix wasn't a better app. I swapped a frictionless Raycast snippet (fast capture, zero context) for a two-field Notion form with a mandatory 15-word minimum on the second field. Slower by 30 seconds. The survival rate difference was not subtle.&lt;/p&gt;

&lt;p&gt;The other thing I hadn't measured: I tracked how often I actually ran the "daily review" my system was designed around. Nine out of 21 days. On the other twelve, the inbox just grew — and once it crossed roughly 25 items, I'd start skimming by title instead of reading. The most fragmentary captures (usually the most valuable) had the worst titles and kept getting skipped.&lt;/p&gt;

&lt;p&gt;There's a third failure point I found that took me longest to see — notes that &lt;em&gt;did&lt;/em&gt; clear capture and processing, then landed in folders that were functionally prettier inboxes. I had 340 of them with a last-reviewed date older than 30 days and zero outbound links.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the four-label routing system I replaced my folder hierarchy with, and what "routing to a workflow" actually means in practice — over on dailyfocusmag.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailyfocusmag.com/posts/knowledge-capture-friction-audit/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>workflow</category>
      <category>knowledgework</category>
    </item>
    <item>
      <title>My Anthropic bill dropped from $312 to $156 after I added two bash hooks to Claude Code</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Mon, 29 Jun 2026 01:11:57 +0000</pubDate>
      <link>https://dev.to/riversea/my-anthropic-bill-dropped-from-312-to-156-after-i-added-two-bash-hooks-to-claude-code-4eei</link>
      <guid>https://dev.to/riversea/my-anthropic-bill-dropped-from-312-to-156-after-i-added-two-bash-hooks-to-claude-code-4eei</guid>
      <description>&lt;p&gt;60% of a $312 Anthropic bill came from a single pattern: Claude Code hitting a D1 migration failure, then spinning up 7–8 retry Bash calls trying to diagnose what went wrong. Each loop burned 40–60K tokens. Three or four loops per session, and you're looking at $0.50–$0.70 just evaporating.&lt;/p&gt;

&lt;p&gt;The fix wasn't prompt engineering. It was a &lt;code&gt;PostToolUse&lt;/code&gt; hook that fires the moment &lt;code&gt;wrangler d1 migrations apply&lt;/code&gt; exits non-zero — before the agent has a chance to start its retry spiral.&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# post_bash_hook.sh&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;EXIT_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"wrangler d1 migrations apply"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ALERT: D1 migration failed (exit &lt;/span&gt;&lt;span class="nv"&gt;$EXIT_CODE&lt;/span&gt;&lt;span class="s2"&gt;). Check schema state."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/accounts/&lt;/span&gt;&lt;span class="nv"&gt;$CF_ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;/storage/kv/namespaces/&lt;/span&gt;&lt;span class="nv"&gt;$KV_NS&lt;/span&gt;&lt;span class="s2"&gt;/values/migration_failed"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$CF_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
  &lt;span class="k"&gt;fi
fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Slack bot polls that KV key every 3 minutes. When it flips to &lt;code&gt;1&lt;/code&gt;, I get pinged and can intervene before Claude Code decides to investigate further on my token budget. Six months running this setup: zero schema-mismatch incidents, and the next month's bill came in at $156.&lt;/p&gt;

&lt;p&gt;The other half of the chain is a &lt;code&gt;PreToolUse&lt;/code&gt; hook that blocks &lt;code&gt;wrangler deploy&lt;/code&gt; whenever the agent is on &lt;code&gt;main&lt;/code&gt; — learned that one the hard way after a production deploy went out from the wrong branch and left two Workers in a broken state for five minutes. The thing most people miss: when your hook returns &lt;code&gt;exit 2&lt;/code&gt;, Claude Code reads whatever you wrote to stderr as context. A vague &lt;code&gt;BLOCK&lt;/code&gt; does nothing useful. &lt;code&gt;BLOCK: wrangler deploy on main — use staging namespace instead&lt;/code&gt; actually redirects the agent correctly.&lt;/p&gt;

&lt;p&gt;There's also a &lt;code&gt;pre-commit&lt;/code&gt; hook at the end of the chain that scans staged diffs for hardcoded production binding names and secret key patterns — a last filter before anything reaches git history.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the exact &lt;code&gt;.claude/settings.json&lt;/code&gt; structure, how hook matcher patterns work (and where they don't), and the FAQ on execution order guarantees — over on riversealab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riversealab.com/en/posts/hook-chain-design-postbash-precommit/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>800 simultaneous Workers, one cache miss, $40/mo surprise — the Cloudflare coalescing fix</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Mon, 29 Jun 2026 01:08:26 +0000</pubDate>
      <link>https://dev.to/riversea/800-simultaneous-workers-one-cache-miss-40mo-surprise-the-cloudflare-coalescing-fix-1o2a</link>
      <guid>https://dev.to/riversea/800-simultaneous-workers-one-cache-miss-40mo-surprise-the-cloudflare-coalescing-fix-1o2a</guid>
      <description>&lt;p&gt;A Korean flash sale at 9PM cost me $40 in a single month — not from KV reads, but from 800 concurrent Workers all racing to fetch the same product JSON from my origin in a 120ms window.&lt;/p&gt;

&lt;p&gt;The naive KV pattern looks fine on paper: check KV, miss, fetch origin, write KV, return. At low concurrency it works. Under burst traffic, though, KV write latency was running ~80ms and my origin fetch took ~120ms. That gap was wide enough for roughly 400 Workers to independently conclude the cache was cold and hammer the origin simultaneously. It started returning 429s by 9:03PM.&lt;/p&gt;

&lt;p&gt;The fix isn't faster KV writes — it's ensuring only &lt;em&gt;one&lt;/em&gt; Worker ever starts the fetch. Everything else waits on the same in-flight promise. That's request coalescing.&lt;/p&gt;

&lt;p&gt;The trick is that Workers are stateless across isolates — you can't share a &lt;code&gt;Promise&lt;/code&gt; between them directly. But a Durable Object runs in a single-threaded JS environment, which makes a &lt;code&gt;Map&lt;/code&gt; of in-flight promises inside a DO completely race-condition-free. The structure is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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;promise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchAndCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;finally&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promise&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; delete is non-negotiable. A &lt;code&gt;catch&lt;/code&gt;-only cleanup means a failed fetch permanently poisons that map entry — every future request for that key gets a rejected promise with no recovery. One other gotcha I hit early: I initially keyed all requests to a single DO instance with &lt;code&gt;idFromName("global-coalescer")&lt;/code&gt;. A Durable Object processes requests serially, so one slow origin fetch for product A would block product B entirely. The right move is &lt;code&gt;idFromName(key)&lt;/code&gt; — one DO instance per resource, not one global bottleneck.&lt;/p&gt;

&lt;p&gt;The Worker entry point stays simple: KV hit returns immediately (that hot path costs nothing up to 10M reads/day), and only on a miss does the request route to the Durable Object coalescer.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the &lt;code&gt;wrangler.toml&lt;/code&gt; config, the KV write timing problem inside a DO, and the exact production numbers before and after — over on dailymanuallab.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailymanuallab.com/posts/workers-request-coalescing-pattern/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>serverless</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>73% of my context switches came from one mistake: sharing a workspace across two businesses</title>
      <dc:creator>강해수</dc:creator>
      <pubDate>Mon, 29 Jun 2026 01:06:38 +0000</pubDate>
      <link>https://dev.to/riversea/73-of-my-context-switches-came-from-one-mistake-sharing-a-workspace-across-two-businesses-la8</link>
      <guid>https://dev.to/riversea/73-of-my-context-switches-came-from-one-mistake-sharing-a-workspace-across-two-businesses-la8</guid>
      <description>&lt;p&gt;73% of my context switches last year traced back to a single root cause — not bad habits, not weak discipline, but a Notion dashboard that mixed ad ops client work with content publishing in the same view.&lt;/p&gt;

&lt;p&gt;I run two genuinely different businesses. One is reactive: client Slack pings, campaign pacing, Meta and Google platform alerts. The other is slow and accumulative: SEO content, internal tooling, long-horizon experiments. For months I treated them as one organism inside one workspace. I even built a unified "Today" dashboard I was proud of — filtered views from both businesses, a single command center. What it actually did was guarantee the most urgent ad ops ticket was always sitting next to the most important content task. Urgency won every time. I tracked interruptions for six weeks to confirm I wasn't imagining it. I wasn't.&lt;/p&gt;

&lt;p&gt;The fix that actually moved the needle wasn't a new tool — it was hard namespace separation. Two top-level Notion page trees with zero cross-links between them. No shared databases, no cross-references. Ad ops contractors can't see the content root; content collaborators can't see ad ops. Sounds obvious. I ignored it for four months because the shared setup felt "efficient." What it really did was create accidental visibility — a contractor would spot something from the content side, ask about it, and that was 20 minutes gone before I'd even started the task I opened Notion for.&lt;/p&gt;

&lt;p&gt;The second shift was moving all drafting out of Notion entirely into Obsidian. Notion is good at structured status tracking. It is genuinely bad for thinking — the database UI creates a false sense of progress. A row exists, therefore work is happening. Moving first drafts to Obsidian and only syncing final status back to Notion cut my half-finished project count from 22 down to 8 in two months. That number surprised me more than anything else in 18 months of running this.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown — including the specific Notion properties I use in each business, how I handle the two categories that legitimately touch both sides (invoicing and contractor comms), and the CRM decision that goes against the standard "one master CRM" advice — over on dailyfocusmag.com.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dailyfocusmag.com/posts/multi-business-project-separation/" rel="noopener noreferrer"&gt;Full post →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>workflow</category>
      <category>knowledgework</category>
    </item>
  </channel>
</rss>
