<?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: RAXXO Studios</title>
    <description>The latest articles on DEV Community by RAXXO Studios (@raxxostudios).</description>
    <link>https://dev.to/raxxostudios</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%2F3848289%2Ffd2912c9-5820-4993-8fdc-62ec1e778980.png</url>
      <title>DEV Community: RAXXO Studios</title>
      <link>https://dev.to/raxxostudios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raxxostudios"/>
    <language>en</language>
    <item>
      <title>Scheduled Jobs and Time Zones: The Midnight Bug I Keep Seeing</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:49:32 +0000</pubDate>
      <link>https://dev.to/raxxostudios/scheduled-jobs-and-time-zones-the-midnight-bug-i-keep-seeing-1hgg</link>
      <guid>https://dev.to/raxxostudios/scheduled-jobs-and-time-zones-the-midnight-bug-i-keep-seeing-1hgg</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cron runs in UTC by default, so a "midnight" job can fire on the wrong local day&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define the business day explicitly with a timezone string, never a UTC offset&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DST shifts your offset twice a year, which silently moves job times by an hour&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test the four edge cases: midnight, month boundary, spring-forward, fall-back&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A report that was supposed to cover Monday landed with Sunday's data. The job ran at midnight, the clock just disagreed about which midnight. I've now seen this exact bug in four different codebases, and it's always the same root cause.&lt;/p&gt;

&lt;p&gt;Cron thinks in UTC. Your users think in their own kitchen clock. Those two things drift apart in ways that only show up at the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Midnight Is the Worst Possible Time to Run a Job
&lt;/h2&gt;

&lt;p&gt;Here's the setup that bites everyone. You want a daily summary of yesterday's orders. You schedule it for "midnight." On a server set to UTC, that's &lt;code&gt;0 0 * * *&lt;/code&gt;. It looks fine in review and ships.&lt;/p&gt;

&lt;p&gt;Then someone in Berlin opens the report.&lt;/p&gt;

&lt;p&gt;In summer, Berlin is UTC+2. Your midnight UTC job fires at 02:00 Berlin time. So far harmless. But the logic inside the job usually does something like "give me everything from yesterday," and "yesterday" gets computed from the server clock. If the server is UTC and the business lives in Berlin, the window is off by two hours on both ends. Orders placed between 22:00 and 00:00 Berlin time on Monday get filed under Tuesday.&lt;/p&gt;

&lt;p&gt;Now flip it. A customer in Los Angeles (UTC-8 in winter) places an order at 23:30 their time on the last day of the month. On the UTC server, that's already 07:30 the next day, next month. Your monthly rollup drops it into the wrong month entirely. Nobody notices until accounting asks why the totals don't match.&lt;/p&gt;

&lt;p&gt;The reason midnight is the villain is that it's the one time where a small offset flips the calendar date. A 15-minute delay at 14:00 changes nothing you care about. The same delay at 23:58 changes the day, and sometimes the month, and twice a year it changes by a whole hour on its own.&lt;/p&gt;

&lt;p&gt;I've started treating any job scheduled near 00:00 as a code smell.&lt;/p&gt;

&lt;p&gt;Not wrong, but worth a second look. Nine times out of ten the person wrote &lt;code&gt;0 0&lt;/code&gt; because it felt tidy, not because midnight meant anything to the business.&lt;/p&gt;

&lt;p&gt;If your scheduled job also talks to an external service that might retry it, the date confusion compounds. I wrote up the safe pattern for that in &lt;a href="https://dev.to/blogs/lab/writing-idempotent-webhooks-patterns-that-survive-retries"&gt;Writing Idempotent Webhooks&lt;/a&gt;, and the same discipline applies here: assume the job can run twice and land on two different calendar days if you're not careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the Business Day on Purpose, Not by Accident
&lt;/h2&gt;

&lt;p&gt;The fix starts with a decision, not with code. What timezone is the business day measured in? Write it down. Put it in config as a string.&lt;/p&gt;

&lt;p&gt;Use an IANA timezone name like &lt;code&gt;Europe/Berlin&lt;/code&gt; or &lt;code&gt;America/Los_Angeles&lt;/code&gt;. Don't use &lt;code&gt;UTC+2&lt;/code&gt;. A fixed offset is a lie for half the year, because the offset changes when daylight saving kicks in. The name encodes all the rules; the offset encodes a single frozen moment.&lt;/p&gt;

&lt;p&gt;Once you have that string, stop asking the server what time it is. Compute your reporting window in the business timezone, then convert to UTC only when you query the database.&lt;/p&gt;

&lt;p&gt;In practice that looks like this. You want "yesterday in Berlin." You take now, convert it to &lt;code&gt;Europe/Berlin&lt;/code&gt;, subtract a day, snap to the start of that day, and snap to the start of the next day. Those two instants are your window boundaries. Then you convert both to UTC for the actual query, because your timestamps are almost certainly stored in UTC. If they're not, that's a separate fire to put out first.&lt;/p&gt;

&lt;p&gt;The key move is that the window is defined in local time and the query runs in UTC. People keep collapsing those two steps into one and that's where the bug lives.&lt;/p&gt;

&lt;p&gt;I keep a tiny helper for this so I never hand-roll the math. Something that takes a timezone string and a date and returns &lt;code&gt;{ startUtc, endUtc }&lt;/code&gt;. One function, one test file, reused everywhere. The alternative is scattering &lt;code&gt;new Date()&lt;/code&gt; and manual hour arithmetic across six jobs, and each copy drifts slightly until they all disagree.&lt;/p&gt;

&lt;p&gt;Schedule the cron trigger in UTC still.&lt;/p&gt;

&lt;p&gt;That part's fine. The trigger time and the data window are different concerns. Run the trigger a safe distance from midnight (I like 03:00 or 04:00 local, converted to UTC), and let the window logic figure out which day it's reporting on. A late trigger then can't change the answer, because the answer doesn't depend on when the job actually fired. It depends on the explicit window you computed.&lt;/p&gt;

&lt;p&gt;This is the same instinct as validating inputs before you trust them. I go deeper on drawing those boundaries in &lt;a href="https://dev.to/blogs/lab/request-validation-at-the-edge-a-minimal-pattern-i-reuse"&gt;Request Validation at the Edge&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Daylight Saving Is the Bug That Hides for Six Months
&lt;/h2&gt;

&lt;p&gt;Your job runs fine for months. Then on a Sunday in late March or late October, something's an hour off. Welcome to DST.&lt;/p&gt;

&lt;p&gt;Two things happen. In spring, clocks jump forward, so 02:00 to 03:00 doesn't exist. In autumn, clocks fall back, so 02:00 to 03:00 happens twice. If you scheduled a local-time job inside that window, it either skips or doubles.&lt;/p&gt;

&lt;p&gt;That's exactly why you should schedule the cron trigger in UTC, which has no DST, and compute the window in local time. UTC marches forward one second at a time, forever, no jumps. Your trigger is stable. The local window shifts by an hour across the DST boundary, which is correct, because the business day genuinely is an hour shorter or longer on those two days.&lt;/p&gt;

&lt;p&gt;Here's the part people get wrong even after they've fixed the first bug. They store a UTC offset for the business, say &lt;code&gt;+01:00&lt;/code&gt;, and reuse it all year. From late March to late October in Berlin the real offset is &lt;code&gt;+02:00&lt;/code&gt;. So for about seven months, every window is an hour off. It looks fine in winter when they test it, and goes quietly wrong in summer.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;Europe/Berlin&lt;/code&gt; carries the rule that the offset is &lt;code&gt;+01:00&lt;/code&gt; in winter and &lt;code&gt;+02:00&lt;/code&gt; in summer, and it knows the exact instants where it flips. Any decent date library with timezone support will apply that for you. You just have to feed it the name, not the number.&lt;/p&gt;

&lt;p&gt;One more trap. The hour that occurs twice in autumn is ambiguous. "01:30 local" maps to two different UTC instants on that night. If your window boundary lands there, pick a convention (most libraries default to the earlier instant) and make sure your test asserts which one you got. Don't leave it to chance.&lt;/p&gt;

&lt;p&gt;I'll be honest, I don't trust myself to reason about these by hand.&lt;/p&gt;

&lt;p&gt;I write a test that pins a fake clock to a specific UTC instant on a DST night and checks the window boundaries come out right. The test is the only thing I trust when the next DST change rolls around and I've forgotten all of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Tests That Catch Everything
&lt;/h2&gt;

&lt;p&gt;You don't need a hundred cases. You need four, each pinned to a fixed instant so the test gives the same answer forever.&lt;/p&gt;

&lt;p&gt;Test one is plain midnight. Set the clock to 00:30 local time and check that "yesterday" is actually the previous calendar day. This catches the off-by-a-day error where a late-firing job or a careless window calculation rolls into the wrong date.&lt;/p&gt;

&lt;p&gt;Test two is the month boundary. Set the clock to 00:30 local on the first of the month and confirm "yesterday" lands in the previous month, and that a monthly rollup includes the right 28, 30, or 31 days. This is where the Los Angeles customer at 23:30 gets lost if your window is computed in UTC.&lt;/p&gt;

&lt;p&gt;Test three is spring-forward. Pin the clock to the DST gap night and check that the window still spans a full local day even though that day is 23 hours long in real time. The start and end boundaries should be correct instants, not naive hour math.&lt;/p&gt;

&lt;p&gt;Test four is fall-back. Pin to the night with the repeated hour and check your window handles the 25-hour day and picks a consistent instant for the ambiguous boundary.&lt;/p&gt;

&lt;p&gt;Run these with a frozen clock.&lt;/p&gt;

&lt;p&gt;Real &lt;code&gt;new Date()&lt;/code&gt; makes tests that pass today and fail next October. Inject the current time as a parameter, or use a library that lets you set a fake system time. Then assert on exact UTC instants, not on strings, because string formatting adds its own timezone confusion on top.&lt;/p&gt;

&lt;p&gt;I keep these four as a template and copy them into any project that has scheduled jobs. The edge cases don't change between projects. Only the timezone string and the window function do. Writing them once and reusing them is the whole point, the same way I reuse a structure for articles so I don't rebuild it each time, which I described in &lt;a href="https://dev.to/blogs/lab/the-blog-post-structure-i-use-so-readers-actually-finish"&gt;The Blog Post Structure I Use&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you also schedule social posts off the back of these jobs, the same timezone logic applies to when they publish. A tool like &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; handles the posting side, but the "which day's content" decision still has to be made correctly upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Pick the business timezone, store it as an IANA name, and compute your reporting window in that zone before converting to UTC for the query. Trigger the cron in UTC and keep it away from midnight so a late run can't change which day you report. Then write the four tests with a frozen clock and stop reasoning about DST in your head.&lt;/p&gt;

&lt;p&gt;If I were starting a new scheduled job today, I'd write the window function and its tests before the job logic. The logic is the easy part. The dates are where the money goes missing.&lt;/p&gt;

&lt;p&gt;For how I wire up the agent that scaffolds this kind of helper and its tests in one pass, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the setup I use. Start there, then drop in the four tests above, and the midnight bug stops being a thing you ship.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Profiling Liquid: How I Find What Slows a Shopify Page</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:48:15 +0000</pubDate>
      <link>https://dev.to/raxxostudios/profiling-liquid-how-i-find-what-slows-a-shopify-page-3l7o</link>
      <guid>https://dev.to/raxxostudios/profiling-liquid-how-i-find-what-slows-a-shopify-page-3l7o</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Theme Inspector shows per-section render time right in Chrome DevTools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server-Timing header reveals Liquid vs network in one number&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nested loops over full collections are the usual slow culprit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cached snippets with the render tag cut repeat work to near zero&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A product page that took 900ms to render Liquid dropped to under 200ms once I found the one section eating the whole budget. The fix wasn't clever. It was a loop inside a loop over a 400-product collection. Here's how I find that kind of thing instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the numbers the theme already gives you
&lt;/h2&gt;

&lt;p&gt;Before touching any code I want one honest number: how long does Liquid take to render this page on the server? &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; hands you that number in two places, and I check both.&lt;/p&gt;

&lt;p&gt;The first is the Shopify Theme Inspector for Chrome. Install the extension, log into your store, open DevTools, and you get a flame graph of every Liquid partial on the page with its render time. Sections, snippets, the lot. You can see that your header took 4ms and your "recommended products" section took 310ms, and suddenly you know where to look.&lt;/p&gt;

&lt;p&gt;The second is the &lt;code&gt;Server-Timing&lt;/code&gt; HTTP header. Open the Network tab, click the document request, and look at the Timing breakdown. Shopify reports the Liquid processing time there as a server timing entry. This is the number that matters because it's measured on their machine, not yours. Your laptop being slow doesn't move it.&lt;/p&gt;

&lt;p&gt;I treat these two as a pair. The header tells me the total damage. The inspector tells me which section did it.&lt;/p&gt;

&lt;p&gt;One thing I learned the hard way: measure a warm page, not a cold one. The first request after a theme edit or a deploy can be slower because Shopify hasn't cached the compiled template yet. Load the page twice and read the second number. If I skip this step I end up "fixing" a section that was never the problem.&lt;/p&gt;

&lt;p&gt;I also measure a real page, not a demo. A collection page with 8 products behaves nothing like the same template with 400. The slow loop only shows up under real data.&lt;/p&gt;

&lt;p&gt;If you want the deeper context on why I keep all of this in files I can profile, see &lt;a href="https://dev.to/blogs/lab/why-every-raxxo-section-lives-in-code-not-the-customizer"&gt;Why Every RAXXO Section Lives in Code, Not the Customizer&lt;/a&gt;. You can't profile what the customizer hides from you.&lt;/p&gt;

&lt;p&gt;So the routine is simple. Open the page, read Server-Timing for the total, open the inspector for the breakdown, and write down the three slowest sections. Those three are my entire to-do list. Everything else can stay exactly as it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested loops are almost always the real cost
&lt;/h2&gt;

&lt;p&gt;Once I know the slow section, the first thing I look for is a loop inside a loop. This is the single most common performance bug I find in themes, including ones I wrote.&lt;/p&gt;

&lt;p&gt;Here's the shape of the problem. You loop over products in a collection. Inside that loop, for each product, you loop over all its variants. Inside that, maybe you loop over variant metafields. Three products and four variants is nothing. Four hundred products with six variants each is 2,400 inner iterations, and Liquid runs every one of them on the server before a single byte reaches the browser.&lt;/p&gt;

&lt;p&gt;The fix is usually to stop doing work you don't need. If I'm showing a product card, I don't need every variant. I need the first available one and maybe the price range, and both of those are available without a loop. &lt;code&gt;product.price_min&lt;/code&gt; and &lt;code&gt;product.price_max&lt;/code&gt; give me the range directly. &lt;code&gt;product.selected_or_first_available_variant&lt;/code&gt; gives me the one I actually show.&lt;/p&gt;

&lt;p&gt;The other nested-loop trap is filtering inside the loop. I've seen code that loops over a full collection and then uses an &lt;code&gt;if&lt;/code&gt; to show only products with a certain tag. That means Liquid still iterates the whole collection even if only 12 products match. Better to narrow the data before the loop.&lt;/p&gt;

&lt;p&gt;Shopify's &lt;code&gt;where&lt;/code&gt; filter helps here. &lt;code&gt;collection.products | where: "type", "Poster"&lt;/code&gt; hands you a smaller array, and now your loop runs over 12 items instead of 400. The filter happens once. The loop runs fewer times. Both wins compound.&lt;/p&gt;

&lt;p&gt;Limits matter too. &lt;code&gt;for product in collection.products limit: 8&lt;/code&gt; stops the loop after 8 iterations no matter how big the collection is. On a homepage carousel that shows 8 cards, there's no reason to render 400.&lt;/p&gt;

&lt;p&gt;I covered the shortcuts I reach for first in &lt;a href="https://dev.to/blogs/lab/the-liquid-patterns-that-cut-my-theme-build-time-in-half"&gt;The Liquid Patterns That Cut My Theme Build Time in Half&lt;/a&gt;. A lot of those patterns are fast on purpose, not just short.&lt;/p&gt;

&lt;p&gt;The rule I follow: touch the smallest set of data that produces the right output. If the loop is reading more than the page displays, that's wasted server time, and the inspector will show it as a fat bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache the snippets that repeat the same work
&lt;/h2&gt;

&lt;p&gt;The second big win is caching, and Shopify gives you more here than people realize.&lt;/p&gt;

&lt;p&gt;When you use the &lt;code&gt;render&lt;/code&gt; tag instead of &lt;code&gt;include&lt;/code&gt;, the rendered snippet is isolated. It only sees the variables you pass it. That isolation is what makes caching safe, because the output depends only on the inputs, not on whatever global state happened to exist when it ran.&lt;/p&gt;

&lt;p&gt;Shopify caches rendered fragments internally when it can, and you help it by keeping snippets pure. A snippet that takes a product and returns a card will cache better than one that reaches out to &lt;code&gt;cart&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;, and three section settings at once. The more a snippet depends on, the less it can be reused.&lt;/p&gt;

&lt;p&gt;So when I have a product card that renders 40 times on a collection page, I make it a clean snippet with one input:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{% render 'product-card', product: product %}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's it. No globals, no surprise reads. Shopify can treat identical inputs as identical work.&lt;/p&gt;

&lt;p&gt;For data that genuinely doesn't change per visitor, I lean on metafields instead of computing things in Liquid. If I need a product's "care instructions" or a pre-formatted spec table, I store it as a metafield and read it directly. No loop, no string building, no conditional logic on every render. I wrote up the patterns I trust for this in &lt;a href="https://dev.to/blogs/lab/shopify-metafields-done-right-three-patterns-from-production"&gt;Shopify Metafields Done Right&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There's a quieter form of caching too: don't recompute inside the loop what you can compute once above it. If every iteration formats the same currency or looks up the same shop setting, hoist it out. Assign it to a variable before the loop and reference the variable inside. Forty lookups become one.&lt;/p&gt;

&lt;p&gt;I also watch out for section settings read inside loops. Each &lt;code&gt;section.settings.something&lt;/code&gt; is cheap on its own, but called 400 times it adds up. Read it once into a local variable at the top. Small change, measurable bar on the flame graph.&lt;/p&gt;

&lt;p&gt;When I need to pass context between the storefront and checkout without recomputing it, I use cart attributes instead of rebuilding state. Background: &lt;a href="https://dev.to/blogs/lab/shopify-cart-attributes-passing-context-through-checkout"&gt;Shopify Cart Attributes&lt;/a&gt; walks through the setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure again, then stop
&lt;/h2&gt;

&lt;p&gt;The part people skip is measuring after the fix. I've convinced myself a change helped and then the Server-Timing number didn't move at all. Guessing feels like progress. The header doesn't care about feelings.&lt;/p&gt;

&lt;p&gt;So after every change I reload twice and read the number again. If the slow section dropped from 310ms to 40ms, good, I move to the next bar. If it barely moved, I revert and look harder, because I misread the cause.&lt;/p&gt;

&lt;p&gt;I keep a tiny log while I work. Just a text note with the section name, the before time, and the after time. On a recent collection template the three slowest sections started at 310ms, 180ms, and 90ms. After the loop and caching work they came in at 40ms, 55ms, and 45ms. The total Liquid time went from roughly 900ms to under 200ms. That's the whole story in five numbers, and I only trust it because I measured both ends.&lt;/p&gt;

&lt;p&gt;Knowing when to stop matters as much as knowing where to start. Once a page renders Liquid in a couple hundred milliseconds, the next win usually isn't Liquid at all. It's image sizes, third-party scripts, or fonts. Those live in the browser, not on Shopify's server, and the Server-Timing Liquid number won't move them. Chasing Liquid further at that point is effort spent on the wrong layer.&lt;/p&gt;

&lt;p&gt;A quick checklist I run before calling a page done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Server-Timing Liquid under ~250ms on a warm load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No section in the inspector over ~60ms unless it has a very good reason.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No loop reading more data than the page shows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repeated snippets use &lt;code&gt;render&lt;/code&gt;, not &lt;code&gt;include&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all four pass, I close the tab. The page is fast enough, and the remaining milliseconds aren't worth the complexity of squeezing them out.&lt;/p&gt;

&lt;p&gt;One more habit: I profile on a template I've already shipped, not just new work. Stores drift. A collection that was fast with 30 products gets slow at 300 without anyone changing a line of code. Re-profiling the big templates a couple of times a year catches that drift before a customer feels it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If a Shopify page feels slow, don't rewrite the theme. Open the Theme Inspector, read the Server-Timing header, and find the one section that's eating the budget. Nine times out of ten it's a nested loop over a collection that's grown too big, and the fix is narrowing the data or caching a clean snippet with &lt;code&gt;render&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Start with the measurement. Everything else is guessing until you have the two numbers: total Liquid time and the per-section breakdown. Then fix the biggest bar, measure again, and stop when the page renders in a couple hundred milliseconds.&lt;/p&gt;

&lt;p&gt;If you're building a store on &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; and want the thinking behind how I keep themes in code where they can be profiled and versioned, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; lays out the full approach I use. Profile first, fix the expensive thing, leave the rest alone.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Writing Commit Messages That Double as Documentation</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 19 Oct 2026 21:40:45 +0000</pubDate>
      <link>https://dev.to/raxxostudios/writing-commit-messages-that-double-as-documentation-241k</link>
      <guid>https://dev.to/raxxostudios/writing-commit-messages-that-double-as-documentation-241k</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A three-part commit structure (what, why, impact) answers 90% of git blame questions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bad messages like "fix bug" cost me 20 minutes per investigation months later&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The body matters more than the subject line for future archaeology&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Templates and a commit hook make the good format the default&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months after I shipped a feature, I ran &lt;code&gt;git blame&lt;/code&gt; on a weird line of code and found a commit that said "fix stuff". That commit was mine. I had no memory of what stuff, or why. The fix was to write messages that answer the questions I'd actually ask later.&lt;/p&gt;

&lt;p&gt;A commit message is the only documentation that sits right next to the code it describes. Most people waste it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Questions Future You Actually Asks
&lt;/h2&gt;

&lt;p&gt;When I run &lt;code&gt;git blame&lt;/code&gt; on a confusing line, I'm never asking "what does this code do". I can read the code. I'm asking why it exists.&lt;/p&gt;

&lt;p&gt;The three questions are always the same: what changed, why it changed, and what it affected. If a commit message answers those three, I close the terminal and get back to work. If it doesn't, I go spelunking through pull requests and chat logs that may not exist anymore.&lt;/p&gt;

&lt;p&gt;Here's a real bad one from my own history: &lt;code&gt;update handler&lt;/code&gt;. That's the whole message. When I found it later, I had to read the diff to learn it added a timeout to an API call. The diff told me the what. It couldn't tell me the why. Was the timeout a guess? A response to a real hang? Did a specific endpoint need it? I had no idea, so I was scared to touch it.&lt;/p&gt;

&lt;p&gt;Compare that to a message that says the timeout was added because the upstream payment API would hang for 30 seconds under load, and a 5 second timeout with a retry was faster for the user. Now I know the number isn't random. I know what breaks if I remove it. I can change it with confidence.&lt;/p&gt;

&lt;p&gt;The "why" is the part that dies first.&lt;/p&gt;

&lt;p&gt;Code carries its own "what". A commit carries the "what" for a day, then the diff carries it forever. But the reasoning in your head evaporates fast. You'll forget the production incident, the slow endpoint, the edge case a customer hit. Write that down while it's fresh, because the code physically cannot store it.&lt;/p&gt;

&lt;p&gt;The impact line is the one people skip most and regret most. "This also changes the shape of the cache key, so old cached entries become stale on deploy." That sentence saves someone a confusing afternoon. I've written versions of it and been grateful to past me more times than I can count.&lt;/p&gt;

&lt;p&gt;So the method is simple: answer those three questions in order, every time. That's all it takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Structure I Actually Type
&lt;/h2&gt;

&lt;p&gt;A good commit has a subject line and a body, separated by a blank line. Git treats that first line specially, so it carries weight.&lt;/p&gt;

&lt;p&gt;The subject is 50 characters or fewer, written as a command: "Add timeout to payment handler", not "Added" or "Adding". Present tense, like you're telling the codebase what to do. It should finish the sentence "if applied, this commit will..."&lt;/p&gt;

&lt;p&gt;Then a blank line. Then the body, wrapped around 72 characters, where the real work happens.&lt;/p&gt;

&lt;p&gt;My body has three parts, and I don't label them. I just write paragraphs in this order:&lt;/p&gt;

&lt;p&gt;First, what changed in plain language. Not a restatement of the diff, a summary a human can read. "The payment handler now times out after 5 seconds and retries once."&lt;/p&gt;

&lt;p&gt;Second, why. This is the paragraph that justifies the change existing. "Under load the upstream API hangs for up to 30 seconds. Users saw a frozen checkout. A short timeout with one retry fails faster and recovers in most cases."&lt;/p&gt;

&lt;p&gt;Third, impact and anything surprising. "Side effect: the retry doubles the request count on genuine slow responses. If the upstream recovers, this is fine. If it's truly down, the second request also fails fast."&lt;/p&gt;

&lt;p&gt;That's it. Most of my commits are four to six lines of body. Some are one line because the change is trivial and self-explanatory ("Fix typo in error string"). The structure scales down.&lt;/p&gt;

&lt;p&gt;The retry example above isn't hypothetical in spirit. Getting retries right is its own topic, and I covered the pattern I reuse in &lt;a href="https://dev.to/blogs/lab/retry-with-backoff-and-jitter-the-version-i-copy-everywhere"&gt;Retry With Backoff and Jitter&lt;/a&gt;. A commit that adds a retry should point at the reasoning behind the numbers, even if only in one sentence.&lt;/p&gt;

&lt;p&gt;One thing I avoid: dumping ticket IDs as the entire message. "JIRA-4471" tells future me nothing without a login and a working link. Reference the ticket at the bottom if you must, but the message has to stand alone. Issue trackers get migrated, archived, and shut down. The git history outlives all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and After on Real Commits
&lt;/h2&gt;

&lt;p&gt;Let me show the difference with the kind of change I make all the time.&lt;/p&gt;

&lt;p&gt;Before: &lt;code&gt;migrate db&lt;/code&gt;. After: a subject of "Add nullable discount_code to orders" with a body explaining that the column is nullable so the migration runs without a default backfill on the existing 80,000 rows, and that application code treats null as "no code applied".&lt;/p&gt;

&lt;p&gt;The before version is a landmine. Six months later, someone wonders why the column is nullable instead of defaulting to an empty string. The after version answers it in advance. Schema decisions are exactly the kind of thing you forget and then second-guess. If you want the deeper context on making schema changes safe, see &lt;a href="https://dev.to/blogs/lab/database-migrations-i-can-roll-back-at-2am"&gt;Database Migrations I Can Roll Back at 2am&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another one. Before: &lt;code&gt;perf&lt;/code&gt;. After: "Switch catalog lookup to binary search" with a body noting the list is pre-sorted and has 50,000 rows, and that linear scan was showing up in profiling. Now anyone reading it knows the precondition (the list must stay sorted) and won't accidentally break it. I measured that exact tradeoff in &lt;a href="https://dev.to/blogs/lab/binary-search-vs-array-includes-on-a-50-000-row-catalog"&gt;Binary Search vs Array.includes()&lt;/a&gt;, and a one-line reference in the commit saves a re-benchmark.&lt;/p&gt;

&lt;p&gt;A third. Before: &lt;code&gt;fix logging&lt;/code&gt;. That's useless because logging changes are often subtle and load-bearing. After: "Add request ID to structured log context" with a body saying every log line in a request now carries the same ID so I can grep a full trace, and that this depends on the middleware running first.&lt;/p&gt;

&lt;p&gt;See the pattern? The before messages are all two words and all worthless. The after messages take maybe 40 extra seconds to write and answer the question before it's asked.&lt;/p&gt;

&lt;p&gt;I keep logs structured partly so these commit messages have something concrete to reference. The two habits feed each other, which I get into in &lt;a href="https://dev.to/blogs/lab/structured-logging-i-add-on-day-one-of-every-project"&gt;Structured Logging I Add on Day One&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The 40 seconds feels like a tax when you're typing it. It's not. It's a loan to future you at a great interest rate. One avoided 20 minute investigation pays for 30 careful commits. I've been on both sides of that trade enough to know which I prefer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Good Format the Default
&lt;/h2&gt;

&lt;p&gt;Discipline fails. I don't trust myself to write a good message every time on willpower, so I made the environment push me toward it.&lt;/p&gt;

&lt;p&gt;The first thing is a commit template. Git lets you set one with a config pointing at a text file. Mine has comment lines reminding me of the what, why, impact order. When I run &lt;code&gt;git commit&lt;/code&gt; without &lt;code&gt;-m&lt;/code&gt;, my editor opens with those prompts already there. I delete the comments as I fill them in. The friction is gone because the structure is sitting in front of me.&lt;/p&gt;

&lt;p&gt;The second thing is banning &lt;code&gt;-m&lt;/code&gt; for anything non-trivial. The &lt;code&gt;-m&lt;/code&gt; flag is how bad messages happen, because you're typing into a cramped shell with no room to think. Open the editor. Give yourself space. Typos in a one-line flag become "fix stuff" commits.&lt;/p&gt;

&lt;p&gt;The third thing is squashing before I merge. My work-in-progress commits say "wip" and "oops forgot import" and I don't care, because they never reach the main branch. I squash the whole branch into one clean commit with one good message that describes the finished change. The history people read should be the story of what shipped, not the mess of how I got there.&lt;/p&gt;

&lt;p&gt;I also use AI to draft the first pass now. I paste the diff and ask for a what/why/impact message, then I fix the "why" because the model can only guess at my reasoning. It gets the "what" right almost every time, which is the boring part anyway. If you want the setup I use for that kind of task, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through how I wire these helpers in.&lt;/p&gt;

&lt;p&gt;A warning on automation: don't let a tool write the "why" for you and ship it unread. The model infers intent from the diff, and sometimes it's wrong in a way that sounds confident. A wrong "why" in the permanent history is worse than a blank one. Read what it drafts.&lt;/p&gt;

&lt;p&gt;If you're on a solo project and think none of this matters because you're the only reader, you're the exact person it helps most. There's no teammate to ask. The git history is your only record. Past me is a stranger I can only talk to through commit messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Start with one change today: stop using &lt;code&gt;git commit -m&lt;/code&gt; for anything bigger than a typo. Open the editor, write three short paragraphs answering what, why, and impact, and see how it feels in a month when &lt;code&gt;git blame&lt;/code&gt; lands on it.&lt;/p&gt;

&lt;p&gt;If you want to go further, set a commit template so the three prompts greet you every time. That single config line did more for my history than any amount of trying to remember.&lt;/p&gt;

&lt;p&gt;The payoff isn't visible on the day you write the message. It shows up later, when a confusing line has a clear explanation attached and you close the terminal in ten seconds instead of digging for twenty minutes. Good commits are a message to a person who's forgotten everything. That person is you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Optimistic UI Updates That Never Lie to the User</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:33:12 +0000</pubDate>
      <link>https://dev.to/raxxostudios/optimistic-ui-updates-that-never-lie-to-the-user-4j25</link>
      <guid>https://dev.to/raxxostudios/optimistic-ui-updates-that-never-lie-to-the-user-4j25</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Optimistic UI shows the result in under 16ms, before the server replies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep the old value stashed so rollback is a one-line restore&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Show a specific error on failure, not a silent revert&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry with backoff beats a dead button after one failed request&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A like button that waits 400ms to turn red feels broken. A like button that turns red instantly and then quietly turns gray again when the request fails is worse. I'll show you the pattern I use to get the speed without the lie.&lt;/p&gt;

&lt;p&gt;Optimistic UI means you paint the success state before the server confirms it. The trick is handling the moment it does not confirm. That's the whole post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the delay feels so bad
&lt;/h2&gt;

&lt;p&gt;A network round trip from Berlin to a server in the same region is maybe 30ms to 80ms. Add the server work, the database read, the serialization, and you're often at 200ms to 500ms for a trivial write. That's under the threshold where people call something slow, but it's well over the 100ms where an interaction stops feeling instant.&lt;/p&gt;

&lt;p&gt;So the user taps. Nothing happens for a third of a second. Then the heart fills in.&lt;/p&gt;

&lt;p&gt;That gap trains people to tap twice. They think the first tap missed. Now you've got two requests, a race condition, and a count that reads 2 when it should read 1.&lt;/p&gt;

&lt;p&gt;Optimistic UI closes the gap. You update the local state the instant the tap lands, which runs in the next frame (roughly 16ms at 60fps). The server call fires in the background. If it succeeds, nothing visible changes because you already showed the right thing. If it fails, you clean up.&lt;/p&gt;

&lt;p&gt;The honest version of this has three parts and most people only build one. They build the optimistic update and skip the rollback and the error message. That's the version that lies.&lt;/p&gt;

&lt;p&gt;Here's the shape I keep coming back to:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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;previous&lt;/span&gt; &lt;span class="o"&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;liked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// stash&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;liked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&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;liked&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// paint now&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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;liked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;liked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// roll back&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Could not save your like. Tap to retry.&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="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stash line is the part that makes rollback cheap. You keep the old value in a const before you overwrite the state. If the request throws, you put it back. No refetch, no reload, no guessing what the value used to be.&lt;/p&gt;

&lt;p&gt;I think about this the same way I think about database rollbacks. If you want the deeper context on undoing a change cleanly, see &lt;a href="https://dev.to/blogs/lab/database-migrations-i-can-roll-back-at-2am"&gt;Database Migrations I Can Roll Back at 2am&lt;/a&gt;. Same instinct, different layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollback is the actual feature
&lt;/h2&gt;

&lt;p&gt;Everyone shows the optimistic part in demos. The rollback is where honesty lives, and it's the part that gets cut when a deadline is near.&lt;/p&gt;

&lt;p&gt;Think about what a user sees when the rollback is missing. They tap like and the heart fills, then they scroll away without noticing the request failed silently. They come back later and the heart is empty. Now they don't trust the button at all, because sometimes it works and sometimes it doesn't and there's no pattern they can see.&lt;/p&gt;

&lt;p&gt;A good rollback does two things in the same tick. It restores the old value so the screen matches reality, and it tells the user why.&lt;/p&gt;

&lt;p&gt;The restore has to be exact. If you had a count of 41 and you optimistically bumped it to 42, rolling back means going to 41, not decrementing from whatever the current value is. That matters because another update might have landed in between. The safe move is to stash the full value, not the delta.&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;prevCount&lt;/span&gt; &lt;span class="o"&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;count&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prevCount&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;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prevCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// exact restore, not count - 1&lt;/span&gt;
  &lt;span class="nf"&gt;render&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;For list operations the stash gets bigger. If you optimistically remove an item from a list and the delete fails, you need the item back and in the same position. I stash the item and its index before splicing, then splice it back on failure.&lt;/p&gt;

&lt;p&gt;One thing I learned the hard way: don't animate the rollback the same way you animate the success. If deleting a row slides it out, don't slide it back in on failure. Snap it back instantly and flash the error. The slide-in reads as "here's a new thing," which confuses people. The snap reads as "that didn't happen," which is the truth.&lt;/p&gt;

&lt;p&gt;When the error message is useless the whole pattern falls apart. "Something went wrong" tells the user nothing they can act on. "Could not save, check your connection" at least points somewhere. Structured logs on the server side tell you which failures are real. I set that up on day one, which I covered in &lt;a href="https://dev.to/blogs/lab/structured-logging-i-add-on-day-one-of-every-project"&gt;Structured Logging I Add on Day One of Every Project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry so one dropped packet doesn't kill the action
&lt;/h2&gt;

&lt;p&gt;A single failed request is often not a real failure. It's a flaky connection, a cold server, a dropped packet on a subway WiFi. If you roll back and give up, you've punished the user for the network's problem.&lt;/p&gt;

&lt;p&gt;So I retry before I roll back. Not forever, and not instantly.&lt;/p&gt;

&lt;p&gt;The naive retry loops immediately and hammers a server that might already be struggling. If 500 clients all retry at the same millisecond, you get a thundering herd and the server that was slow becomes a server that's down. The fix is backoff with jitter: wait a bit longer each attempt, and add randomness so the retries spread out instead of landing in sync.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;try&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="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&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;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;attempts&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;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// 200, 400, 800&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// spread them out&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&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;Wrap the API call in that and the optimistic update stays painted the whole time. The user never sees the retries. They see the heart turn red, and it stays red because the second attempt landed. Only after three tries fail do you roll back and show the error.&lt;/p&gt;

&lt;p&gt;I use this exact helper in a lot of places. The full version with the reasoning behind the numbers is in &lt;a href="https://dev.to/blogs/lab/retry-with-backoff-and-jitter-the-version-i-copy-everywhere"&gt;Retry With Backoff and Jitter: The Version I Copy Everywhere&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One caution: only retry things that are safe to repeat. A like toggle is idempotent if you send the target state (liked: true) instead of a command (toggle). Sending the target state means two retries that both land just set it to true twice, which is fine. Sending "toggle" twice flips it back, which is a bug. Design the API so retries can't hurt you, and the whole pattern gets safer.&lt;/p&gt;

&lt;p&gt;And give people a manual retry too. After the automatic attempts fail, the error message itself should be the retry button. "Tap to retry" that re-runs the whole optimistic flow. That turns a dead end into a second chance without a page reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where optimistic UI is a bad idea
&lt;/h2&gt;

&lt;p&gt;Speed is not always worth the risk of showing something false. Some actions should wait for the server, and pretending otherwise erodes trust faster than any spinner.&lt;/p&gt;

&lt;p&gt;Money is the obvious one. Never show "payment complete" optimistically. If the charge fails after you've shown success, you've told someone they paid when they didn't. Wait for the real confirmation. A 600ms spinner on a checkout button is fine because people expect money to take a moment.&lt;/p&gt;

&lt;p&gt;Anything irreversible is the same category. Deleting an account, sending a message that can't be unsent, publishing something public. If rollback can't fully undo the visible effect, don't do the optimistic update.&lt;/p&gt;

&lt;p&gt;The sweet spot is small, reversible, low-stakes actions. Likes, bookmarks, reordering a list, toggling a setting, adding to a cart. If it fails and rolls back, the user shrugs and taps again. Nobody's day is ruined.&lt;/p&gt;

&lt;p&gt;There's also a counting problem to watch. If you optimistically update a number that other users also change, your local view can drift. Say a post has 100 likes, you add one and show 101, but three other people liked it in the same second so the real count is 104. Your optimistic 101 is now wrong in a way rollback won't catch, because the request succeeded.&lt;/p&gt;

&lt;p&gt;I handle this by treating the server response as the source of truth when it arrives. The optimistic value is a guess. When the real count comes back, I replace the guess with it, even on success. The user sees 101 for a moment, then it snaps to 104, which reads as "oh, other people liked it too" rather than a bug.&lt;/p&gt;

&lt;p&gt;For reads rather than writes, the performance questions are different. If your list is huge and the bottleneck is rendering, not the network, optimistic UI won't help. I measured that tradeoff in &lt;a href="https://dev.to/blogs/lab/i-measured-content-visibility-auto-on-3-000-cards"&gt;I Measured content-visibility: auto on 3,000 Cards&lt;/a&gt;, and the fix there is rendering strategy, not request timing.&lt;/p&gt;

&lt;p&gt;Know which problem you have before you reach for this pattern. Optimistic UI fixes perceived latency on writes. It does nothing for a slow render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Build all three parts or skip the pattern. The optimistic paint is the easy 20 percent. The stash-and-restore rollback and the specific error message are the 80 percent that keeps the screen honest when the network drops a packet on a train.&lt;/p&gt;

&lt;p&gt;My default: optimistic for small reversible writes, retry three times with backoff before giving up, and always let the server's real response overwrite my guess. For money and anything irreversible, I wait for confirmation and eat the spinner.&lt;/p&gt;

&lt;p&gt;If you're wiring this into a project from scratch and want a setup that's structured for a solo build, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; is how I lay the groundwork before I write a single component.&lt;/p&gt;

&lt;p&gt;Start with one button. Add the stash, the rollback, the error. Get that one honest, then copy the shape everywhere else.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>AVIF, WebP or JPEG: Picking an Image Format in 2026</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sat, 17 Oct 2026 19:38:18 +0000</pubDate>
      <link>https://dev.to/raxxostudios/avif-webp-or-jpeg-picking-an-image-format-in-2026-2925</link>
      <guid>https://dev.to/raxxostudios/avif-webp-or-jpeg-picking-an-image-format-in-2026-2925</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AVIF cuts photo file size 30-50% below JPEG but encodes slower&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebP is the safe middle ground with 97%+ browser support&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JPEG stays as the final fallback so nobody gets a broken image&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The picture element tries each format top to bottom automatically&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I shipped a product page last month with a hero photo that weighed 1.4MB as JPEG. Re-encoded as AVIF it dropped to 320KB with no visible loss. That's the whole reason this choice matters, and in 2026 you don't have to pick just one.&lt;/p&gt;

&lt;p&gt;Here's how I decide which format to serve, and how I stack all three so a visitor on any browser still sees the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each Format Is Actually Good At
&lt;/h2&gt;

&lt;p&gt;JPEG is 30 years old and it still works everywhere. Every browser, every email client, every ancient Android phone. It handles photos well. It falls apart on sharp edges, text, and flat color, where you get those grey smudges around lines. It can't do transparency. For a photograph where support matters more than bytes, it's still fine.&lt;/p&gt;

&lt;p&gt;WebP is the one I reach for by default. It does both lossy and lossless, it supports transparency, and it animates. On a typical product photo it lands 25-35% smaller than JPEG at the same visual quality. The encode is fast. The support is basically universal now.&lt;/p&gt;

&lt;p&gt;AVIF is the heavy hitter. Based on the AV1 video codec, it crushes file sizes harder than anything else I've tested. That 1.4MB JPEG becoming 320KB wasn't a fluke, 30-50% below JPEG is the normal result. It handles transparency, wide color gamut, and HDR. The catch is encode time. AVIF is slow to compress, sometimes 10-20x slower than WebP for the same image.&lt;/p&gt;

&lt;p&gt;So the rough rule I use:&lt;/p&gt;

&lt;p&gt;Photos where every byte counts: AVIF first.&lt;/p&gt;

&lt;p&gt;UI assets, icons, screenshots with text: WebP, because the encode is quick and the quality holds on sharp edges.&lt;/p&gt;

&lt;p&gt;Anything that has to display on hardware I can't predict: keep a JPEG around.&lt;/p&gt;

&lt;p&gt;One thing people forget is that quality settings aren't comparable across formats. AVIF quality 50 is not WebP quality 50. I tune each one by eye, or by target file size, not by copying a number across. For a 1600px-wide hero I usually land around AVIF quality 55, WebP quality 78, JPEG quality 82. Those numbers drift per image, but they're my starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Support in 2026 and Why It's Not a Blocker
&lt;/h2&gt;

&lt;p&gt;The support question used to end these debates. It doesn't anymore.&lt;/p&gt;

&lt;p&gt;WebP is supported in every current browser and has been for years. If you're only serving WebP with a JPEG fallback, you're covering everyone who's touched a browser update since roughly 2020. No caveats.&lt;/p&gt;

&lt;p&gt;AVIF is the newer one, and it's the reason people still hesitate. But Chrome, Firefox, Edge, Safari, and the mobile versions of all of them decode AVIF now. The gaps are old installs and a few embedded webviews. That's a small slice, and it's shrinking every month.&lt;/p&gt;

&lt;p&gt;The reason none of this stops me is the fallback chain. I don't have to bet on AVIF support. I serve AVIF to browsers that take it and quietly drop down to WebP, then JPEG, for everyone else. The browser picks the first format it understands. No feature detection in JavaScript, no user-agent sniffing, no broken image icon.&lt;/p&gt;

&lt;p&gt;That's the part worth sitting with. You can adopt the newest, smallest format with zero downside, because the fallback is built into HTML itself. The worst case for an old browser is that it gets the JPEG it would have gotten anyway. The best case for a modern browser is a file less than a quarter the size.&lt;/p&gt;

&lt;p&gt;I treat performance work the same way I treat reliability work: assume something will fail and make the failure boring. If you want the same mindset applied to retries, &lt;a href="https://dev.to/blogs/lab/retry-with-backoff-and-jitter-the-version-i-copy-everywhere"&gt;Retry With Backoff and Jitter&lt;/a&gt; walks through the pattern I copy into every project.&lt;/p&gt;

&lt;p&gt;The only real cost of adding AVIF is the encode step at build time, and that's a machine problem, not a user problem. Which brings up the trade I actually think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encode Cost Versus File Size
&lt;/h2&gt;

&lt;p&gt;Smaller files are not free. Somebody's CPU paid for them.&lt;/p&gt;

&lt;p&gt;AVIF's compression comes from doing a lot of work up front. On my build machine, encoding a single 2000px photo to AVIF at a good quality setting takes a few seconds. WebP does the same image in a fraction of that. JPEG is near instant. Multiply by a catalog of a few hundred product shots and the AVIF step is suddenly the slowest thing in the build.&lt;/p&gt;

&lt;p&gt;I handle this with a cache. I hash the source image plus the encode settings, and if nothing changed I reuse the output. So AVIF only pays its slow encode once per image, not once per build. After the first run, rebuilds skip anything untouched. This is the same caching instinct I use for catalog lookups, which I benchmarked in &lt;a href="https://dev.to/blogs/lab/binary-search-vs-array-includes-on-a-50-000-row-catalog"&gt;Binary Search vs Array.includes()&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The numbers that make the slow encode worth it: a 10-image gallery at 1.4MB each is 14MB of JPEG. The same gallery in AVIF can come in under 4MB. That's the difference between a page that loads instantly on mobile and one where images pop in a second late while someone's already scrolling past.&lt;/p&gt;

&lt;p&gt;I also don't encode every size for every format. I pick two or three widths (say 640, 1280, 1920), and generate AVIF and WebP for each, plus one JPEG fallback per width. That's a manageable fan-out. Going wider than three sizes rarely earns its keep for the kind of pages I build.&lt;/p&gt;

&lt;p&gt;If you're generating or upscaling source art before you compress it, the encode step runs on whatever you export. I run a lot of my source images through &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; for upscaling first, then compress the result into the format stack. Clean source in, small files out.&lt;/p&gt;

&lt;p&gt;One honest caveat: AVIF can look worse than WebP at very low file sizes on images with fine grain or film noise. It smooths detail it thinks is unimportant. For a product on a plain background that's invisible. For textured photography, check it with your own eyes before trusting the byte count.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fallback Chain I Actually Ship
&lt;/h2&gt;

&lt;p&gt;This is the whole thing in one HTML element. The &lt;code&gt;picture&lt;/code&gt; element lets you list sources in order, and the browser uses the first one it supports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Product hero shot"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1280"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"720"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser reads top to bottom. If it's AVIF-capable, it takes the first line and stops. If there's no AVIF support but WebP works, it uses the second line instead. Nothing modern at all? It falls through to the plain &lt;code&gt;img&lt;/code&gt;, which every browser understands. That &lt;code&gt;img&lt;/code&gt; is not optional. It's the thing that guarantees nobody sees a broken image.&lt;/p&gt;

&lt;p&gt;A few details I never skip.&lt;/p&gt;

&lt;p&gt;Always set &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; on the &lt;code&gt;img&lt;/code&gt;. This reserves the space before the image loads so the page doesn't jump around. Layout shift is one of the easiest performance wins and most people leave it on the table.&lt;/p&gt;

&lt;p&gt;Always write a real &lt;code&gt;alt&lt;/code&gt;. The fallback chain protects the format, the alt protects the meaning when the image itself fails.&lt;/p&gt;

&lt;p&gt;For responsive sizes, each &lt;code&gt;source&lt;/code&gt; gets its own &lt;code&gt;srcset&lt;/code&gt; with the width descriptors, and a &lt;code&gt;sizes&lt;/code&gt; attribute so the browser knows how wide the image renders. That gets verbose fast, which is why I generate the whole block from a template rather than writing it by hand. I feed the image name and dimensions in, and the markup comes out with all three formats and all my widths wired up.&lt;/p&gt;

&lt;p&gt;I keep that generation logic in the same place as the rest of my asset pipeline, with structured logs so I can see which images got re-encoded and which came from cache. If that sounds useful, &lt;a href="https://dev.to/blogs/lab/structured-logging-i-add-on-day-one-of-every-project"&gt;Structured Logging I Add on Day One&lt;/a&gt; is how I set that up.&lt;/p&gt;

&lt;p&gt;The result is one source of truth. I drop in a JPEG or PNG, the build produces the AVIF and WebP variants, caches them, and writes the &lt;code&gt;picture&lt;/code&gt; markup. I never touch the fallback chain by hand, so I never get it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you do nothing else, serve WebP with a JPEG fallback. That's a 25-35% size cut on your photos with support you don't have to think about, and it's maybe an hour of pipeline work.&lt;/p&gt;

&lt;p&gt;If you want the bigger drop and you've got a build step that can cache, add AVIF as the top of the chain. The &lt;code&gt;picture&lt;/code&gt; element means there's no risk: old browsers get the JPEG they'd have gotten anyway, modern ones get a file a third of the size. Just watch textured images by eye, and cache the slow encode so it only runs once.&lt;/p&gt;

&lt;p&gt;I'd start with the three-format stack on your heaviest page, measure the load, and roll it out from there. The pattern is the same everywhere, which is the point. I document how I wire these build-time steps into a repeatable setup in the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; if you want the fuller picture of how my pipeline fits together.&lt;/p&gt;

&lt;p&gt;Pick the format per use, keep the fallback, and let the browser do the choosing.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Database Migrations I Can Roll Back at 2am</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:01:58 +0000</pubDate>
      <link>https://dev.to/raxxostudios/database-migrations-i-can-roll-back-at-2am-4356</link>
      <guid>https://dev.to/raxxostudios/database-migrations-i-can-roll-back-at-2am-4356</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Every schema change ships with a down step, tested before deploy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expand and contract splits one risky change into four safe ones&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploy code before the destructive migration, never the reverse&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A dropped column at 2am can't be undone, so never drop first&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst database mistake I ever made was a migration with no down step, run at 2am, that dropped a column before I had a backup. I spent three hours reconstructing data from logs. Now every schema change I ship can roll back cleanly, and I've never had that panic again.&lt;/p&gt;

&lt;p&gt;Here's how I structure migrations so the rollback is boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Change Ships With Its Down Step
&lt;/h2&gt;

&lt;p&gt;A migration has two halves. The up step moves the schema forward. The down step moves it back. Most ORM tools generate both for you, and most people delete or ignore the down step because it feels like extra work.&lt;/p&gt;

&lt;p&gt;I don't. If I can't write the down step, I don't ship the up step.&lt;/p&gt;

&lt;p&gt;The reason is simple. At 2am, half asleep, with an error spiking, I don't want to be writing SQL. I want to run one command that puts the schema back exactly where it was five minutes ago. If I planned the reverse ahead of time, that command already exists.&lt;/p&gt;

&lt;p&gt;Writing the down step also forces me to think about whether the change is reversible at all. Adding a column? Easy to reverse, just drop it. Renaming a column? That's two operations pretending to be one, and the naive version loses data on rollback. Dropping a column? There is no honest down step, because the data is gone. The moment I write "down: re-add the column" I realize the data to fill it doesn't exist anymore.&lt;/p&gt;

&lt;p&gt;That realization is the whole point. The down step is a test of whether the change is safe, and I'd rather fail that test at my desk than in production.&lt;/p&gt;

&lt;p&gt;I test the down step before I deploy. I run up, then immediately run down, then up again. If the schema doesn't return to a working state after that cycle, the migration isn't ready. This catches things like a foreign key that gets dropped on the way down but not recreated, or an index that survives a rollback and collides with the next up run.&lt;/p&gt;

&lt;p&gt;The same discipline shows up in how I handle undo across my tools. If you want the deeper context on reversible operations, see &lt;a href="https://dev.to/blogs/lab/the-undo-button-every-raxxo-tool-ships-with"&gt;The Undo Button Every RAXXO Tool Ships With&lt;/a&gt;. The database version is just the same idea with foreign keys attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expand And Contract: One Change, Four Steps
&lt;/h2&gt;

&lt;p&gt;The pattern that saved me is expand and contract. Instead of changing a column in place, I split the change into stages that are each safe on their own.&lt;/p&gt;

&lt;p&gt;Say I want to rename &lt;code&gt;user_name&lt;/code&gt; to &lt;code&gt;username&lt;/code&gt;. The dangerous way is one migration that renames the column. If anything goes wrong mid-deploy, old code looks for &lt;code&gt;user_name&lt;/code&gt; and new code looks for &lt;code&gt;username&lt;/code&gt;, and one of them is always wrong.&lt;/p&gt;

&lt;p&gt;The expand and contract way takes four steps across two or more deploys.&lt;/p&gt;

&lt;p&gt;First, expand. I add the new &lt;code&gt;username&lt;/code&gt; column alongside the old one. Nothing reads from it yet. This migration is pure addition, so its down step is a clean drop with zero data loss.&lt;/p&gt;

&lt;p&gt;Second, backfill. I copy data from &lt;code&gt;user_name&lt;/code&gt; into &lt;code&gt;username&lt;/code&gt;, and I make the application write to both columns on every update. Now the two columns stay in sync. Old code reads the old column, new code can start reading the new one.&lt;/p&gt;

&lt;p&gt;Third, migrate reads. I deploy code that reads from &lt;code&gt;username&lt;/code&gt;. The old column still exists and still gets written, so if I need to roll this deploy back, nothing breaks.&lt;/p&gt;

&lt;p&gt;Fourth, contract. Once I'm confident the new column is correct and nothing reads the old one, I drop &lt;code&gt;user_name&lt;/code&gt; in a separate migration, days later.&lt;/p&gt;

&lt;p&gt;Each step is reversible until the very last one. The only irreversible operation, the drop, happens when I'm calm and certain, not in the middle of a deploy. For large tables the backfill runs in batches so I don't lock a 50,000-row table in one transaction. I covered batching big scans in more depth at &lt;a href="https://dev.to/blogs/lab/binary-search-vs-array-includes-on-a-50-000-row-catalog"&gt;Binary Search vs Array.includes() on a 50,000-Row Catalog&lt;/a&gt;, and the same instinct applies here: never touch every row at once if you can help it.&lt;/p&gt;

&lt;p&gt;This feels slower. It is slower. Four deploys instead of one. But I've never corrupted data with this pattern, and the four small steps each take two minutes while the one big step takes three hours when it goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Order Of Deploy And Migrate Matters
&lt;/h2&gt;

&lt;p&gt;The single rule that prevents most 2am incidents: additive migrations run before the deploy, destructive migrations run after.&lt;/p&gt;

&lt;p&gt;Think about what happens during a deploy. There's a window where old code and new code both exist. Instances restart one at a time. For thirty seconds, maybe two minutes, the database serves both versions of your application at once.&lt;/p&gt;

&lt;p&gt;If I add a column before deploying the code that uses it, both old and new code are fine. Old code ignores the column. New code finds it waiting. The additive migration ran first, so the schema is always ahead of the code.&lt;/p&gt;

&lt;p&gt;If I drop a column during that window, old code that still expects it crashes. So drops wait until every instance runs the new code. Destructive migration runs last, after the deploy fully settles.&lt;/p&gt;

&lt;p&gt;The order flips depending on direction:&lt;/p&gt;

&lt;p&gt;For adding things, migrate then deploy. The schema leads.&lt;/p&gt;

&lt;p&gt;For removing things, deploy then migrate. The code leads.&lt;/p&gt;

&lt;p&gt;Rollback follows the same logic in reverse. If I roll back a deploy, the old code comes back, and I need the old schema to still support it. That's exactly why the destructive step waited. The column is still there, so the rolled-back code finds everything it expects. No data lost, because I never dropped anything the old code depended on.&lt;/p&gt;

&lt;p&gt;This is the piece people skip, and it's the piece that turns a routine rollback into a data-loss event. A rollback should never require a down migration that recreates data. If your rollback plan is "run the down step to re-add the dropped column," you've already lost, because the data to fill it is gone.&lt;/p&gt;

&lt;p&gt;I keep a one-line rule in my deploy notes: a rollback must work with the schema that's live right now, without running any down migration at all. If that's not true, the migration order is wrong. For how I watch the deploy window itself, &lt;a href="https://dev.to/blogs/lab/structured-logging-i-add-on-day-one-of-every-project"&gt;Structured Logging I Add on Day One of Every Project&lt;/a&gt; covers the tracing I lean on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failures That Need Retries, Not Rollbacks
&lt;/h2&gt;

&lt;p&gt;Not every migration problem is a rollback problem. Some are just a flaky connection, and rolling back is the wrong response.&lt;/p&gt;

&lt;p&gt;A long backfill on a big table can fail halfway because the database connection drops, or a lock times out, or a replica falls behind. If I panic and roll back, I've thrown away the half that worked. The better move is making the migration safe to re-run.&lt;/p&gt;

&lt;p&gt;I write backfills to be idempotent. Instead of "copy every row," the query is "copy every row where the target is still null." Run it once, it fills 20,000 rows. The connection drops. Run it again, it fills the remaining 30,000 and skips the ones already done. No duplicates, no corruption, no need to start over.&lt;/p&gt;

&lt;p&gt;For the actual retry logic around a flaky migration runner, I use the same backoff I use everywhere. If you want the exact code, &lt;a href="https://dev.to/blogs/lab/retry-with-backoff-and-jitter-the-version-i-copy-everywhere"&gt;Retry With Backoff and Jitter&lt;/a&gt; is the version I copy into every project. A backfill that hits a transient lock should wait and retry, not fail the whole deploy.&lt;/p&gt;

&lt;p&gt;The distinction I hold onto: a schema change that's structurally wrong gets a rollback. A schema change that's structurally fine but hit a network hiccup gets a retry. Mixing those up is how people make a small problem bigger.&lt;/p&gt;

&lt;p&gt;I also set a statement timeout on every migration. Without one, a migration that grabs a lock can block every query on the table and bring the whole app down while it waits. With a timeout, the migration fails fast, I see the lock contention, and I run it again during a quieter window. A failed migration that failed in two seconds is a non-event. A migration that hung for ten minutes holding a lock is an outage.&lt;/p&gt;

&lt;p&gt;The running database here is Postgres behind a &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; storefront and a few side services, so I care a lot about not locking tables that serve live traffic. Idempotent, timeout-bounded, re-runnable. That trio keeps a shaky migration from turning into a decision I have to make at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you only change one thing, make it this: write the down step before you write the up step, and test the full up-down-up cycle at your desk. That one habit catches the irreversible changes before they reach production.&lt;/p&gt;

&lt;p&gt;Then layer in the order rule. Additive changes migrate before deploy, destructive changes migrate after, and drops wait days behind their own separate migration. A rollback should work against the live schema with no down migration required. If it doesn't, you've ordered something wrong.&lt;/p&gt;

&lt;p&gt;Expand and contract feels like overkill until the first time it saves you from a 2am data loss. After that it's just how you rename a column.&lt;/p&gt;

&lt;p&gt;I keep these rules in the same operations playbook I build every project from. If you want the full setup I start with, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through how I wire logging, retries, and migrations together from day one. Start there, steal what's useful, and make your next rollback boring.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>How I Write Alt Text That Helps Screen Readers and Search</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Thu, 15 Oct 2026 20:29:14 +0000</pubDate>
      <link>https://dev.to/raxxostudios/how-i-write-alt-text-that-helps-screen-readers-and-search-505i</link>
      <guid>https://dev.to/raxxostudios/how-i-write-alt-text-that-helps-screen-readers-and-search-505i</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Decorative images get empty alt (alt=""), never missing alt&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aim for 8 to 15 words, one sentence, no "image of"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product alt names the thing plus one distinguishing detail&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Charts get the takeaway, not the axis labels&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alt text is the one accessibility task that pays off twice. A screen reader reads it out loud to someone who can't see the image, and a search engine reads it to understand what the page is about. Get it right once and both audiences win.&lt;/p&gt;

&lt;p&gt;Most alt text I see on the web is either missing entirely or stuffed with keywords that help nobody.&lt;/p&gt;

&lt;p&gt;Here's how I decide what to write and when to write nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  When An Image Needs Alt Text And When It Should Be Empty
&lt;/h2&gt;

&lt;p&gt;The first decision isn't what to write. It's whether to write anything.&lt;/p&gt;

&lt;p&gt;An image needs real alt text when it carries information the surrounding text doesn't. A product photo, a diagram, a screenshot of a result, a chart with numbers in it. If the image went missing and the page got worse, it needs a description.&lt;/p&gt;

&lt;p&gt;An image should have empty alt text when it's decorative. A background texture, a divider line, a stock photo of a laptop next to an article about laptops. These add nothing a screen reader user needs. For those I write &lt;code&gt;alt=""&lt;/code&gt;, an empty but present attribute.&lt;/p&gt;

&lt;p&gt;That empty string matters.&lt;/p&gt;

&lt;p&gt;There's a real difference between &lt;code&gt;alt=""&lt;/code&gt; and no alt attribute at all. Empty alt tells the screen reader "skip this on purpose." A missing attribute makes the screen reader fall back to reading the file name out loud, so the user hears "IMG underscore 4471 dot jpg" in the middle of a sentence. That's the worst outcome and it happens constantly.&lt;/p&gt;

&lt;p&gt;So my rule is simple. Every image gets an alt attribute, always. The only question is whether it's filled or empty.&lt;/p&gt;

&lt;p&gt;A quick test I run in my head: if I read the page out loud to a friend over the phone, would I describe this image? If yes, write it. If I'd skip it because the text already covered the point, leave it empty.&lt;/p&gt;

&lt;p&gt;Icons are the tricky middle case. An icon that's purely visual next to a text label gets empty alt, because the label already says "Settings." An icon that stands alone as a button, like a magnifying glass with no word next to it, needs alt text that says what it does: "Search."&lt;/p&gt;

&lt;p&gt;One more case worth naming. If an image is also a link, the alt text describes where the link goes, not what the picture looks like. A logo that links home gets &lt;code&gt;alt="RAXXO home"&lt;/code&gt;, not &lt;code&gt;alt="orange logo"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I apply this same "empty on purpose" discipline to other invisible states in my tools. The idea of a deliberate default shows up in &lt;a href="https://dev.to/blogs/lab/the-undo-button-every-raxxo-tool-ships-with"&gt;The Undo Button Every RAXXO Tool Ships With&lt;/a&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Long Alt Text Should Be
&lt;/h2&gt;

&lt;p&gt;Short. Shorter than you think.&lt;/p&gt;

&lt;p&gt;My target is 8 to 15 words. One sentence, no period needed, no padding. A screen reader user hears this text read out loud, and nobody wants to sit through a paragraph describing a thumbnail.&lt;/p&gt;

&lt;p&gt;Here's the trap. People treat alt text as a place to write everything they know about the image. That turns a 10-word description into a 60-word essay the listener can't skip past easily. If the image genuinely needs a long explanation, like a complex infographic, the right move is a short alt plus a longer description in the visible page text or a caption.&lt;/p&gt;

&lt;p&gt;Skip the phrases "image of" and "photo of." The screen reader already announces that it's an image before reading your text. So "image of a blue mug" gets read as "image, image of a blue mug." You said image twice. Just write "blue ceramic mug on a wooden desk."&lt;/p&gt;

&lt;p&gt;Watch for keyword stuffing.&lt;/p&gt;

&lt;p&gt;I've seen alt text like "best coffee mug buy coffee mug cheap coffee mug online." A search engine treats that as spam, and a human listener hears gibberish. One honest description beats five repeated keywords every time.&lt;/p&gt;

&lt;p&gt;Punctuation matters less than rhythm. I write alt text the way I'd say it. "Dashboard showing 1,200 orders in the last 30 days" reads cleanly out loud. "Dashboard; orders; 30-day view; metrics" does not.&lt;/p&gt;

&lt;p&gt;When I generate or upscale images for product pages, I write the alt text in the same pass, while I still remember what the image was meant to show. If I'm using something like &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; to clean up a product render, the description goes in right then, not three weeks later when I've forgotten which variant this was.&lt;/p&gt;

&lt;p&gt;A good length check: read your alt text out loud at normal speed. If you run out of breath or lose the thread before the end, cut it.&lt;/p&gt;

&lt;p&gt;Most of my best alt text lands around 10 words and says exactly one thing clearly.&lt;/p&gt;

&lt;p&gt;I also keep a small list of alt text patterns the way I keep naming patterns. Consistency helps, and I wrote about that habit in &lt;a href="https://dev.to/blogs/lab/the-command-naming-convention-every-raxxo-tool-follows"&gt;The Command Naming Convention Every RAXXO Tool Follows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describing A Product Image So Both Audiences Get The Point
&lt;/h2&gt;

&lt;p&gt;Product images are where good alt text earns its keep. A shopper using a screen reader is deciding whether to buy, and a search engine is deciding whether to rank your product for the right query. Both need the same core facts.&lt;/p&gt;

&lt;p&gt;My formula: name the product, then add the one detail that distinguishes this photo from the other photos of the same product.&lt;/p&gt;

&lt;p&gt;So for a listing with five images of the same jacket, I don't write "green jacket" five times. I write "green waxed jacket, front view," then "green waxed jacket, inside lining," then "green waxed jacket worn open over a sweater." Each one adds something. A sighted shopper flips through photos to learn more; the alt text should give a screen reader user that same progression.&lt;/p&gt;

&lt;p&gt;Color, material, and the angle or context are usually the facts that matter most. For a mug: "matte black mug, 350ml, held in one hand for scale." That last part tells someone the size better than a number alone.&lt;/p&gt;

&lt;p&gt;Avoid describing emotion or vibe. "Cozy inviting lifestyle shot" means nothing to a listener trying to decide on a purchase. Say what's actually pictured: "mug steaming on a windowsill next to an open book."&lt;/p&gt;

&lt;p&gt;Price and stock go in the page text, not the alt. Those change, and alt text shouldn't carry data that goes stale.&lt;/p&gt;

&lt;p&gt;If you run your store on &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt;, the alt field sits right in the media editor for each image, and the theme usually outputs it correctly. The platform handles the plumbing. The words are still your job, and the default is almost always blank.&lt;/p&gt;

&lt;p&gt;One thing I watch: variant images.&lt;/p&gt;

&lt;p&gt;If a product has 12 color variants and each has its own photo, generic alt like "product image" wastes all of them. The color name is the single most useful word you can add, so "rust orange variant, folded flat" does real work for search and for a listener comparing options.&lt;/p&gt;

&lt;p&gt;When I repurpose product shots into short social posts, the alt text travels with them. I described that whole pipeline in &lt;a href="https://dev.to/blogs/lab/how-i-turn-one-long-article-into-a-week-of-short-posts"&gt;How I Turn One Long Article Into a Week of Short Posts&lt;/a&gt;, and keeping alt text attached across platforms saves rewriting it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describing A Chart Or Diagram Without Listing Every Axis
&lt;/h2&gt;

&lt;p&gt;Charts break the "describe what you see" rule, and this is the part most people get wrong.&lt;/p&gt;

&lt;p&gt;A sighted reader doesn't read a chart pixel by pixel. They glance at it and absorb the takeaway: sales went up, this bar is the tallest, the trend is flat. So the alt text should deliver that takeaway, not recite the axis labels and every data point.&lt;/p&gt;

&lt;p&gt;Bad alt for a bar chart: "Bar chart with x-axis labeled months January through June and y-axis labeled sales from 0 to 500." That describes the furniture, not the story. A listener still has no idea what the chart says.&lt;/p&gt;

&lt;p&gt;Good alt: "Bar chart showing sales rising from 120 in January to 480 in June." Now they know the point in one sentence.&lt;/p&gt;

&lt;p&gt;For a chart with a clear single message, lead with the message. "Monthly sales nearly doubled over the first half of the year." That's often better than any number because it's the reason the chart is on the page.&lt;/p&gt;

&lt;p&gt;When the chart genuinely holds data a listener might need, I give short alt plus a real alternative. A caption with the key figures, or better, the underlying numbers in an actual table on the page. A data table is readable by screen readers cell by cell, so complex data belongs there, not crammed into alt text.&lt;/p&gt;

&lt;p&gt;Pie charts: name the biggest slice and roughly how dominant it is. "Pie chart, mobile traffic makes up about 70 percent, desktop the rest." Nobody needs all six slivers read out.&lt;/p&gt;

&lt;p&gt;Line charts: direction and shape.&lt;/p&gt;

&lt;p&gt;"Line chart trending steadily upward, with a dip around April." The dip is worth mentioning because a sighted reader would notice it instantly.&lt;/p&gt;

&lt;p&gt;The same logic applies to screenshots of data. If I post a screenshot of a dashboard, the alt text says the number that matters, like "dashboard showing 3,400 downloads this week," not "screenshot of a dashboard interface with sidebar and charts."&lt;/p&gt;

&lt;p&gt;Audio content gets the same respect. When I describe a chart in a voiceover using &lt;a href="https://try.elevenlabs.io/8pbaehnkoq4u" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt;, I lead with the takeaway there too, because a listener's attention works the same way whether it's a screen reader or a narrator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Write an alt attribute on every image, then decide if it's filled or empty. Decorative images get &lt;code&gt;alt=""&lt;/code&gt;. Everything that carries meaning gets one short sentence, 8 to 15 words, that says the thing the picture is actually there to say.&lt;/p&gt;

&lt;p&gt;For products, name it plus the one detail this photo adds. For charts, give the takeaway, not the axis labels, and push complex data into a real table.&lt;/p&gt;

&lt;p&gt;If you want a repeatable way to bake this into your whole publishing setup, start with the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;. It's how I keep small habits like alt text from slipping through the cracks when I'm moving fast.&lt;/p&gt;

&lt;p&gt;The best part is that none of this is extra work once it's a reflex. You write the words while the image is fresh, and a screen reader user and a search engine both thank you for it. Start with your next upload.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Retry With Backoff and Jitter: The Version I Copy Everywhere</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:53:29 +0000</pubDate>
      <link>https://dev.to/raxxostudios/retry-with-backoff-and-jitter-the-version-i-copy-everywhere-1b9o</link>
      <guid>https://dev.to/raxxostudios/retry-with-backoff-and-jitter-the-version-i-copy-everywhere-1b9o</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Naive retries hammer a recovering server and turn blips into outages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full jitter spreads retries so clients stop firing in sync&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry 429 and 503, never 400 or 401, and respect idempotency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 20-line function I paste into every project, copy it&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single failed API call shouldn't take down your backend. But the naive retry loop most people write does exactly that, because it waits the same amount of time on every client and fires them all back at once. Here's the version I copy into every project instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive retries make outages worse
&lt;/h2&gt;

&lt;p&gt;Picture a service that handles a thousand requests a second. Something hiccups for two seconds. A load balancer blips, a database connection pool fills up, whatever. During those two seconds, maybe two thousand requests fail.&lt;/p&gt;

&lt;p&gt;Now every one of those clients retries. If they all retry immediately, you get two thousand extra requests the instant the service comes back. On top of the normal thousand per second. The service that was already struggling now gets hit with triple the load at the worst possible moment.&lt;/p&gt;

&lt;p&gt;That's a retry storm. The retries cause the outage to keep going.&lt;/p&gt;

&lt;p&gt;I've seen this pattern play out with fixed-delay retries too. "Wait one second, then try again" sounds safe. But if all your clients failed at the same moment, they all wait one second, and they all come back at the same moment. You've just moved the stampede one second into the future. You didn't spread it out.&lt;/p&gt;

&lt;p&gt;The naive loop looks reasonable on paper:&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;try&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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;Five tries, one second apart. Fine for one client sitting alone. Terrible when ten thousand clients run the identical loop and fail together.&lt;/p&gt;

&lt;p&gt;The fix has two parts. First, back off more each time so you're not pounding a recovering service. Second, add randomness so clients spread out instead of syncing up. The first part is exponential backoff. The second part is jitter. People remember the first and forget the second, and the second is the one that actually saves you.&lt;/p&gt;

&lt;p&gt;Backoff alone still clusters. If everyone uses &lt;code&gt;delay = base * 2^attempt&lt;/code&gt;, then everyone's third retry lands at the same tick. You need the random offset to break the alignment. Think of it as politeness from each client toward a server that's having a bad day. If you want to see the failures clearly while this is happening, you'll want logging in place first. I covered this in more depth at &lt;a href="https://dev.to/blogs/lab/structured-logging-i-add-on-day-one-of-every-project"&gt;Structured Logging I Add on Day One of Every Project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full jitter, and why I don't use the other kinds
&lt;/h2&gt;

&lt;p&gt;There are a few flavors of jitter. I'll save you the comparison I did and tell you the winner: full jitter.&lt;/p&gt;

&lt;p&gt;With full jitter, you compute the exponential ceiling, then pick a random delay between zero and that ceiling. So on attempt three with a 200ms base, the ceiling might be 1600ms, and you sleep somewhere between 0 and 1600ms. Every client picks its own number. They scatter across the whole window instead of bunching at the top.&lt;/p&gt;

&lt;p&gt;The formula is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
cap = min(maxDelay, base * 2^attempt)
delay = random(0, cap)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to "equal jitter," where you sleep half the ceiling plus a random half. That still keeps a floor under every client, so they're less spread out. Full jitter uses the whole range and spreads the hardest.&lt;/p&gt;

&lt;p&gt;The counterintuitive part: picking a delay that can be as low as zero feels wrong. Won't some clients retry almost instantly? Yes, a few will. But only a few, because the random pick is uniform across the window. The rest land all over the place. The total load at any single instant stays low, which is the entire point.&lt;/p&gt;

&lt;p&gt;I cap the maximum delay too. Without a cap, &lt;code&gt;2^attempt&lt;/code&gt; grows fast, and by attempt ten you're waiting 200 seconds. A 30-second cap is usually sensible. I also cap the number of attempts, because retrying forever just hides a real problem. Four or five attempts is my default.&lt;/p&gt;

&lt;p&gt;One more thing: measure before you tune these numbers. Base delay, cap, and attempt count all interact. The habit of counting actual calls instead of guessing is the same one I used in &lt;a href="https://dev.to/blogs/lab/debounce-vs-throttle-i-counted-every-call"&gt;Debounce vs Throttle: I Counted Every Call&lt;/a&gt;. Don't pick 2^attempt because it's traditional. Pick it because you watched the timing and it spread your load the way you needed.&lt;/p&gt;

&lt;p&gt;If your clients are all on cron jobs that fire on the minute, jitter matters even more. Without it, every scheduled job hits your API on the exact same second, every minute, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which errors deserve a retry, and which never do
&lt;/h2&gt;

&lt;p&gt;Retrying the wrong error is worse than not retrying at all. You burn attempts, you add latency, and sometimes you do real damage.&lt;/p&gt;

&lt;p&gt;Retry these: HTTP 429 (too many requests), 503 (service unavailable), 502 and 504 (gateway errors), and network-level failures like a connection reset or a timeout. These are temporary. The server is busy or briefly unreachable, and a later attempt has a real chance of working.&lt;/p&gt;

&lt;p&gt;Never retry these: 400 (bad request), 401 (unauthorized), 403 (forbidden), 404 (not found), and 422 (unprocessable). These are your fault, not a transient blip. Your payload is malformed, your token is wrong, or the thing doesn't exist. Retrying sends the identical broken request again and gets the identical rejection. You just wasted five round trips to confirm what the first one told you.&lt;/p&gt;

&lt;p&gt;For 429 specifically, check for a &lt;code&gt;Retry-After&lt;/code&gt; header. If the server tells you exactly when to come back, use that number instead of your computed backoff. The server knows better than your formula does.&lt;/p&gt;

&lt;p&gt;Then there's idempotency, which is the part people skip and regret. A GET is safe to retry; you're just reading. A PUT or DELETE is usually safe because repeating it lands you in the same state. A POST is the dangerous one. If you POST "create order," time out before the response, and retry, you might create two orders. The first one may have succeeded and you never saw the reply.&lt;/p&gt;

&lt;p&gt;So for non-idempotent writes, either don't retry, or send an idempotency key so the server deduplicates. Most payment and order APIs support a key header exactly for this. Set it once per logical operation and reuse it across retries.&lt;/p&gt;

&lt;p&gt;I keep a tiny classifier function that answers one question: is this error retryable? It checks the status code against the retry set, checks for network error types, and returns a boolean. Keeping that logic in one place means I never second-guess it at the call site.&lt;/p&gt;

&lt;p&gt;Timeouts are the subtle case. A timeout on a read is retryable. A timeout on a write might mean the write already happened. Treat write timeouts with the same caution as a POST. If you want to understand the failure modes that hide behind a simple-looking call, &lt;a href="https://dev.to/blogs/lab/i-found-the-exact-recursion-depth-that-crashes-node"&gt;I Found the Exact Recursion Depth That Crashes Node&lt;/a&gt; is a good companion read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The function I actually paste
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing. It's short on purpose. I'd rather read twenty lines than trust a dependency for this.&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;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;200&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;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;30000&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;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&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;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;RETRYABLE&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;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ECONNRESET&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&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;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ceiling&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&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="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Base delay, a cap, a max attempt count, full jitter on every wait. You call it like &lt;code&gt;await retry(() =&amp;gt; fetch(url))&lt;/code&gt; and it handles the rest.&lt;/p&gt;

&lt;p&gt;A few notes on using it. Pass your own retryable check when you need one; the default set covers most HTTP cases, but a database driver throws different error shapes. I usually add an &lt;code&gt;onRetry&lt;/code&gt; callback so I can log each attempt with the delay it chose, which makes retry storms obvious in the logs when they happen.&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;Retry-After&lt;/code&gt; header, I read it before computing the jitter and clamp my delay to at least that value. The server's instruction wins.&lt;/p&gt;

&lt;p&gt;Don't wrap this around non-idempotent writes without an idempotency key. The function doesn't know your POST is dangerous. That judgment stays with you at the call site.&lt;/p&gt;

&lt;p&gt;I keep the retryable set small and explicit on purpose. It's tempting to retry on any 5xx, but a 500 is often a genuine bug on the server that won't fix itself in 400ms. I'd rather fail fast and see it than mask it behind four quiet retries.&lt;/p&gt;

&lt;p&gt;This same instinct (small, boring, reusable building blocks I trust) is how I build everything at the studio. The full approach is in the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;. It's the same idea applied to a whole workflow instead of one function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Copy the function above into your next project and tune three numbers: base delay, cap, and max attempts. Start with 200ms, 30 seconds, five tries, then watch your logs and adjust once you see real failures.&lt;/p&gt;

&lt;p&gt;The single change that matters most is the jitter line. &lt;code&gt;Math.random() * ceiling&lt;/code&gt; instead of a fixed wait is what stops your clients from stampeding a server that's already down. Everything else is detail.&lt;/p&gt;

&lt;p&gt;And before you wrap any write in a retry, ask whether running it twice is safe. If it isn't, add an idempotency key or leave the retry off. A missed error you can see beats a duplicate order you can't.&lt;/p&gt;

&lt;p&gt;If you want the broader way I think about resilient, boring code that keeps working when I'm not looking at it, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the whole setup. Grab the function, ship it today, and your next outage stays a blip instead of becoming the story.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Focus States That Survive a Redesign: A Keyboard-First Checklist</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Tue, 13 Oct 2026 21:56:29 +0000</pubDate>
      <link>https://dev.to/raxxostudios/focus-states-that-survive-a-redesign-a-keyboard-first-checklist-jd5</link>
      <guid>https://dev.to/raxxostudios/focus-states-that-survive-a-redesign-a-keyboard-first-checklist-jd5</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Redesigns silently break focus states; I run a 6-point check before every ship&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace :focus with :focus-visible so mouse clicks stay quiet, keyboards stay loud&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 3:1 contrast outline against both themes is the bar I hold&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tab the whole page once with the mouse unplugged, no exceptions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A visual refresh almost always eats the keyboard experience. The design looks cleaner, the colors are nicer, and somewhere in the diff the focus outline quietly vanished. I've shipped enough redesigns to treat this as a certainty, not a risk, so I built a short checklist I run every single time.&lt;/p&gt;

&lt;p&gt;Here's the whole thing. You can run it on your own site in about twenty minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Redesigns Eat Focus States First
&lt;/h2&gt;

&lt;p&gt;Focus is invisible until you need it. That's the core problem. When I'm staring at a new layout in the browser, I'm clicking with a mouse, and a mouse never shows me a focus ring. So the one thing a keyboard user depends on is the one thing I'm least likely to notice is gone.&lt;/p&gt;

&lt;p&gt;The usual culprit is a CSS reset. A designer drops in &lt;code&gt;outline: none&lt;/code&gt; to kill the default browser ring because it "looks ugly," and then forgets to add anything back. Now every button, link and input is a dead end for anyone using Tab.&lt;/p&gt;

&lt;p&gt;The second culprit is a component library swap. You move from one set of styled components to another, and the new ones ship with their own focus behavior. Maybe it's there, maybe it's a 1px gray line that disappears against a gray background. You inherited someone else's decision and never checked it.&lt;/p&gt;

&lt;p&gt;The third is theme work. You add a dark mode, the outline color was hardcoded to a dark value, and now it's invisible on the dark canvas. The light theme is fine. The dark one is a black box, literally.&lt;/p&gt;

&lt;p&gt;I treat the redesign diff as guilty until proven innocent. Any time I touch global styles, swap a component, or add a theme, I assume focus is broken and I go check. This is the same defensive habit behind &lt;a href="https://dev.to/blogs/lab/the-undo-button-every-raxxo-tool-ships-with"&gt;The Undo Button Every RAXXO Tool Ships With&lt;/a&gt;: assume the scary path is the one nobody tests, then test it on purpose.&lt;/p&gt;

&lt;p&gt;One more thing worth saying plainly. Keyboard users aren't a tiny edge group. Power users navigate with Tab because it's faster. Screen reader users depend on it. People with motor conditions that make a mouse painful rely on it entirely. When the focus ring dies, all of them lose their place on the page at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap :focus For :focus-visible
&lt;/h2&gt;

&lt;p&gt;The single highest-value change I make is replacing &lt;code&gt;:focus&lt;/code&gt; with &lt;code&gt;:focus-visible&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Old focus styling had an annoying tradeoff. If you styled &lt;code&gt;:focus&lt;/code&gt; to be loud enough for keyboard users, mouse users saw a big ring every time they clicked a button, which looked wrong. So people removed it, and both groups lost out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:focus-visible&lt;/code&gt; solves this. The browser decides. Click a button with a mouse, no ring. Tab to it with a keyboard, loud ring. Same CSS, different behavior based on how the element got focus.&lt;/p&gt;

&lt;p&gt;My baseline rule looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;:focus:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&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;The &lt;code&gt;outline-offset&lt;/code&gt; matters more than people expect. A ring pressed flat against the element edge reads as a border, not a focus state. Two pixels of gap makes it obvious the element is selected, and it keeps the ring from clipping on rounded corners.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;outline&lt;/code&gt;, not &lt;code&gt;box-shadow&lt;/code&gt;, for the ring. An &lt;code&gt;outline&lt;/code&gt; doesn't trigger layout shift and it doesn't get clipped by &lt;code&gt;overflow: hidden&lt;/code&gt; on a parent. &lt;code&gt;box-shadow&lt;/code&gt; focus rings vanish inside scroll containers all the time, and you won't catch it until something scrolls.&lt;/p&gt;

&lt;p&gt;There's one trap with &lt;code&gt;outline: none&lt;/code&gt; as a reset. Never, ever apply it globally without a &lt;code&gt;:focus-visible&lt;/code&gt; style to replace it. If you write &lt;code&gt;*:focus { outline: none }&lt;/code&gt; and nothing else, you've deleted keyboard navigation for the entire site in one line.&lt;/p&gt;

&lt;p&gt;I've seen this ship in production more than once.&lt;/p&gt;

&lt;p&gt;Test the swap by unplugging your mouse, or just setting it aside, and pressing Tab. Every interactive element should light up as you move. If one doesn't, it's either not actually focusable (a &lt;code&gt;div&lt;/code&gt; pretending to be a button) or your ring style doesn't apply to it.&lt;/p&gt;

&lt;p&gt;This is the kind of small, boring default I bake into the starting point so I never have to think about it again, same idea as &lt;a href="https://dev.to/blogs/lab/the-starter-template-every-raxxo-tool-begins-from"&gt;The Starter Template Every RAXXO Tool Begins From&lt;/a&gt;. Set it once, inherit it everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contrast The Ring Against Both Themes
&lt;/h2&gt;

&lt;p&gt;A focus ring that exists but can't be seen is the same as no ring. The number I hold it to is a 3:1 contrast ratio between the outline color and whatever's directly behind it.&lt;/p&gt;

&lt;p&gt;Three to one is the threshold for non-text UI elements in the accessibility guidelines. A focus indicator counts. So I check the ring against the element's own background and against the page background, because the offset gap means the ring often sits half on one and half on the other.&lt;/p&gt;

&lt;p&gt;The theme problem is where most of the failures hide. I run the check against every background the element can appear on. Light theme, dark theme, and any colored sections like a hero band or a footer with an inverted palette. One outline color almost never passes against all of them.&lt;/p&gt;

&lt;p&gt;My fix is to tie the ring color to the theme, not hardcode it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--focus-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--focus-ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f5f5f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--focus-ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&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;For elements that sit on unpredictable backgrounds, like a button over a photo or a video, I add a second ring with a contrasting color so at least one half is always visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;#000&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;White ring, black halo. One of them wins no matter what's behind it. I'd only reach for the &lt;code&gt;box-shadow&lt;/code&gt; here, where I actually need the double layer, and I'd accept the clipping risk because the fallback matters more.&lt;/p&gt;

&lt;p&gt;Check the ratio with any contrast tool. Pick the outline color, pick the background, read the number. If it's under 3:1, bump the outline to a thicker 2px or change the color. Don't make the ring thinner to look subtle. Subtle and invisible are the same thing to the person who needs it.&lt;/p&gt;

&lt;p&gt;While you're in there, check that focus works in forced-colors mode too. Windows high contrast mode overrides your colors entirely, and &lt;code&gt;outline&lt;/code&gt; survives that override where &lt;code&gt;box-shadow&lt;/code&gt; sometimes doesn't. That's the other reason I default to outline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tab The Whole Page, Mouse Unplugged
&lt;/h2&gt;

&lt;p&gt;This is the test that catches everything the CSS checks miss. I put the mouse away and Tab through the entire page, start to finish, like I have no other option.&lt;/p&gt;

&lt;p&gt;The order should follow the visual layout. Top to bottom, left to right, the way you'd read. If focus jumps from the header to the footer and back up to the middle, your DOM order doesn't match your visual order, and a keyboard user is being bounced around the page with no map.&lt;/p&gt;

&lt;p&gt;This breaks constantly after a redesign because CSS lets you move things visually without touching the DOM. You use flexbox &lt;code&gt;order&lt;/code&gt; or grid placement to put a sidebar on the left that lives at the bottom of the HTML. Looks right, tabs wrong. Fix the DOM order, not the tab order. Resist &lt;code&gt;tabindex&lt;/code&gt; with positive numbers; it's a patch that makes the next person's job harder.&lt;/p&gt;

&lt;p&gt;Things I watch for on the walk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A visible focus ring on every single stop, no silent jumps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nothing focusable that's hidden. If a mobile menu is closed, its links shouldn't be in the tab order.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No trap. I can Tab out of every modal, dropdown and date picker. If I open something and Tab cycles forever inside with no escape, that's a trap, and Escape should close it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A skip link as the first stop, so keyboard users can jump past the nav to the main content instead of tabbing through forty menu items on every page.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The modal trap is the one that bites after a component swap. A good modal traps focus on purpose while open, then returns focus to the button that opened it when it closes. A bad one traps focus forever, or dumps you back at the top of the page where you've lost your place entirely. Open every overlay, Tab around, hit Escape, confirm focus landed somewhere sensible.&lt;/p&gt;

&lt;p&gt;I also tab through after resizing to a narrow width, because mobile layouts reorder things and hide others. The desktop tab order can be perfect while the mobile one is a mess.&lt;/p&gt;

&lt;p&gt;Do this walk once per major view. It takes minutes and it's the closest thing to actually being a keyboard user for five minutes. If you only run one item on this list, run this one. The naming and structure discipline from &lt;a href="https://dev.to/blogs/lab/the-command-naming-convention-every-raxxo-tool-follows"&gt;The Command Naming Convention Every RAXXO Tool Follows&lt;/a&gt; is the same spirit: predictable order so nobody gets lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Run the four checks in order and you'll catch almost every focus regression a redesign introduces. Swap in &lt;code&gt;:focus-visible&lt;/code&gt;, give the ring 2px of offset, hold it to 3:1 contrast against every background including dark mode, then tab the whole page with the mouse put away.&lt;/p&gt;

&lt;p&gt;If I had to keep just one, it's the tab walk. The CSS can look perfect in the stylesheet and still fail in the actual flow, and the only way to know is to use the page the way a keyboard user does.&lt;/p&gt;

&lt;p&gt;Add this to your pre-ship routine and it becomes muscle memory. I keep mine in the same place I keep the rest of my launch gates, because a check you have to remember is a check you'll skip on the busy day.&lt;/p&gt;

&lt;p&gt;If you want the full picture of how I set defaults like this once and inherit them across everything I build, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the whole approach. Go unplug your mouse and tab your homepage. You'll find at least one dead stop.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Request Validation at the Edge: A Minimal Pattern I Reuse</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 12 Oct 2026 23:44:27 +0000</pubDate>
      <link>https://dev.to/raxxostudios/request-validation-at-the-edge-a-minimal-pattern-i-reuse-2o7l</link>
      <guid>https://dev.to/raxxostudios/request-validation-at-the-edge-a-minimal-pattern-i-reuse-2o7l</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reject bad requests at the edge before they touch real logic or a database&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 40-line validator with zero dependencies handles most of my needs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check shape, types, and size limits first, then business rules later&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Returning 400 early cut wasted function runs on one endpoint by half&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the junk that hits an API never deserved a full response. A malformed body, a missing field, a 4MB payload where I expected 2KB. I catch all of that at the edge now, before a single line of real logic runs, and the whole backend got calmer for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the edge is the right place to say no
&lt;/h2&gt;

&lt;p&gt;The edge is the first machine your request touches after DNS. On most platforms that means a worker or function running close to the user, before the request ever reaches your origin server or database. That position is the whole point.&lt;/p&gt;

&lt;p&gt;If I validate at the origin, the bad request has already crossed the network, woken up a container, and maybe opened a database connection. I'm paying for compute to tell someone they forgot a field. If I validate at the edge, I reject it in a few milliseconds and the origin never hears about it.&lt;/p&gt;

&lt;p&gt;There's a cost angle too.&lt;/p&gt;

&lt;p&gt;Edge runtimes bill differently from full origin invocations, and a request that gets rejected in 3ms barely registers. I had one endpoint where roughly half the incoming traffic was malformed scanner noise and duplicate retries. Moving the shape check to the edge meant those never counted as a real function run.&lt;/p&gt;

&lt;p&gt;The other reason is blast radius. A validation layer sitting in front of everything is a single place to enforce rules. I don't have to remember to check &lt;code&gt;body.email&lt;/code&gt; in six different handlers. I check it once, at the door.&lt;/p&gt;

&lt;p&gt;Edge runtimes are also deliberately limited. No full Node API, tight memory, short execution budgets. That sounds like a constraint, but it pushes me toward validation code that's small and fast, which is exactly what validation should be. If my check needs a database lookup or a heavy library, it probably isn't a shape check, it's business logic, and that belongs further in.&lt;/p&gt;

&lt;p&gt;I keep the rule simple: the edge decides if a request is &lt;em&gt;well-formed&lt;/em&gt;. Whether it's &lt;em&gt;allowed&lt;/em&gt; or &lt;em&gt;correct&lt;/em&gt; in a business sense comes later. The edge answers "is this even a sensible request" and nothing more.&lt;/p&gt;

&lt;p&gt;That one line keeps the validator tiny. It also keeps me from dragging auth tables or inventory checks into a layer that should stay stateless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal validator I actually reuse
&lt;/h2&gt;

&lt;p&gt;I don't reach for a schema library at the edge unless I have to. Most of my endpoints need maybe five checks, and a dependency-light function covers them. Here's the shape of it.&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;function&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rules&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;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &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;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rules&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;field&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is required`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&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;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must be &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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="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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; exceeds &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; chars`&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="nx"&gt;errors&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;That's it. About 40 lines once I add a couple more checks like enum membership and a regex for email. Calling it looks like this:&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;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&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;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&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;errors&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;The order matters. I check &lt;code&gt;required&lt;/code&gt; first and &lt;code&gt;continue&lt;/code&gt; so a missing field doesn't also throw a type error. A user who forgot &lt;code&gt;email&lt;/code&gt; shouldn't get told it's the wrong type too. One clear reason per field.&lt;/p&gt;

&lt;p&gt;I return every error at once, not the first one.&lt;/p&gt;

&lt;p&gt;Nothing's worse than fixing a field, resubmitting, and getting hit with the next complaint. Collect them, send them back together.&lt;/p&gt;

&lt;p&gt;For 90% of endpoints this is enough and it has zero install cost. When an endpoint genuinely needs nested objects or conditional fields, I'll pull in a real schema validator and accept the bundle size. But I reach for the small function first, and most of the time I never outgrow it.&lt;/p&gt;

&lt;p&gt;If you want the deeper context on handling repeated and retried requests, &lt;a href="https://dev.to/blogs/lab/writing-idempotent-webhooks-patterns-that-survive-retries"&gt;Writing Idempotent Webhooks&lt;/a&gt; goes through that side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the cheap things before the expensive things
&lt;/h2&gt;

&lt;p&gt;Validation has an order, and getting it wrong wastes the exact compute you were trying to save.&lt;/p&gt;

&lt;p&gt;The cheapest check is size. Before I parse anything, I look at &lt;code&gt;Content-Length&lt;/code&gt;. If a request claims to be 10MB and my endpoint expects a small JSON body, I reject it with a 413 and never read the stream. Parsing a huge body just to discover it's huge is backwards.&lt;/p&gt;

&lt;p&gt;Next is content type. If the header says &lt;code&gt;text/plain&lt;/code&gt; and I need JSON, that's a 415, decided in one &lt;code&gt;if&lt;/code&gt;. No parse attempt.&lt;/p&gt;

&lt;p&gt;Then I parse.&lt;/p&gt;

&lt;p&gt;I wrap the parse in a try/catch, because a broken JSON body is a client error, not a server crash. A missing closing brace should return 400, not throw a 500 and page me at night.&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;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid JSON&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&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;Only after the body is a real object do I run the field validator from the last section. Shape checks are cheap but they're the last cheap thing, so they go last among the cheap things.&lt;/p&gt;

&lt;p&gt;Everything expensive comes after all of that. Database lookups, auth token verification, rate-limit counters. By the time I'm doing any of that, I already know the request is well-formed. I'm not querying a table to then discover &lt;code&gt;quantity&lt;/code&gt; was a string.&lt;/p&gt;

&lt;p&gt;I learned to respect ordering the hard way on a different problem. When I measured lookup strategies in &lt;a href="https://dev.to/blogs/lab/binary-search-vs-array-includes-on-a-50-000-row-catalog"&gt;Binary Search vs Array.includes()&lt;/a&gt;, the lesson was the same: the order and method of a check decides whether it costs microseconds or milliseconds. Validation is no different. Put the 3ms rejections in front of the 300ms ones.&lt;/p&gt;

&lt;p&gt;One more thing on size.&lt;/p&gt;

&lt;p&gt;Edge runtimes have real memory limits, and a request that tries to stuff a giant string into a field is sometimes less a mistake and more a probe. Cap every string field with a &lt;code&gt;max&lt;/code&gt;. It's a free defense against someone trying to blow up your queue or your memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs here and what doesn't
&lt;/h2&gt;

&lt;p&gt;The edge validator earns its keep by staying small. The fastest way to ruin it is to let business rules creep in.&lt;/p&gt;

&lt;p&gt;Shape, types, and size limits belong at the edge. "Is &lt;code&gt;email&lt;/code&gt; a string under 254 characters" is a question I can answer with no outside information. It's pure, stateless, and fast. That's the test I use: if the check needs nothing but the request itself, it can live at the edge.&lt;/p&gt;

&lt;p&gt;Does this user actually exist? Is this coupon still valid? Is the item in stock? None of those belong here. They need a database, they need state, and they're slower by nature. Dragging them to the edge forces the edge to talk to your origin anyway, which throws away the latency win you came for.&lt;/p&gt;

&lt;p&gt;There's a cleaner way to think about it.&lt;/p&gt;

&lt;p&gt;The edge answers "could this request ever be valid." The origin answers "is this request valid right now, for this user, in this state." The first question is about form. The second is about truth. Keep them separate and both layers stay readable.&lt;/p&gt;

&lt;p&gt;I also keep error messages at the edge generic on purpose. "Invalid request" plus a list of malformed fields is fine. I don't leak which internal rule failed or whether a user exists, because that validator is public-facing and anyone can poke it. Detailed, state-aware errors come from deeper in, where I've already decided the caller is allowed to know.&lt;/p&gt;

&lt;p&gt;Watch out for edge runtime limits while you're at it. Some platforms cap execution time hard, and a validator that accidentally does real work can blow that budget. I once watched a tiny Node process fall over from a different kind of overreach in &lt;a href="https://dev.to/blogs/lab/i-found-the-exact-recursion-depth-that-crashes-node"&gt;I Found the Exact Recursion Depth That Crashes Node&lt;/a&gt;, and the fix in both cases is the same discipline: know your limits and stay well under them.&lt;/p&gt;

&lt;p&gt;The payoff is that the origin code gets simpler. Every handler can assume the body is well-formed, because the only way to reach it is through the door I built. That assumption removes a pile of defensive &lt;code&gt;if&lt;/code&gt; checks from the real logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Start with the 40-line validator and only graduate to a schema library when an endpoint genuinely needs nested or conditional rules. Order your checks cheapest-first: size, content type, parse, then shape, then anything that touches state. Keep business rules out of the edge entirely.&lt;/p&gt;

&lt;p&gt;If you build one shared validator and run it in front of every endpoint, you get a single place to change rules and a backend that can trust its inputs. That trust is worth more than the code it costs.&lt;/p&gt;

&lt;p&gt;I wire this pattern into most of my projects the same way I wire in a few other defaults, and I documented that whole setup in the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; if you want the fuller picture of how I scaffold a new service. Grab the validator, drop it at your edge, and let the junk bounce off the door.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Debounce vs Throttle: I Counted Every Call</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:38:55 +0000</pubDate>
      <link>https://dev.to/raxxostudios/debounce-vs-throttle-i-counted-every-call-f16</link>
      <guid>https://dev.to/raxxostudios/debounce-vs-throttle-i-counted-every-call-f16</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Debounce waits for a pause; 25 fast keystrokes collapsed into exactly 1 call at 300 ms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Throttle keeps a steady beat; 60 scroll events at 60 Hz became 11 calls at 100 ms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debounce alone let a slow, stale search response overwrite the newer one in my test&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pick by the question the UI asks: "when they stop" means debounce, "while it happens" means throttle&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Debounce and throttle both cut the number of times a handler runs, and that's where most explanations stop. I wanted numbers, so I wrote both functions, fired fake keystrokes and scroll events at them in Node, and counted every call that came out the other side.&lt;/p&gt;

&lt;p&gt;The counts are below. So is the bug that neither of them fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each One Actually Does to Your Events
&lt;/h2&gt;

&lt;p&gt;Debounce waits. Every new event resets a timer, and the handler only runs once the events stop for the full wait time. Throttle doesn't wait for a pause. It lets the handler run at most once per window, no matter how many events arrive inside it.&lt;/p&gt;

&lt;p&gt;Here are the two versions I tested. Both are small enough to paste into any project without a dependency.&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;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&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;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;wait&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;function&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;now&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&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;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&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;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&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="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nx"&gt;last&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="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&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;pending&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;remaining&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;The throttle has one detail people often leave out: the trailing call. Without it, the last events inside a window get dropped, and a scroll position or slider value can freeze a few pixels short of where the user actually stopped. With it, the final event always lands. In my test the throttled handler received event 59 of 59, the true last one.&lt;/p&gt;

&lt;p&gt;The opposite edge matters too. My throttle fires on the first event right away (the leading edge), so a drag handle reacts the instant you grab it. My debounce only fires on the trailing edge, after the quiet period. That's right for search and wrong for a button that should respond to the first click, which is why some libraries offer a &lt;code&gt;leading: true&lt;/code&gt; option on debounce as well. If a control feels laggy on the very first touch, check which edge it's firing on before you touch the timing.&lt;/p&gt;

&lt;p&gt;If you already use lodash, its &lt;code&gt;_.throttle&lt;/code&gt; is built on &lt;code&gt;_.debounce&lt;/code&gt; with a &lt;code&gt;maxWait&lt;/code&gt; option, so the two are closer relatives than their names suggest. You don't need lodash for either, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers: 25 Keystrokes and 60 Scroll Events
&lt;/h2&gt;

&lt;p&gt;Test one simulated someone typing "debouncing and throttling", which is 25 characters, at one key every 80 ms. That's a fast but realistic typist.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Debounce at 300 ms: &lt;strong&gt;1 call&lt;/strong&gt;, after the last key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The same typing with one 450 ms pause halfway through: 2 calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Throttle at 300 ms: 8 calls, spread across the whole burst.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test two simulated one second of scrolling at 60 Hz: 60 events, 16 ms apart.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Throttle at 100 ms: 11 calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debounce at 100 ms: 1 call, after the scrolling stopped.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those two tests show the whole difference better than any definition. Debounce turned a burst into one final answer. Throttle turned the same burst into a steady beat of intermediate answers.&lt;/p&gt;

&lt;p&gt;Why 300 ms for the search box? Fast typists leave something like 80 to 200 ms between keys, so any wait above that collapses a word into one call. 300 sits safely above the typing gap and still feels instant once they stop. The 450 ms test shows the other side of that choice: a real pause mid-word counts as "stopped", which is usually what you want, because the user is probably reading the suggestions.&lt;/p&gt;

&lt;p&gt;Neither is better. They answer different questions, and using the wrong one feels broken in a way users notice immediately. A search box on throttle fires requests for "debo", "debounc" and so on, most of them useless. A scroll-linked progress bar on debounce sits frozen for the whole scroll, then jumps once at the end.&lt;/p&gt;

&lt;p&gt;Scroll and input are exactly where this shows up in Core Web Vitals. INP, Interaction to Next Paint, has been a Core Web Vital since March 2024, and 200 ms or less counts as good. A heavy handler running on every keystroke is one of the easiest ways to blow past that. I looked at what moved INP on a real store in &lt;a href="https://dev.to/blogs/lab/six-months-of-shopify-web-vitals-data-what-actually-moved-inp"&gt;Six Months of Shopify Web Vitals Data What Actually Moved INP&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug Neither of Them Fixes
&lt;/h2&gt;

&lt;p&gt;This is the part that bites people who did everything "right".&lt;/p&gt;

&lt;p&gt;Say the search box is debounced at 300 ms. The user types "deb", pauses for a moment, then finishes "debounce". Two debounced calls fire, 320 ms apart. The short query is the slow one, because it matches more results: 450 ms for "deb", 120 ms for "debounce".&lt;/p&gt;

&lt;p&gt;So the responses come back in the wrong order. "debounce" lands first, and then "deb" lands 10 ms later and overwrites it.&lt;/p&gt;

&lt;p&gt;In my simulation the UI ended up showing results for "deb" while the input said "debounce". Debounce did its job perfectly. It cut two dozen keystrokes down to two requests. It just has no opinion on the order those requests come back in.&lt;/p&gt;

&lt;p&gt;The fix is one &lt;code&gt;AbortController&lt;/code&gt; per query:&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;let&lt;/span&gt; &lt;span class="nx"&gt;controller&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;onSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&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;res&lt;/span&gt; &lt;span class="o"&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="s2"&gt;`/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;render&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rerun with the abort in place, the same test showed results for "debounce". The older request is cancelled before its response can land, so the order stops mattering. Two things to keep: swallow only &lt;code&gt;AbortError&lt;/code&gt;, so real network failures still surface, and create a new controller per call, because an aborted controller stays aborted.&lt;/p&gt;

&lt;p&gt;One more leak to close in component code. A debounced call that's still waiting when the component unmounts will fire anyway and try to update something that no longer exists. Give the debounce a cancel and call it in your cleanup:&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;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&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;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&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;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&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="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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;d&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;In React that's &lt;code&gt;useEffect(() =&amp;gt; () =&amp;gt; onSearch.cancel(), [])&lt;/code&gt;, plus aborting the controller in the same cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Each One Belongs
&lt;/h2&gt;

&lt;p&gt;This is the map I'd use. It's organised by the question the interface is asking, because that question decides it faster than any rule about event types.&lt;/p&gt;

&lt;p&gt;"Tell me when they stop" is debounce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Search-as-you-type and autocomplete, at 250 to 300 ms, always with the abort above.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Form validation that calls a server, like "is this username free".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Autosave, with one addition. A pure debounce never saves while someone types without pausing, so add a &lt;code&gt;maxWait&lt;/code&gt; (lodash has it) and save at least every few seconds anyway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expensive work after a window resize. For layout that reacts to one element's size, &lt;code&gt;ResizeObserver&lt;/code&gt; is usually the better tool.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Keep me updated while it happens" is throttle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scroll position for sticky headers, progress bars or analytics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Drag handles, sliders and anything following the pointer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate-limiting your own API calls from a chatty UI.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything that paints, reach for &lt;code&gt;requestAnimationFrame&lt;/code&gt; before either. It runs once per frame, which is exactly the rate the screen can show, and it pauses in background tabs for free.&lt;/p&gt;

&lt;p&gt;And one case where neither belongs: the double-submitted form. Throttling the click handler looks like a fix and isn't. A slow network or a second tab still gets two orders through. Disable the button on the first click, and make the server treat a repeated request as the same request. That's the server's half of the job, and I covered the storage side of it in &lt;a href="https://dev.to/blogs/lab/rate-limiting-without-redis-3-patterns-i-use-in-serverless"&gt;Rate Limiting Without Redis: 3 Patterns I Use in Serverless&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Testing all of this doesn't need real waiting. Vitest and Jest both ship fake timers: switch them on, fire your events, advance the clock by the wait time and assert on the call count. That's the same test I ran in Node, minus the real 80 ms sleeps, so it runs in milliseconds and never flakes on a slow CI machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you remember one test from this, make it the search box. Debounce it at 300 ms, abort the previous request every time, and you've fixed the two problems that make search feel broken: the flood of requests and the stale result.&lt;/p&gt;

&lt;p&gt;Everything else falls out of the question the UI is asking. "When they stop" means debounce. "While it happens" means throttle, or &lt;code&gt;requestAnimationFrame&lt;/code&gt; if it's drawing something. And if the real problem is a duplicate action, neither timer fixes it, the button and the server do.&lt;/p&gt;

&lt;p&gt;The two functions above are dependency-free and tested. Copy them. If you'd rather read more of these build notes by topic, the &lt;a href="https://dev.to/pages/lab-overview"&gt;Lab overview&lt;/a&gt; groups every development article in one place.&lt;/p&gt;

&lt;p&gt;Which one did you last reach for out of habit when the other one fit better?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Weekly Operating Rhythm That Keeps a Solo Studio Shipping</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:38:19 +0000</pubDate>
      <link>https://dev.to/raxxostudios/the-weekly-operating-rhythm-that-keeps-a-solo-studio-shipping-5g2m</link>
      <guid>https://dev.to/raxxostudios/the-weekly-operating-rhythm-that-keeps-a-solo-studio-shipping-5g2m</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Monday is planning only, no building, protects the week's focus&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tuesday through Thursday ship one thing each, nothing parallel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Friday handles publishing, scheduling, and the 30-minute metrics review&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I refuse to schedule meetings, standups, and anything open-ended&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running a studio alone means nobody tells you what to do on Tuesday. That sounds like freedom until you realize the absence of a manager is also the absence of structure. The thing that keeps me shipping isn't motivation. It's a small set of rituals I run on the same days every week.&lt;/p&gt;

&lt;p&gt;Here's what I actually do, day by day, and the stuff I deliberately keep off the calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday Is for Deciding, Not Doing
&lt;/h2&gt;

&lt;p&gt;Monday is planning only. I don't write code, I don't touch a prompt, I don't open the image generator. I sit with a list and decide what the week ships.&lt;/p&gt;

&lt;p&gt;The reason is simple. When I let Monday blur into building, I'd start the week reacting to whatever was open in my editor on Friday. That's how you end up three weeks into a feature nobody needs. So Monday is a hard stop on execution.&lt;/p&gt;

&lt;p&gt;The planning itself is short. I pick three things, max. One build, one content piece, one operational fix. If a fourth thing feels urgent, it goes on a list for next Monday. Almost always it turns out it wasn't urgent at all.&lt;/p&gt;

&lt;p&gt;I also use Monday to look at what's actually in front of users. If a tool has a rough edge, that beats any new feature I had planned. I wrote about the discipline of finishing the unglamorous parts in &lt;a href="https://dev.to/blogs/lab/the-settings-page-every-raxxo-tool-gets-before-launch"&gt;The Settings Page Every RAXXO Tool Gets Before Launch&lt;/a&gt;, and most Monday decisions boil down to that same instinct: polish what exists before chasing what doesn't.&lt;/p&gt;

&lt;p&gt;The hardest part of Monday is resisting the dopamine of starting. Building feels productive. Planning feels like procrastination dressed up. But a bad week usually traces back to a Monday where I skipped the decision and just dove in.&lt;/p&gt;

&lt;p&gt;I keep the plan written in plain text, one file, three lines. No project management tool. No board with columns. The overhead of maintaining a fancy system would cost me more than the system saves. For a one-person studio, the plan fits on a sticky note, and anything bigger is a tax I refuse to pay.&lt;/p&gt;

&lt;p&gt;By Monday afternoon I know exactly what Tuesday, Wednesday, and Thursday look like.&lt;/p&gt;

&lt;p&gt;That's the whole point. The decisions are made when I'm fresh and calm, not at 9am Wednesday when I'm staring at a blank editor wondering what matters.&lt;/p&gt;

&lt;p&gt;One rule I never break: the Monday plan can shrink but it can't grow. If something breaks, I cut, I don't add.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuesday to Thursday: One Thing Per Day
&lt;/h2&gt;

&lt;p&gt;The middle of the week is build time, and the rule is one thing per day. Not one project. One thing.&lt;/p&gt;

&lt;p&gt;I used to work on three tools at once, switching when I got stuck. It felt efficient. It wasn't. Every switch cost me 20 minutes of reloading context into my head, and by Friday I'd have three half-finished things and nothing to ship. Now I pick one item from Monday's plan and stay on it until it's done or until the day ends.&lt;/p&gt;

&lt;p&gt;Context switching is the silent killer for solo work. There's no teammate holding the other threads in their head. If I drop a task halfway, the entire mental model goes with it, and rebuilding it tomorrow costs real hours.&lt;/p&gt;

&lt;p&gt;A typical Tuesday: I'll take one tool, define the smallest useful version, and ship it. I lean hard on Claude for the first pass of anything, which is why I keep a tight setup around it. If you want the full picture of how I structure that work, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the whole approach.&lt;/p&gt;

&lt;p&gt;Wednesday is usually the hard one. It's the day I tackle whatever I was quietly avoiding on Monday. The thing with the ambiguous requirements or the dependency I don't trust. Putting the ugliest task in the middle of the week means I hit it with energy still in the tank, not on a Friday when I'm fading.&lt;/p&gt;

&lt;p&gt;I'm deliberately conservative about what I add to a tool. The fewer moving parts, the fewer things break at 2am. I went deep on that philosophy in &lt;a href="https://dev.to/blogs/lab/the-one-dependency-i-refuse-to-add-to-any-raxxo-tool"&gt;The One Dependency I Refuse to Add to Any RAXXO Tool&lt;/a&gt;, and it shapes most of my Tuesday-to-Thursday decisions.&lt;/p&gt;

&lt;p&gt;The output target is modest and that's intentional. One shipped thing a day beats five started things a week. Over a month that's roughly 12 shipped items, which for one person is more than enough to keep a catalog moving.&lt;/p&gt;

&lt;p&gt;When I finish early, I don't start the next day's task.&lt;/p&gt;

&lt;p&gt;I stop. Pulling tomorrow's work into today just means tomorrow starts empty and I lose the rhythm. The constraint is the feature, not a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friday Is Publishing and Review
&lt;/h2&gt;

&lt;p&gt;Friday is when the week becomes public. All the content, scheduling, and the one review that actually matters happen here.&lt;/p&gt;

&lt;p&gt;I write during the week but I never publish same-day. Everything queues up for Friday. That gap of a day or two catches the typos and the half-baked arguments that look fine when you're close to them and embarrassing by Friday morning.&lt;/p&gt;

&lt;p&gt;Social scheduling runs through &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt;, which lets me load a week of posts in one sitting instead of context-switching into social mode every single day. Thirty minutes on Friday replaces what used to be seven small daily interruptions. That's the trade I want: batch the shallow work, protect the deep days.&lt;/p&gt;

&lt;p&gt;Then comes the review, and I keep it to 30 minutes with a timer. I look at what shipped, what didn't, and why. The "why" is the valuable part. If something slipped, I want to know if it was a bad estimate or a bad priority, because those get fixed in completely different ways.&lt;/p&gt;

&lt;p&gt;I look at a small number of signals: which tools got used, which posts got read, and whatever broke that week. I don't build dashboards and I don't track vanity metrics. A number I won't act on is a number I shouldn't be looking at.&lt;/p&gt;

&lt;p&gt;Friday is also when I decide what to kill. A tool that nobody touches after a fair shot gets archived without ceremony. The sunk-cost pull is strong when you built the thing yourself, so having a fixed day to make the call removes the emotion. It's just the Friday review, not a referendum on my judgment.&lt;/p&gt;

&lt;p&gt;Keeping up with the tools I depend on happens here too. The pace of change from Anthropic alone means I'd fall behind fast if I didn't check in weekly. When something like &lt;a href="https://dev.to/blogs/lab/claude-opus-5-5-ships-cheaper-with-four-breaking-changes"&gt;Claude Opus 5.5 Ships Cheaper, With Four Breaking Changes&lt;/a&gt; lands, Friday is when I read it, decide if it affects my setup, and schedule any migration for a future Tuesday.&lt;/p&gt;

&lt;p&gt;By Friday evening the week is closed. Published, reviewed, decided. Nothing dangling into the weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Refuse to Schedule
&lt;/h2&gt;

&lt;p&gt;The rhythm works because of what's missing from it as much as what's in it.&lt;/p&gt;

&lt;p&gt;I don't schedule meetings. There's no standup, no sync, no recurring call. As one person, a meeting with myself is just thinking with extra steps, and a meeting with anyone else is usually a decision that could've been an email. I protect that empty space on purpose.&lt;/p&gt;

&lt;p&gt;I don't schedule open-ended research. "Explore the new thing" is how a full day vanishes with nothing to show. If I want to learn something, it gets a specific question and a time cap. One hour, one question, write down the answer. No wandering.&lt;/p&gt;

&lt;p&gt;I don't schedule work on weekends.&lt;/p&gt;

&lt;p&gt;This one took me a while to respect. The temptation to grab a few quiet hours on Saturday is constant. But every weekend I worked made the following Monday worse, because the week never actually ended. The rest is part of the system, not a reward for finishing it.&lt;/p&gt;

&lt;p&gt;I don't schedule reactive work into the deep days. If a message or a new feature announcement lands on Wednesday, it waits for Friday review or next Monday's plan. The only exception is something actively broken for users, and even then I fix the break and go back to the plan. Nothing else gets to jump the queue.&lt;/p&gt;

&lt;p&gt;I also don't schedule perfection passes. A thing ships when it's useful, not when it's flawless. I used to lose entire afternoons polishing details nobody would notice. Now the Monday plan sets the bar and I ship to that bar, no higher.&lt;/p&gt;

&lt;p&gt;The staying-current reading has a cap too. The AI space moves fast enough that you could spend every hour just keeping up. Pieces like &lt;a href="https://dev.to/blogs/lab/anthropic-launches-claude-marketplace-with-2-000-connectors"&gt;Anthropic Launches Claude Marketplace With 2,000+ Connectors&lt;/a&gt; matter, but they go in the Friday slot, not into Wednesday's build time. Reading about tools isn't the same as shipping with them, and it's easy to confuse the two when you're alone and nobody's measuring your output.&lt;/p&gt;

&lt;p&gt;The rhythm isn't about doing more. It's about doing one clear thing per day and letting the rest wait its turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Try the one-thing-per-day rule for a single week before you adopt anything else here. It's the piece that does the most work. Pick one item each morning, finish it, and refuse to start the next until tomorrow. You'll ship more by Friday than you expect, and you'll feel less scattered doing it.&lt;/p&gt;

&lt;p&gt;The planning and review rituals matter, but they're scaffolding around that core habit. If Monday-only planning feels too rigid, loosen it. The point isn't my exact schedule. The point is that a solo operator needs structure imposed from somewhere, and if no boss provides it, you build it yourself out of small repeatable rituals.&lt;/p&gt;

&lt;p&gt;If you want to see how I keep the actual building fast enough to fit inside this rhythm, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; covers the setup I run every day. Start with one ritual, keep it for a month, and add the next only when the first one sticks.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
