<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/rss"/>
    <language>en</language>
    <item>
      <title>I built a live secret scanner for VS Code (and why CI scanning is too late)</title>
      <dc:creator>thunderbird</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:35:30 +0000</pubDate>
      <link>https://dev.to/thund3rbird/i-built-a-live-secret-scanner-for-vs-code-and-why-ci-scanning-is-too-late-46f5</link>
      <guid>https://dev.to/thund3rbird/i-built-a-live-secret-scanner-for-vs-code-and-why-ci-scanning-is-too-late-46f5</guid>
      <description>&lt;p&gt;We've all done it: pasted an API key into a file "just to test," then a week later&lt;br&gt;
it's in your git history, a screenshot, or a livestream. Most secret scanners —&lt;br&gt;
gitleaks, trufflehog — run in CI, &lt;strong&gt;after&lt;/strong&gt; the secret is already committed.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Secret Guardian&lt;/strong&gt;, a VS Code extension that catches secrets &lt;em&gt;live, in&lt;br&gt;
the editor&lt;/em&gt;, the moment they appear — and visually &lt;strong&gt;masks&lt;/strong&gt; them so they never show&lt;br&gt;
up in screenshots or screen-shares.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=thund3rbird.secret-guardian" rel="noopener noreferrer"&gt;Secret Guardian on the VS Code Marketplace&lt;/a&gt;&lt;/strong&gt; (free)&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Detects 17+ secret types as you type: AWS, GitHub, GitLab, Google, Slack, Stripe,
OpenAI, SendGrid, Twilio, npm tokens, private keys, JWTs, credentials in URLs.&lt;/li&gt;
&lt;li&gt;A generic &lt;strong&gt;high-entropy&lt;/strong&gt; rule catches the long-tail.&lt;/li&gt;
&lt;li&gt;Masks detected secrets with a lock overlay — safe for demos.&lt;/li&gt;
&lt;li&gt;Flags everything in the Problems panel + a one-click workspace scan.&lt;/li&gt;
&lt;li&gt;100% local. Nothing leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How detection works
&lt;/h3&gt;

&lt;p&gt;Two layers: tight &lt;strong&gt;regexes&lt;/strong&gt; for known formats (e.g. &lt;code&gt;AKIA…&lt;/code&gt;, &lt;code&gt;ghp_…&lt;/code&gt;), and an&lt;br&gt;
&lt;strong&gt;entropy + context&lt;/strong&gt; check for unknown secrets (high Shannon entropy assigned to a&lt;br&gt;
secret-like name), with placeholders like &lt;code&gt;your_api_key&lt;/code&gt; filtered out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;Install from the &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=thund3rbird.secret-guardian" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt;&lt;/strong&gt;,&lt;br&gt;
open a file, paste a fake key. I'd love feedback on accuracy and false positives.&lt;/p&gt;

</description>
      <category>security</category>
      <category>showdev</category>
      <category>tooling</category>
      <category>vscode</category>
    </item>
    <item>
      <title>I built 168 pages of free trading tools with vanilla HTML/JS — here's how</title>
      <dc:creator>Mobin Moharami</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:35:12 +0000</pubDate>
      <link>https://dev.to/mbinmhr/i-built-168-pages-of-free-trading-tools-with-vanilla-htmljs-heres-how-4n0d</link>
      <guid>https://dev.to/mbinmhr/i-built-168-pages-of-free-trading-tools-with-vanilla-htmljs-heres-how-4n0d</guid>
      <description>&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Prop firm traders (people who trade with other people's money) need specific calculators that account for strict drawdown rules. Every existing calculator is either ugly, full of ads, or doesn't know that FTMO has a 5% daily drawdown limit.&lt;/p&gt;

&lt;p&gt;So I built PropWise — 10 interactive tools across 168 SEO-optimized pages, all in vanilla HTML, CSS, and JS. No framework. No build step. No dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;10 core tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Position Size Calculator (pre-configured for 12 prop firms)&lt;/li&gt;
&lt;li&gt;Challenge Cost Calculator (expected cost to get funded based on pass rate)&lt;/li&gt;
&lt;li&gt;Risk of Ruin Calculator (probability of blowing your account)&lt;/li&gt;
&lt;li&gt;Revenge Trade Calculator (mathematical damage of emotional trading)&lt;/li&gt;
&lt;li&gt;Overtrading Detector (optimal trading frequency analysis)&lt;/li&gt;
&lt;li&gt;Live Session Timer with ICT kill zones&lt;/li&gt;
&lt;li&gt;Compound Growth Calculator&lt;/li&gt;
&lt;li&gt;Pip Value Calculator&lt;/li&gt;
&lt;li&gt;Prop Firm Comparison (12 firms)&lt;/li&gt;
&lt;li&gt;"Am I Ready?" readiness quiz&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Plus 158 programmatic pages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;55 firm + account size specific calculators (e.g. "FTMO 100K Position Size Calculator")&lt;/li&gt;
&lt;li&gt;66 head-to-head comparison pages (e.g. "FTMO vs FundedNext")&lt;/li&gt;
&lt;li&gt;36 brand-specific tool pages (e.g. "Topstep Risk Calculator")&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Intentionally simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vanilla HTML/CSS/JS&lt;/li&gt;
&lt;li&gt;Python build script to generate 168 pages from templates&lt;/li&gt;
&lt;li&gt;Nginx on a VPS&lt;/li&gt;
&lt;li&gt;Certbot for SSL&lt;/li&gt;
&lt;li&gt;Google Analytics&lt;/li&gt;
&lt;li&gt;No database, no API, no framework&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The programmatic SEO approach
&lt;/h2&gt;

&lt;p&gt;Instead of 10 pages targeting broad keywords like "position size calculator," I generated pages for every combination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every firm × every account size = unique page with pre-calculated lot sizes&lt;/li&gt;
&lt;li&gt;Every firm × every firm = comparison page with side-by-side data&lt;/li&gt;
&lt;li&gt;Every firm × every tool type = brand-specific landing page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique title, description, and keywords&lt;/li&gt;
&lt;li&gt;Schema.org markup (SoftwareApplication, BreadcrumbList, FAQPage)&lt;/li&gt;
&lt;li&gt;Breadcrumb navigation&lt;/li&gt;
&lt;li&gt;Heavy internal linking (5+ contextual links per page)&lt;/li&gt;
&lt;li&gt;Pre-calculated data tables (not just a blank calculator)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One Python script generates everything:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
for firm in FIRMS:
    for account in firm["accounts"]:
        # Generate unique page with pre-filled calculations
        build_page(
            slug=f"{firm['slug']}-{format(account)}-position-size-calculator",
            title=f"{firm['name']} {format(account)} Position Size Calculator",
            body=generate_calculator_page(firm, account)
        )




/// Design decisions
Dark theme — traders stare at dark charts all day. A bright white tool site would feel jarring.
No signup wall — every tool works immediately. No email capture, no "sign up to see results." Trust is built by being useful, not by gating.
Share cards — every calculation produces a shareable card with the result, optimized for Twitter screenshots. Organic distribution &amp;gt; paid ads.
Sound effects — subtle audio feedback: a cha-ching for positive results, an alert for dangerous risk levels. Small detail, big UX impact.
What I'd do differently
Would use a static site generator instead of a custom Python script
Would add a service worker for offline use
Would build a proper design system instead of inline styles
Results
Just launched. 168 pages indexed in Google, zero budget spent. Let's see what happens.
Check it out: propwise.site
Source approach and all tools are free. If you're a trader, I'd love feedback on what's missing.
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>seo</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Superconductor review: the cleanest way I've found to run AI agents in parallel</title>
      <dc:creator>Daniel Bergholz</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:33:02 +0000</pubDate>
      <link>https://dev.to/danielbergholz/superconductor-review-the-cleanest-way-ive-found-to-run-ai-agents-in-parallel-1c87</link>
      <guid>https://dev.to/danielbergholz/superconductor-review-the-cleanest-way-ive-found-to-run-ai-agents-in-parallel-1c87</guid>
      <description>&lt;p&gt;I use Claude Code every single day. At my day job, it's my main agent, and I've talked plenty about how much I love it. But on my side projects, I like to play around with other agents: Codex, OpenCode, whatever new CLI shows up that week. And recently I hit a wall that turned out to be more annoying than I expected.&lt;/p&gt;

&lt;p&gt;I wanted to run a bunch of agents in parallel. Not one at a time, but several, across different projects, at the same time. And the moment I tried to do that, I realized every tool was trying to lock me into its own little world.&lt;/p&gt;

&lt;p&gt;So I went looking for something better, and I found a product I've been using for the past few days that I genuinely can't stop recommending. This is &lt;strong&gt;not&lt;/strong&gt; sponsored. I'm paying nothing and using it on my own machine. It's called &lt;a href="https://super.engineering/" rel="noopener noreferrer"&gt;Superconductor&lt;/a&gt;, and this post is my honest review.&lt;/p&gt;

&lt;p&gt;If you'd rather watch me click through the whole thing instead of reading, the full walkthrough is right here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/T5G_aUIEwEk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I needed this in the first place
&lt;/h2&gt;

&lt;p&gt;Here's the situation. If I'm on Claude, I can open the Claude desktop app and run multiple Claudes in parallel. Cool. I can do the same with OpenAI's Codex app. Also cool.&lt;/p&gt;

&lt;p&gt;But what if I want a different agent? What if I want to run OpenCode, or the Pi coding agent? Suddenly I'm stuck. Each desktop app only knows about its own provider. The second I commit to one app, I'm locked into whatever model that company ships.&lt;/p&gt;

&lt;p&gt;I don't want that. I want one interface that doesn't care which agent I'm using. One place where I can run Claude Code for my day job and Codex for my side projects, side by side, without juggling three separate apps. &lt;strong&gt;Agent-agnostic&lt;/strong&gt; is the whole point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qu5amub6gpg909tljmz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qu5amub6gpg909tljmz.gif" alt="This is the way" width="480" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's exactly what Superconductor is: a single workspace for running coding agents in parallel, across different projects and different worktrees, with whatever agent you feel like using.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Superconductor actually is
&lt;/h2&gt;

&lt;p&gt;A few things make it stand out before we even open it.&lt;/p&gt;

&lt;p&gt;First, it's written &lt;strong&gt;100% in Rust&lt;/strong&gt;. No Electron. The whole thing is a native macOS binary rendered on your GPU with Metal, and the startup time is around &lt;strong&gt;50 milliseconds&lt;/strong&gt;. That number sounds like marketing until you feel it. Some of the provider apps, like the Codex app, are genuinely nice but also pretty janky. Every time you open a sidebar, it slides in this sluggish way. Superconductor just snaps. Everything is instant.&lt;/p&gt;

&lt;p&gt;Second, because it's built for developers, it's absolutely covered in keyboard shortcuts. You can drive the entire app without touching the mouse. Here are the ones I lean on the most:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + Shift + E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle the left side panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Toggle the right side panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + Shift + A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add a new project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open your default agent (Claude Code, for me)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + Shift + T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pick from all available agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close the current tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new worktree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cmd + R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run your run script (dev server, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Option + Cmd + Left&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switch between workspaces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let me walk through how I actually use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layout
&lt;/h2&gt;

&lt;p&gt;On the left, you have your projects. Open a project folder and you see all of its worktrees. Right now I just have the main branch for two projects: my personal website and CourseShelf v2. That starred branch row is the &lt;strong&gt;primary worktree&lt;/strong&gt;, which is just the original checkout Superconductor opened, the source of truth you branch off from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqof9ozzvdosdua1hpd1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqof9ozzvdosdua1hpd1v.png" alt="Default view" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the right, you have a panel you can fully customize. By default it shows your files, changes, and checks on top, and a terminal on the bottom (where you can also wire up a run script and a setup script, more on that in a second).&lt;/p&gt;

&lt;p&gt;I don't love that default arrangement, so I switched it to show two tabs on the right instead: one for my terminal with setup and run, and another for my files. It takes ten seconds and the layout sticks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo3c3s80yb8krwo6eell.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo3c3s80yb8krwo6eell.png" alt="My custom view" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding a new project is just &lt;code&gt;Cmd + Shift + A&lt;/code&gt; (or the "Add Project" button on the bottom left), pick your folder, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting an agent
&lt;/h2&gt;

&lt;p&gt;This is where the "just like a browser" mental model clicks. Press &lt;code&gt;Cmd + T&lt;/code&gt; to open your default agent, which in my case is Claude Code. Or press &lt;code&gt;Cmd + Shift + T&lt;/code&gt; to pick from every agent you've got configured. Superconductor supports a long list out of the box: Claude Code, Codex, Gemini CLI, OpenCode, Cursor Agent, Grok, Pi, Kiro, Copilot, basically any CLI agent you already use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0v3ikwpp3r6yru4bpjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0v3ikwpp3r6yru4bpjv.png" alt="Agent picker" width="564" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a native chat UI too, where you can switch providers and models from a dropdown. It's nice. But honestly, I almost always stick to the CLIs. So I'll close the chat with &lt;code&gt;Cmd + W&lt;/code&gt;, hit &lt;code&gt;Cmd + Shift + T&lt;/code&gt; again, choose "terminal," and just run the CLI straight from there. The point is, Superconductor doesn't force a workflow on you. CLI, native chat, plain terminal, it's your call.&lt;/p&gt;

&lt;p&gt;One thing worth knowing: it never routes your prompts or code through its own servers. You bring your own provider CLIs and your own subscriptions, and everything stays local. No account to create, no credentials proxied anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running real parallel work
&lt;/h2&gt;

&lt;p&gt;Let me actually test the parallel part. I'll open a tab for OpenCode and pick a model. Do they have Kimi? Nope, not yet. Fine, I'll grab DeepSeek V4 Pro on high reasoning and ask it: "what is this project about?" Enter.&lt;/p&gt;

&lt;p&gt;Now I switch tabs to another project and start a second agent. I'd normally reach for Claude Fable here, but it got banned right when I was recording this, so I switched to Claude Opus on X-high effort instead. Same question: "what is this project about?"&lt;/p&gt;

&lt;p&gt;And that's it. Two agents, two different providers, two different projects, both working at the same time. I flip between tabs and watch OpenCode finish while Opus is still thinking. This already solves my original problem: multiple agents, multiple projects, zero provider lock-in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyrzpr1tbilxxzswxjvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyrzpr1tbilxxzswxjvf.png" alt="Two providers" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But running across separate projects is the easy case. The real question is: what about parallel work &lt;strong&gt;inside the same project&lt;/strong&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  Worktrees: parallel work inside one project
&lt;/h2&gt;

&lt;p&gt;This is where worktrees come in, and where Superconductor gets genuinely clever.&lt;/p&gt;

&lt;p&gt;To create a new worktree, press &lt;code&gt;Cmd + N&lt;/code&gt; (or the plus button). Under the hood you're still using Git, but a worktree copies your codebase into a brand new folder so each task gets its own isolated working directory. Concurrent agents never step on each other's files. That's the whole appeal.&lt;/p&gt;

&lt;p&gt;But copying your code into a fresh folder creates two very real headaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables.&lt;/strong&gt; Your &lt;code&gt;.env&lt;/code&gt; is gitignored, so a fresh worktree doesn't have it. Your new folder is missing the one file your app can't run without.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies.&lt;/strong&gt; No &lt;code&gt;node_modules&lt;/code&gt; either. So... do I have to manually run &lt;code&gt;npm install&lt;/code&gt; every single time I spin up a worktree?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If that's the deal, this feature is dead on arrival. Nobody wants to babysit &lt;code&gt;npm install&lt;/code&gt; and copy &lt;code&gt;.env&lt;/code&gt; files by hand all day.&lt;/p&gt;

&lt;p&gt;Worry not, my friend. This is exactly the problem the scripts solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The magic: setup and run scripts
&lt;/h2&gt;

&lt;p&gt;Open your project settings (you can do this per project on the left sidebar), scroll down, and you'll find the &lt;strong&gt;scripts&lt;/strong&gt; section. This is where everything gets automated.&lt;/p&gt;

&lt;p&gt;There are a few hooks. The two I care about are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup script&lt;/strong&gt; runs automatically whenever a new worktree is created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run script&lt;/strong&gt; runs when you hit the run button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my personal website, my setup script does two things: copy my environment variables from the main folder into the new worktree, then run &lt;code&gt;npm install&lt;/code&gt;. My run script is simply &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can configure these in the app's project settings, or commit them to your repo so the whole team shares them. Superconductor reads a &lt;code&gt;.superconductor/config.json&lt;/code&gt; from the worktree (falling back to the primary worktree if there isn't one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.superconductor/config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"setup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cp ../main/.env .env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm install"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm run dev"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"teardown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rm -rf tmp/superconductor-preview"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;setup&lt;/code&gt; array is what runs on a fresh worktree, &lt;code&gt;run&lt;/code&gt; is what the run button executes, and &lt;code&gt;teardown&lt;/code&gt; cleans up when you delete a worktree. (My actual setup uses my own paths for the env copy, so treat the &lt;code&gt;cp&lt;/code&gt; line above as the shape of it, not my literal command.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now watch what happens. I press &lt;code&gt;Cmd + N&lt;/code&gt; on my Next.js personal website. The setup script kicks off, and I can see its output right there in the panel: &lt;code&gt;added 49 packages&lt;/code&gt;. That's the classic &lt;code&gt;npm install&lt;/code&gt; output. The worktree is ready to go.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyol94hgv8f5r12wmrplp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyol94hgv8f5r12wmrplp.png" alt="Creating new worktree" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jump to the files and check it out. First, &lt;code&gt;node_modules&lt;/code&gt; is there, so dependencies are installed. Second, my &lt;code&gt;.env&lt;/code&gt; is sitting right there too. I didn't run a single &lt;code&gt;cp&lt;/code&gt; or &lt;code&gt;npm install&lt;/code&gt; by hand. It's all automated. Create another worktree with &lt;code&gt;Cmd + N&lt;/code&gt; and the same thing happens again: dependencies, env vars, ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0mvttzb3hg7bkuo2tpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0mvttzb3hg7bkuo2tpd.png" alt="Files" width="688" height="1012"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in browser
&lt;/h2&gt;

&lt;p&gt;Remember that run script? To run it, press &lt;code&gt;Cmd + R&lt;/code&gt;. My dev server boots up. You can open it in a regular browser like normal, or you can use Superconductor's &lt;strong&gt;built-in browser&lt;/strong&gt;: click "Open Preview" and a browser pane opens right inside the app.&lt;/p&gt;

&lt;p&gt;It's got some nice touches: a button to copy the URL, a button to open the site in your external browser, and a button to inspect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faot8ju46hgup36z6ssmy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faot8ju46hgup36z6ssmy.png" alt="Built-in browser" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now picture the full parallel setup. I start CourseShelf, press &lt;code&gt;Cmd + R&lt;/code&gt;, open its preview. I do the same for my personal website. Two dev servers, two previews, both live at once. And on the left sidebar, any worktree with a running server gets a glowing blue dot, so at a glance I know exactly what's up without hunting through tabs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07d6jbl20q9et52v40bj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07d6jbl20q9et52v40bj.png" alt="Running server" width="512" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the part that sold me. Parallel work across different projects, &lt;strong&gt;and&lt;/strong&gt; parallel work inside the same project, each task fully isolated with its own deps, env, server, and preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workspaces and themes
&lt;/h2&gt;

&lt;p&gt;The last feature, and honestly one of my favorites, is workspaces.&lt;/p&gt;

&lt;p&gt;At the bottom left I keep one workspace for work and another for side projects. Want a new one? Create it (I made a "work number two" just to show it off). It feels a lot like Arc's spaces. Each workspace can even have its own theme. I gave one a deep, intense red. You set a chrome and accent color per workspace, so personal, work, and client contexts stay visually distinct and you always know where you are.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5krzsyjl6wgu86e9vk30.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5krzsyjl6wgu86e9vk30.png" alt="Theme picker" width="508" height="1260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To jump between workspaces, press &lt;code&gt;Option + Cmd + Left&lt;/code&gt;. And like everything else in this app, it's instant. The animations are flawless, with zero lag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6x69d5o5pqlu7gp1w0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6x69d5o5pqlu7gp1w0q.png" alt="New workspace" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm not using (yet)
&lt;/h2&gt;

&lt;p&gt;Time for the honest part, because a review that's all praise is useless.&lt;/p&gt;

&lt;p&gt;Superconductor has a bunch of heavier features I'm not touching. It can automatically review your changes, open pull requests for you, and there's a smart action button that walks through commit, push, PR creation, fixing CI, resolving conflicts, and merging, all without leaving the app. It tracks PR status per worktree too. It's clearly built for people who want to run a whole review-and-ship loop inside one window.&lt;/p&gt;

&lt;p&gt;Me? I'm using the simple stuff: create projects, run agents in parallel, spin up worktrees with automated setup, run my dev servers, and switch between workspaces. That's all I need. But if you're a hardcore user, there's a lot of room to go deep.&lt;/p&gt;

&lt;p&gt;A couple of caveats to set expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's still in &lt;strong&gt;alpha&lt;/strong&gt;. Things will change.&lt;/li&gt;
&lt;li&gt;I went looking for a pricing page and couldn't find one. As far as I can tell it's &lt;strong&gt;free right now&lt;/strong&gt;, and I'd assume paid features show up eventually.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://super.engineering/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; is genuinely extensive if you want to dig into the advanced flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should you try it?
&lt;/h2&gt;

&lt;p&gt;Yes. If you run more than one agent at a time, or you bounce between providers like I do, this is the cleanest setup I've found. It's fast, it's keyboard-driven, it's agent-agnostic, and the worktree automation alone saves me from a hundred tiny &lt;code&gt;npm install&lt;/code&gt; papercuts a week. The fact that it's free and this polished while still in alpha is wild. Give it a shot and tell me what you think.&lt;/p&gt;

&lt;p&gt;Thanks for reading all the way to the end, you're awesome. See you in the next one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen4ls4vpepbcofv2dd3e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fen4ls4vpepbcofv2dd3e.gif" alt="That's all folks" width="499" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>superconductor</category>
      <category>claude</category>
      <category>openai</category>
    </item>
    <item>
      <title>CtroEnv vs Zod, envalid, t3-env, Which Env Validator Should You Use?</title>
      <dc:creator>Odejobi Abiola Samuel </dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:25:31 +0000</pubDate>
      <link>https://dev.to/ctrotech/ctroenv-vs-zod-envalid-t3-env-which-env-validator-should-you-use-4aln</link>
      <guid>https://dev.to/ctrotech/ctroenv-vs-zod-envalid-t3-env-which-env-validator-should-you-use-4aln</guid>
      <description>&lt;p&gt;I've built enough projects to know that environment variable validation isn't glamorous — until your app silently uses &lt;code&gt;undefined&lt;/code&gt; as a database URL. There are a bunch of tools for this now, and picking the wrong one means either fighting your tool or fighting your own config. Here's how they stack up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The candidates
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Dependencies&lt;/th&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;th&gt;Framework support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CtroEnv&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;under 5 KB gzip&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Node, Vite, Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zod + manual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50 KB+&lt;/td&gt;
&lt;td&gt;0 (Zod itself is ~13 KB gzip, plus your glue)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;envalid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 KB gzip&lt;/td&gt;
&lt;td&gt;6 (legacy deps)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Node only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;t3-env&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~15 KB&lt;/td&gt;
&lt;td&gt;Depends on Zod&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Next.js only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's look at each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zod + manual parsing
&lt;/h2&gt;

&lt;p&gt;Zod is great for runtime validation in general, and lots of people use it for env vars because they already have it in their project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65535&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; If you're already using Zod, this is zero extra dependencies. Zod's type inference is excellent. The error formatting is detailed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't do:&lt;/strong&gt; No CLI, no &lt;code&gt;.env.example&lt;/code&gt; generation, no secret masking, no framework adapters. You write your own &lt;code&gt;process.exit(1)&lt;/code&gt; boilerplate every time. Coercion for numbers/booleans is manual — &lt;code&gt;z.coerce.number()&lt;/code&gt; doesn't reject non-numeric strings gracefully. No way to generate docs from your schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to pick it:&lt;/strong&gt; You already have Zod in your project, you only have 3-4 env vars, and you don't mind writing the glue code. For anything bigger, the boilerplate gets annoying fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  envalid
&lt;/h2&gt;

&lt;p&gt;envalid was the early leader here. It's been around since the days of &lt;code&gt;dotenv&lt;/code&gt; and has a simple API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cleanEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;makeValidator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;envalid&lt;/span&gt;&lt;span class="dl"&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cleanEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&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;&lt;strong&gt;What it does well:&lt;/strong&gt; Dead simple. The &lt;code&gt;makeValidator&lt;/code&gt; API for custom types is straightforward. Reports all errors at once instead of failing on the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't do:&lt;/strong&gt; It's Node-only, so no Vite or Next.js integrations. The error messages are pretty basic — you get a string like &lt;code&gt;"DATABASE_URL" is missing&lt;/code&gt;, but no grouped or colored output. The &lt;code&gt;choices&lt;/code&gt; option is oddly attached to &lt;code&gt;str()&lt;/code&gt; rather than being a proper &lt;code&gt;pick&lt;/code&gt; type. It has 6 legacy dependencies (some unmaintained). No CLI, no env file generation, no ENVIRONMENT.md generation. Secret masking isn't built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to pick it:&lt;/strong&gt; You need something simple for a Node backend and don't want to learn a new API. It works, but you'll outgrow it quickly if your project grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  t3-env
&lt;/h2&gt;

&lt;p&gt;Created by the t3-stack team, this one wraps Zod with nice DX and is tightly coupled to Next.js.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createEnv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@t3-oss/env-nextjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;runtimeEnv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; The server/client split is genius for Next.js. It prevents accidentally leaking server-only vars to the client bundle. Great integration with Next.js build process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't do:&lt;/strong&gt; It's Next-only. If you extract it for other frameworks, you're fighting it. It depends on Zod (15 KB+ added). No CLI, no &lt;code&gt;.env.example&lt;/code&gt; generation, no docs generation. The &lt;code&gt;runtimeEnv&lt;/code&gt; option is annoying — you have to manually pass &lt;code&gt;process.env&lt;/code&gt; in development and inline values in production. Secret masking isn't built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to pick it:&lt;/strong&gt; You're all-in on the Next.js / t3-stack ecosystem. If you ever leave Next, you'll need to replace it.&lt;/p&gt;




&lt;h2&gt;
  
  
  CtroEnv
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I built this one. I wanted something that worked everywhere without dragging in a schema library I didn't need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineEnv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@ctroenv/core&lt;/span&gt;&lt;span class="dl"&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;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// typed as string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero dependencies. under 5 KB gzipped. Validators are chainable and self-contained — no Zod dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Secret masking is built in&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;string&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="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// "********"&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// actual value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CLI is included&lt;/span&gt;
npx @ctroenv/cli validate        &lt;span class="c"&gt;# validate current env&lt;/span&gt;
npx @ctroenv/cli generate        &lt;span class="c"&gt;# create .env.example from schema&lt;/span&gt;
npx @ctroenv/cli docs            &lt;span class="c"&gt;# generate ENVIRONMENT.md&lt;/span&gt;
npx @ctroenv/cli check           &lt;span class="c"&gt;# CI-friendly diff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Framework adapters&lt;/span&gt;
&lt;span class="c1"&gt;// @ctroenv/node — reads process.env + .env files&lt;/span&gt;
&lt;span class="c1"&gt;// @ctroenv/vite — reads import.meta.env, build plugin&lt;/span&gt;
&lt;span class="c1"&gt;// @ctroenv/nextjs — server/client split like t3-env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does well:&lt;/strong&gt; Framework-agnostic core, but has adapters when you need them. CLI for everyday tasks. Generates &lt;code&gt;.env.example&lt;/code&gt; and docs from schema so they can't drift. Secret masking is built into the runtime, not bolted on. Error grouping and colored output for CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't do:&lt;/strong&gt; It's newer, so smaller ecosystem. No Zod compatibility layer (and that's by design — the APIs are different). The custom validator API uses a different pattern than Zod's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to pick it:&lt;/strong&gt; You want zero-dependency validation that works across frameworks. You hate maintaining &lt;code&gt;.env.example&lt;/code&gt; by hand. You want one tool for dev, CI, and docs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;p&gt;If you're on Next.js and already have Zod, t3-env is fine. If you just need 3 vars validated, envalid works. If you love Zod and want to own your parsing pipeline, go for it.&lt;/p&gt;

&lt;p&gt;If you want something that handles the whole lifecycle — validation, docs generation, CI checks, secret masking — CtroEnv does that without pulling in a 50 KB schema library. But it's also the newest option, so weigh that.&lt;/p&gt;

&lt;p&gt;Pick based on your project, not hype. Each of these tools solves the same problem differently, and none of them are wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ctrotech-tutor/ctroenv" rel="noopener noreferrer"&gt;CtroEnv on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@ctroenv/core" rel="noopener noreferrer"&gt;CtroEnv on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ctroenv.vercel.app" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to make an AI research agent label facts vs inferences — a deterministic provenance pipeline</title>
      <dc:creator>John</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:23:16 +0000</pubDate>
      <link>https://dev.to/hexisteme/how-to-make-an-ai-research-agent-label-facts-vs-inferences-a-deterministic-provenance-pipeline-5dfn</link>
      <guid>https://dev.to/hexisteme/how-to-make-an-ai-research-agent-label-facts-vs-inferences-a-deterministic-provenance-pipeline-5dfn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://hexisteme.github.io/notes/fact-vs-inference-provenance-ai-agent.html" rel="noopener noreferrer"&gt;hexisteme notes&lt;/a&gt;, part of a series on building and running an AI agent fleet.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To stop an AI research or RAG agent from presenting its own inferences as retrieved facts, &lt;strong&gt;split the work so the LLM never decides what is a fact&lt;/strong&gt;: let the LLM only extract and summarize, and let a deterministic, non-LLM pipeline do all scoring, cross-checking, and labeling. Tag a claim &lt;code&gt;FACT&lt;/code&gt; only when a rule is satisfied — corroboration by ≥2 independent sources, or one official API — and downgrade everything else to &lt;code&gt;INFERENCE&lt;/code&gt;. Because labeling is rule-based, the agent can't launder a guess into a fact, and the same query produces the same labels every run.&lt;/p&gt;

&lt;p&gt;An AI agent that gathers information has two kinds of output tangled together: things it &lt;em&gt;retrieved&lt;/em&gt; and things it &lt;em&gt;concluded&lt;/em&gt;. A web page said the market was 1.2 trillion won (retrieved); the agent inferred the market is "growing fast" (concluded). Both come out in the same confident prose. For anything you'll act on, that blend is the problem — you can't tell which sentence is grounded and which is the model filling a gap.&lt;/p&gt;

&lt;p&gt;The fix isn't a better prompt ("only state facts you can cite"). Prompts are probabilistic; under pressure the model reverts. The fix is structural: &lt;strong&gt;take the fact/inference decision away from the model entirely&lt;/strong&gt; and put it in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split: LLM extracts, code judges
&lt;/h2&gt;

&lt;p&gt;Draw a hard line through the pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The LLM does&lt;/th&gt;
&lt;th&gt;Deterministic code does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extract claims from a fetched page; summarize a passage&lt;/td&gt;
&lt;td&gt;Score, cross-check, sort, deduplicate, label FACT/INFERENCE, decide freshness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The LLM is excellent at reading messy text and pulling out a structured claim. It is unreliable at &lt;em&gt;judging&lt;/em&gt; that claim — ask it to "rate confidence 0–1" and it will turn a guess into &lt;code&gt;0.85&lt;/code&gt;, and give a different number next run. So nothing downstream of extraction is allowed to be an LLM call. Scores are token matches, source counts, and recency math. Labels are rule outputs. This buys two things at once: &lt;strong&gt;reproducibility&lt;/strong&gt; (same query → same labels, which you can unit-test) and &lt;strong&gt;no laundering&lt;/strong&gt; (the model can't promote its own inference to a fact, because it never holds the pen on labeling).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reproducibility is the tell.&lt;/strong&gt; If your research agent gives different confidence on the same question across runs, an LLM is scoring somewhere in the pipeline. Find it and replace it with a function. The goal is: re-run the exact query, get the exact same FACT/INFERENCE split.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A six-phase pipeline
&lt;/h2&gt;

&lt;p&gt;Make the stages explicit so each is testable in isolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PLAN → HARVEST → NORMALIZE → CORROBORATE → SCORE → RENDER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PLAN&lt;/strong&gt; — turn the question into concrete sub-queries and the sources to try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HARVEST&lt;/strong&gt; — fetch from multiple paths (see below). LLM-free; just collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NORMALIZE&lt;/strong&gt; — LLM extracts structured claims from each fetched item. This is the only place the model touches the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORROBORATE&lt;/strong&gt; — group claims; count independent sources per claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCORE&lt;/strong&gt; — assign labels and scores by rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RENDER&lt;/strong&gt; — emit FACTs, INFERENCEs, and an explicit gap list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The FACT gate: earn the label
&lt;/h2&gt;

&lt;p&gt;FACT is not a default; it's a status a claim must earn, enforced as a type invariant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A claim constructed as FACT without evidence is a bug, not a soft warning.
&lt;/span&gt;&lt;span class="nc"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provenance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FACT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; raises
&lt;/span&gt;
&lt;span class="c1"&gt;# The corroboration rule (the knob is the count; the principle is independence)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;independent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_independent_sources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# distinct domains, not pages
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;independent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_official_api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FACT&lt;/span&gt;          &lt;span class="c1"&gt;# carries the evidence_ids that corroborated it
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;INFERENCE&lt;/span&gt;         &lt;span class="c1"&gt;# single-source or model-derived
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Independent" is doing real work: one blog quoting another blog is &lt;em&gt;one&lt;/em&gt; source, not two. Two different domains, or a single authoritative API (a government dataset, an exchange's own endpoint), clear the bar. Everything else is rendered as INFERENCE — visible to the reader as exactly that.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Watch for order-dependence.&lt;/strong&gt; An early version of this scored a cross-corroborated FACT &lt;em&gt;lower&lt;/em&gt; than a single-source INFERENCE because the score depended on processing order. That silently breaks reproducibility. Scores must be a pure function of the claim and its evidence, independent of the order claims were processed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Multi-path harvest, without redundancy
&lt;/h2&gt;

&lt;p&gt;Diversity of sources is what makes corroboration meaningful, but firing every source at once is wasteful and noisy. Use escalation, not broadcast: try a primary search, and only escalate to the next path when the first is insufficient.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web search&lt;/td&gt;
&lt;td&gt;primary → escalate to a news-grade engine (ad/spam pollution) → escalate to a semantic engine (papers, near-duplicates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official API&lt;/td&gt;
&lt;td&gt;a government/first-party dataset; one official source may stand alone as FACT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Never send the same query to three engines simultaneously — read the first result, then decide whether to escalate. And when a source fails or is rate-limited, &lt;strong&gt;log the failure and the escalation&lt;/strong&gt;; never substitute a guess for a missing fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness and gaps are first-class
&lt;/h2&gt;

&lt;p&gt;Two more rules complete the provenance picture. &lt;strong&gt;Freshness&lt;/strong&gt;: every datum carries a confirmation date, and a rule marks it &lt;code&gt;stale&lt;/code&gt; when it ages past a threshold — a fact true last quarter is labeled as such, not silently presented as current. &lt;strong&gt;Gaps&lt;/strong&gt;: the render step emits an explicit list of what was asked but not found or not corroborated. A silent gap reads as completeness and is the most dangerous output a research agent can produce; surfacing it is what makes the FACT list trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth the structure
&lt;/h2&gt;

&lt;p&gt;The payoff is a research output a reader (or a downstream AI) can trust per-claim: every FACT points at the independent sources that earned it, every INFERENCE is flagged as the agent's own leap, stale data says so, and the gaps are named. The model still does what it's good at — reading and extracting — but it never gets to decide what's true. In an era where AI answers are increasingly cited as sources themselves, the agents worth citing are the ones that label their own confidence honestly, by rule, and reproducibly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;More notes on building an AI agent fleet — &lt;a href="https://hexisteme.github.io/notes/falsifier-driven-ai.html" rel="noopener noreferrer"&gt;falsifier-driven AI decisions&lt;/a&gt;, &lt;a href="https://hexisteme.github.io/notes/rdu-reusable-decision-units.html" rel="noopener noreferrer"&gt;reusable decision units&lt;/a&gt;, &lt;a href="https://hexisteme.github.io/notes/file-based-agent-work-bus.html" rel="noopener noreferrer"&gt;a file-based agent work-bus&lt;/a&gt; — at &lt;a href="https://hexisteme.github.io/notes/" rel="noopener noreferrer"&gt;hexisteme.github.io/notes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>how do you get real-world experience before your first job?</title>
      <dc:creator>arianna</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:19:39 +0000</pubDate>
      <link>https://dev.to/arianna/how-do-you-get-real-world-experience-before-your-first-job-59eg</link>
      <guid>https://dev.to/arianna/how-do-you-get-real-world-experience-before-your-first-job-59eg</guid>
      <description>&lt;p&gt;hello ! im a CS student (2nd year) and ive been look around for some tech jobs. any kind because honestly i just want a tech related job (i &amp;lt;3 tech). but the problem is,so many jobs/places want you to have experience in the real-world but literally no one is willing to offer it. they want minimum 3-years experience. im considered learning some other programming languages that they require for jobs that could be useful. however, i want to practice what ive learnt in a real job setting. of course after learning a lot about said-subject. but im just confused on how im going to land a job after college with no experience.&lt;/p&gt;

&lt;p&gt;and please dont come for me; im genuinely just trying to think ahead instead of leaving it for when i get there&lt;/p&gt;

&lt;p&gt;any advice would be appreciated !! :p&lt;/p&gt;

</description>
      <category>programming</category>
      <category>career</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Running Async Python Inside Celery Is Harder Than You Think.</title>
      <dc:creator>Kolade Fajimi</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:17:50 +0000</pubDate>
      <link>https://dev.to/akoladefaj/running-async-python-inside-celery-is-harder-than-you-think-2046</link>
      <guid>https://dev.to/akoladefaj/running-async-python-inside-celery-is-harder-than-you-think-2046</guid>
      <description>&lt;p&gt;The problem is straightforward to state and surprisingly hard to solve correctly.&lt;/p&gt;

&lt;p&gt;Celery workers are synchronous. Celery spawns prefork worker processes, and when a task arrives, it calls your task function like this: &lt;code&gt;task_function(*args, **kwargs)&lt;/code&gt;. It expects a return value. It blocks the worker thread until it gets one. It does not know or care that you wrote &lt;code&gt;async def&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But modern Python services are async. FastAPI is async. SQLAlchemy 2.0 is async. httpx, aiohttp, asyncpg the entire interesting half of the ecosystem has gone async-first. The idea of maintaining two parallel code paths, one async for your web layer, one sync for your task layer is exactly the kind of thing that creates maintenance debt, copy-paste bugs, and the kind of divergence you only notice when something breaks in production.&lt;/p&gt;

&lt;p&gt;So you want to write &lt;code&gt;async def&lt;/code&gt; task functions and have them work inside a Celery worker. How hard can it be?&lt;/p&gt;

&lt;p&gt;Harder than it looks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;asyncio.run()&lt;/code&gt; doesn't work
&lt;/h3&gt;

&lt;p&gt;The first thing most people try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;task_wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;your_async_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in isolation. It fails in production for a specific reason: &lt;code&gt;asyncio.run()&lt;/code&gt; creates a new event loop, runs the coroutine to completion, then closes the loop. If there is already a running event loop on the current thread, and there frequently is, in test environments, in newer Celery versions, in signal handlers, it raises:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RuntimeError: This event loop is already running&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix most people find next is &lt;code&gt;nest_asyncio&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;nest_asyncio&lt;/span&gt;
&lt;span class="n"&gt;nest_asyncio&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="c1"&gt;# now asyncio.run() "works" from inside a running loop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nest_asyncio&lt;/code&gt; patches the event loop to allow re-entrant calls. It works in simple cases. The subtle failure mode: re-entrant event loops change the execution order of scheduled callbacks and coroutines. Code that was safe under normal scheduling assumptions becomes non-deterministic under concurrent load. Bugs appear only at production concurrency, only under specific timing, and are nearly impossible to reproduce in development.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prefork complication
&lt;/h3&gt;

&lt;p&gt;Even if you solve the &lt;code&gt;asyncio.run()&lt;/code&gt; problem, Celery's prefork concurrency model introduces a second failure that takes longer to diagnose because it manifests as infinite silence rather than an immediate error.&lt;/p&gt;

&lt;p&gt;When Celery starts, it forks N worker processes from a single parent. After &lt;code&gt;fork()&lt;/code&gt;, the child process inherits the parent's memory including any event loop objects that existed before the fork.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;fork()&lt;/code&gt; does not copy threads. A Python &lt;code&gt;asyncio.AbstractEventLoop&lt;/code&gt; is driven by a thread calling &lt;code&gt;loop.run_forever()&lt;/code&gt;. After &lt;code&gt;fork()&lt;/code&gt;, the child has the loop object but not the thread running it. The loop's internal state may indicate it was running; nothing is actually driving it. Any coroutine scheduled onto this loop hangs indefinitely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@worker_process_init.connect&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# This loop was inherited from the parent.
&lt;/span&gt;    &lt;span class="c1"&gt;# The thread driving it died when the parent forked.
&lt;/span&gt;    &lt;span class="c1"&gt;# loop.is_running() → False.
&lt;/span&gt;    &lt;span class="c1"&gt;# Scheduling coroutines onto it produces no results and no errors.
&lt;/span&gt;    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;some_coro&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# blocks forever
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of bug that produces a zero-width failure window. The loop object exists and looks valid. No exception is raised. Work just never completes. I spent the better part of a day convinced the issue was in the Redis client before realizing the loop scheduled to drive it had died at fork time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The solution: a persistent bridge loop per worker process
&lt;/h3&gt;

&lt;p&gt;The correct approach is to create a brand-new event loop inside each forked worker process and start a dedicated daemon thread to drive it. The bridge loop is the only asyncio runtime in the worker process. All async work runs on it. Celery's synchronous worker threads never touch an event loop directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;worker_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbstractEventLoop&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@worker_process_init.connect&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;init_worker_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;worker_loop&lt;/span&gt;

    &lt;span class="c1"&gt;# Always create a fresh loop in the forked child.
&lt;/span&gt;    &lt;span class="c1"&gt;# Never reuse the inherited parent loop object.
&lt;/span&gt;    &lt;span class="n"&gt;worker_loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# A daemon thread drives the loop independently of Celery's
&lt;/span&gt;    &lt;span class="c1"&gt;# synchronous execution threads.
&lt;/span&gt;    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_run_event_loop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_loop&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now the bridge is &lt;code&gt;asyncio.run_coroutine_threadsafe&lt;/code&gt;. When Celery calls the synchronous task wrapper, the wrapper schedules the async orchestration coroutine onto the background loop and blocks the worker thread waiting for the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_orchestrate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Schema migration, idempotency check, Phoenix heartbeat,
&lt;/span&gt;        &lt;span class="c1"&gt;# OTel span setup, task execution, fence validation, DLQ quarantine.
&lt;/span&gt;        &lt;span class="n"&gt;result&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;your_async_task_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

    &lt;span class="c1"&gt;# Schedule the coroutine from this synchronous thread onto
&lt;/span&gt;    &lt;span class="c1"&gt;# the event loop running on the background thread.
&lt;/span&gt;    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_orchestrate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;worker_loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Block the Celery worker thread here. All actual work happens
&lt;/span&gt;    &lt;span class="c1"&gt;# on the bridge loop thread.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&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;&lt;code&gt;run_coroutine_threadsafe&lt;/code&gt; is the correct API for this pattern. It is thread-safe, it returns a &lt;code&gt;concurrent.futures.Future&lt;/code&gt; (not an asyncio Future), and &lt;code&gt;future.result()&lt;/code&gt; blocks without touching the event loop. The background loop thread does all the async I/O. The Celery worker thread just waits.&lt;/p&gt;

&lt;p&gt;This solves both problems cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;asyncio.run()&lt;/code&gt; from inside a running loop. The loop lives on a different thread.&lt;/li&gt;
&lt;li&gt;No inherited-but-dead loop. Each worker creates its own after fork.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;push/apush&lt;/code&gt; split
&lt;/h3&gt;

&lt;p&gt;Dispatching tasks has its own version of this problem. Celery's send_task is synchronous and blocking, it opens a broker connection and writes a message. If you call it from inside an async FastAPI route handler, you block the event loop during a network round-trip.&lt;/p&gt;

&lt;p&gt;This is why Relier has two dispatch methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# From async code (FastAPI, async Django):
&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;send_invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# From sync code (Flask routes, sync Django views, scripts):
&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;send_invoice&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="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apush&lt;/code&gt; runs the blocking broker send in an executor so the async caller is never blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Admission check, schema wrapping, OTel context injection...
&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_in_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
            &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_id&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;&lt;code&gt;push&lt;/code&gt; explicitly guards against being called from inside a running loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&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="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# No running loop on this thread. Safe.
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.push() was called from inside a running event loop, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;where it would block and deadlock that loop. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use `await &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.apush(...)` instead.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Inside a Celery worker: reuse the bridge loop.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;worker_loop&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;worker_loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_running&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;worker_loop&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Outside Celery (Flask route, script):
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message in the &lt;code&gt;RuntimeError&lt;/code&gt; matters. When someone calls &lt;code&gt;push()&lt;/code&gt; from a FastAPI route handler, they get an actionable message telling them exactly what to do instead. Not a silent deadlock. Not a timeout with no context. A specific message at the exact moment the mistake is made.&lt;/p&gt;

&lt;p&gt;The check itself, &lt;code&gt;asyncio.get_running_loop()&lt;/code&gt; in a &lt;code&gt;try/except&lt;/code&gt; &lt;code&gt;RuntimeError&lt;/code&gt; is the canonical way to detect whether the current thread is running an event loop. It raises &lt;code&gt;RuntimeError&lt;/code&gt; if no loop is running on this thread, which is the safe case for &lt;code&gt;push()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sync tasks in an async world
&lt;/h3&gt;

&lt;p&gt;What about existing sync task functions? A codebase of &lt;code&gt;def tasks&lt;/code&gt; shouldn't require a full rewrite to benefit from Relier's reliability stack.&lt;/p&gt;

&lt;p&gt;Inside the orchestration coroutine, execution branches on whether the function is async:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iscoroutinefunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&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;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;actual_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;actual_kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;actual_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;actual_kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;asyncio.to_thread&lt;/code&gt; runs the sync function in Python's default thread pool executor. The orchestration layer awaits it without blocking the bridge loop. All the async infrastructure, heartbeat refreshes, Phoenix registration, OTel span updates, fence validation keeps running concurrently on the bridge loop while the sync function runs on a thread pool thread.&lt;/p&gt;

&lt;p&gt;The constraint is honest: two-tier timeouts (&lt;code&gt;soft_timeout&lt;/code&gt;, &lt;code&gt;hard_timeout&lt;/code&gt;) only work for &lt;code&gt;async def&lt;/code&gt; tasks. A sync function running in &lt;code&gt;asyncio.to_thread&lt;/code&gt; cannot be cooperatively cancelled from outside. Relier raises &lt;code&gt;ValueError&lt;/code&gt; at decoration time if you pass timeout parameters to a sync task, rather than silently providing no protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@rl_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soft_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hard_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ValueError at import time
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Fix: convert to async def, or remove the timeout parameters.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failing loudly at decoration time is better than failing silently at runtime when the timeout fires and nothing happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout enforcement without thread kills
&lt;/h3&gt;

&lt;p&gt;Two-tier timeouts deserve their own explanation because they interact with the bridge loop in a non-obvious way.&lt;/p&gt;

&lt;p&gt;When a task starts, Relier spawns two watcher coroutines as asyncio tasks alongside the actual work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;task_coro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_soft_timeout_handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soft&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;task_coro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Fire the recovery hook. Task keeps running.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;on_soft&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;on_soft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_hard_timeout_handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hard&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;task_coro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;task_coro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Delivers CancelledError at next await point.
&lt;/span&gt;
&lt;span class="n"&gt;soft_watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_soft_timeout_handler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;hard_watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_hard_timeout_handler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hard_watcher&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;return_when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_COMPLETED&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;All three coroutines run concurrently on the bridge loop. The soft timeout fires and calls your recovery hook, where you can call &lt;code&gt;ctx.set_partial(state)&lt;/code&gt; to checkpoint work in progress while the task keeps running. If the task doesn't finish before the hard deadline, &lt;code&gt;task_coro.cancel()&lt;/code&gt; delivers &lt;code&gt;asyncio.CancelledError&lt;/code&gt; at the task's next await point.&lt;/p&gt;

&lt;p&gt;No thread kills. No SIGALRM. No OS-level signals. Pure cooperative asyncio cancellation. This matters for cleanup: &lt;code&gt;CancelledError&lt;/code&gt; propagates through finally blocks. Resources get released. Partial state gets checkpointed. The task gets quarantined to the DLQ with its full payload and resurrection history. None of that happens with a hard OS kill.&lt;/p&gt;

&lt;h3&gt;
  
  
  The disposable loop case
&lt;/h3&gt;

&lt;p&gt;One edge case worth knowing: outside a Celery worker in a CLI script, a management command, a test, there's no bridge loop. The task wrapper's loop resolution falls through to creating a fresh event loop just for that call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_worker_loop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Check for persistent worker bridge.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;relier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_loop&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;relier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker_loop&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Check for a running loop on this thread (test contexts).
&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="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Create a disposable loop for this one call.
&lt;/span&gt;    &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disposable loops are cleaned up after the call: Redis connections are closed, the loop is stopped and closed, and &lt;code&gt;asyncio.set_event_loop(None)&lt;/code&gt; clears the thread-local reference. The persistent &lt;code&gt;worker_loop&lt;/code&gt; is specifically excluded from this cleanup path closing the bridge loop mid-execution would kill all in-flight tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;p&gt;The prefork problem is the kind of failure that shows up as "nothing happens" rather than an exception. You schedule coroutines, they don't run, no error surfaces. It took a day of debugging the wrong thing before I isolated it to the inherited-but-dead loop. The fix (create a fresh loop in &lt;code&gt;worker_process_init&lt;/code&gt;) is obvious in retrospect. Getting there required understanding exactly what &lt;code&gt;fork()&lt;/code&gt; does to threads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;asyncio.run_coroutine_threadsafe&lt;/code&gt; is underused. Most Python developers never need to cross a thread boundary into a running event loop, so the API is obscure. But for anything that marries a sync framework (Celery, Django ORM, WSGI in general) with async internals, it is the correct and safe way to do it. It appears in the Python docs in a single paragraph. It deserves more.&lt;/p&gt;

&lt;p&gt;The two-method dispatch split (push/apush) is the right API surface even though it introduces surface area. The alternative, a single method that auto-detects the context and does the right thing sounds better but produces confusing failures when the auto-detection is wrong. The explicit split makes the contract clear. Async code always uses &lt;code&gt;apush&lt;/code&gt;. Sync code always uses &lt;code&gt;push&lt;/code&gt;. The guard in &lt;code&gt;push()&lt;/code&gt; exists so that misuse produces a useful error immediately rather than a deadlock ten seconds later.&lt;/p&gt;

&lt;p&gt;Cooperative timeout cancellation is better than OS-level signals for tasks that care about cleanup. The finally block guarantee is the part that matters: partial state can be persisted, connections can be closed, the DLQ entry gets written with everything needed to re-inspect or re-dispatch. An OS kill gives you none of that.&lt;/p&gt;

&lt;p&gt;The whole bridge, bridge loop thread, run_coroutine_threadsafe, push/apush split, disposable loop cleanup is about 200 lines in &lt;code&gt;app.py&lt;/code&gt; and &lt;code&gt;decorator.py&lt;/code&gt; combined. The complexity is real but contained. Once the pattern is in place, every &lt;code&gt;async def&lt;/code&gt; task function just works, without the task author knowing anything about the event loop infrastructure underneath.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/getrelier/relier" rel="noopener noreferrer"&gt;github.com/getrelier/relier&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://getrelier.github.io/relier" rel="noopener noreferrer"&gt;getrelier.github.io/relier&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pip install relier&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Building a Design System Without Recreating CSS</title>
      <dc:creator>Drew Marshall</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:17:44 +0000</pubDate>
      <link>https://dev.to/stinklewinks/building-a-design-system-without-recreating-css-1a41</link>
      <guid>https://dev.to/stinklewinks/building-a-design-system-without-recreating-css-1a41</guid>
      <description>&lt;p&gt;One of the easiest traps to fall into when building a design system is accidentally recreating CSS.&lt;/p&gt;

&lt;p&gt;At first, it seems harmless.&lt;/p&gt;

&lt;p&gt;You add a few utility classes.&lt;/p&gt;

&lt;p&gt;Then a few responsive options.&lt;/p&gt;

&lt;p&gt;Then a few layout helpers.&lt;/p&gt;

&lt;p&gt;Then a few exceptions.&lt;/p&gt;

&lt;p&gt;Before long, you've built an entirely new syntax for doing the exact same thing CSS already does.&lt;/p&gt;

&lt;p&gt;I've become increasingly interested in a different approach.&lt;/p&gt;

&lt;p&gt;Not replacing CSS.&lt;/p&gt;

&lt;p&gt;Not hiding CSS.&lt;/p&gt;

&lt;p&gt;Creating a layer above CSS that focuses on intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With More Options
&lt;/h2&gt;

&lt;p&gt;Developers love flexibility.&lt;/p&gt;

&lt;p&gt;I do too.&lt;/p&gt;

&lt;p&gt;The challenge is that flexibility often comes with complexity.&lt;/p&gt;

&lt;p&gt;Consider responsive layouts.&lt;/p&gt;

&lt;p&gt;Many systems solve this by exposing every possible breakpoint and layout combination.&lt;/p&gt;

&lt;p&gt;The result is powerful.&lt;/p&gt;

&lt;p&gt;But it's also easy to end up with markup that spends more time describing implementation details than communicating purpose.&lt;/p&gt;

&lt;p&gt;The system becomes harder to understand.&lt;/p&gt;

&lt;p&gt;Not because it's incapable.&lt;/p&gt;

&lt;p&gt;Because it's trying to describe everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intent Versus Implementation
&lt;/h2&gt;

&lt;p&gt;Lately I've been thinking less about how a layout is built and more about what the layout is trying to accomplish.&lt;/p&gt;

&lt;p&gt;For example:&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;div&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt; &lt;span class="na"&gt;adapt=&lt;/span&gt;&lt;span class="s"&gt;"grid"&lt;/span&gt; &lt;span class="na"&gt;mobile=&lt;/span&gt;&lt;span class="s"&gt;"stack"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't trying to replace CSS.&lt;/p&gt;

&lt;p&gt;It's trying to express intent.&lt;/p&gt;

&lt;p&gt;The content should adapt as a grid.&lt;/p&gt;

&lt;p&gt;On mobile, it should stack.&lt;/p&gt;

&lt;p&gt;The implementation details can remain inside the design system.&lt;/p&gt;

&lt;p&gt;The goal isn't more abstraction.&lt;/p&gt;

&lt;p&gt;The goal is better communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strong Defaults Create Simplicity
&lt;/h2&gt;

&lt;p&gt;One lesson I've learned from building software is that good defaults are often more valuable than endless configuration.&lt;/p&gt;

&lt;p&gt;Most layouts follow common patterns.&lt;/p&gt;

&lt;p&gt;Most responsive behavior follows common patterns.&lt;/p&gt;

&lt;p&gt;Most developers aren't trying to create entirely new layout systems.&lt;/p&gt;

&lt;p&gt;They're trying to organize content.&lt;/p&gt;

&lt;p&gt;A design system can help by providing thoughtful defaults rather than exposing every possible option.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Danger Of Recreating CSS
&lt;/h2&gt;

&lt;p&gt;The moment a design system attempts to expose every CSS capability, it starts competing with CSS itself.&lt;/p&gt;

&lt;p&gt;That's usually a losing battle.&lt;/p&gt;

&lt;p&gt;CSS already exists.&lt;/p&gt;

&lt;p&gt;CSS is already powerful.&lt;/p&gt;

&lt;p&gt;CSS is already standardized.&lt;/p&gt;

&lt;p&gt;The job of a design system isn't to replace CSS.&lt;/p&gt;

&lt;p&gt;The job of a design system is to reduce cognitive load.&lt;/p&gt;

&lt;p&gt;To create consistency.&lt;/p&gt;

&lt;p&gt;To communicate intent.&lt;/p&gt;

&lt;p&gt;To make common patterns easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing For Readability
&lt;/h2&gt;

&lt;p&gt;One question I increasingly ask when designing APIs is:&lt;/p&gt;

&lt;p&gt;"Can I understand this six months from now?"&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;"Can I configure everything?"&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;"Can I support every edge case?"&lt;/p&gt;

&lt;p&gt;Simply:&lt;/p&gt;

&lt;p&gt;"Can I read this and understand what it is trying to do?"&lt;/p&gt;

&lt;p&gt;Readability scales.&lt;/p&gt;

&lt;p&gt;Complexity doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal Isn't Less Power
&lt;/h2&gt;

&lt;p&gt;Sometimes simplicity gets mistaken for limitation.&lt;/p&gt;

&lt;p&gt;I don't think that's true.&lt;/p&gt;

&lt;p&gt;The goal isn't to remove power.&lt;/p&gt;

&lt;p&gt;The goal is to place power where it belongs.&lt;/p&gt;

&lt;p&gt;Common workflows should be simple.&lt;/p&gt;

&lt;p&gt;Advanced workflows should remain possible.&lt;/p&gt;

&lt;p&gt;The design system should help developers move quickly without preventing them from solving unique problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I've become increasingly convinced that the best design systems don't try to replace CSS.&lt;/p&gt;

&lt;p&gt;They try to create a shared language around common design decisions.&lt;/p&gt;

&lt;p&gt;A language focused on intent rather than implementation.&lt;/p&gt;

&lt;p&gt;A language that helps developers communicate purpose.&lt;/p&gt;

&lt;p&gt;Because at the end of the day, most developers aren't trying to build layouts.&lt;/p&gt;

&lt;p&gt;They're trying to build products.&lt;/p&gt;

&lt;p&gt;The layout is simply one part of the system.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why Daily Transaction Volume is the Wrong Number to Quote in System Design Interviews</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:14:14 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/why-daily-transaction-volume-is-the-wrong-number-to-quote-in-system-design-interviews-53jd</link>
      <guid>https://dev.to/aliasgarmk/why-daily-transaction-volume-is-the-wrong-number-to-quote-in-system-design-interviews-53jd</guid>
      <description>&lt;p&gt;The Mistake Most Candidates Make&lt;br&gt;
Picture this: You are in a system design interview. The interviewer asks you to design a ledger system or a payment settlement pipeline. Eager to show you can handle scale, you confidently state, "We'll design this to handle 100K transactions a day."&lt;/p&gt;

&lt;p&gt;The interviewer nods. But that number just told them very little about whether you can actually engineer the system.&lt;/p&gt;

&lt;p&gt;Leading with total daily volume is the most common trap candidates fall into. It sounds like a big, impressive number. But in reality, quoting a daily aggregate proves you are looking at the system through a marketing lens, not an engineering one.&lt;/p&gt;

&lt;p&gt;What Actually Matters: The Shape of the Workload&lt;br&gt;
Systems do not experience load as a perfectly smooth, distributed stream over 24 hours. They experience load in waves, spikes, and violent bursts. To design a resilient architecture, the raw daily number is useless. You need three specific dimensions:&lt;/p&gt;

&lt;p&gt;Peak TPS (Transactions Per Second): The absolute highest throughput the system must sustain during peak traffic.&lt;/p&gt;

&lt;p&gt;Workload Shape: How traffic distributes over time — business hours concentration, sudden marketing flashes, or scheduled batch windows.&lt;/p&gt;

&lt;p&gt;Burst Duration: How long that peak sustained load lasts. Is it a 5-second spike or a 45-minute sustained mountain?&lt;/p&gt;

&lt;p&gt;Let's do the math that most engineers skip in an interview.&lt;/p&gt;

&lt;p&gt;If you take 100K transactions and spread them evenly across 24 hours, you get roughly 1 TPS — one transaction per second. A modest, single-core database instance running on a laptop can handle that in its sleep. You don't need a distributed system for 1 TPS; you barely need a framework.&lt;/p&gt;

&lt;p&gt;But real-world systems don't work that way.&lt;/p&gt;

&lt;p&gt;A Real-World Case Study: The Deceptive Average&lt;br&gt;
I ran into this exact problem while building an end-of-day (EOD) reconciliation service for a major instant payment rail — one that processes tens of thousands of financial transactions daily and must verify every single one against an external banking ledger within a strict compliance window.&lt;/p&gt;

&lt;p&gt;On paper, the system processed around 100K transactions a day. Reasonable. Manageable. Nothing alarming.&lt;/p&gt;

&lt;p&gt;But the shape of the workload told a completely different story.&lt;/p&gt;

&lt;p&gt;Daytime traffic trickled in slowly. Then, as merchants settled their books, payroll batches triggered, and consumers made last-minute transfers, load climbed sharply toward end of day. And at the reconciliation window itself — where the external bank flushed the final ledger data in a concentrated burst — everything had to be processed before the next business day opened.&lt;/p&gt;

&lt;p&gt;When we actually measured the workload shape during that final window, the naive average and the engineering reality had almost nothing in common:&lt;/p&gt;

&lt;p&gt;Naive average: ~1 TPS, spread across 24 hours&lt;/p&gt;

&lt;p&gt;Actual peak: 22 TPS, concentrated in a 45-minute EOD window&lt;/p&gt;

&lt;p&gt;That is a 22x gap between the number you'd quote in a planning meeting and the number your system actually has to survive.&lt;/p&gt;

&lt;p&gt;Why This Changes Every Engineering Decision&lt;br&gt;
If you design for 1 TPS average, you might reasonably ask: do we even need a message queue here? A simple cron job running a sequential loop would work.&lt;/p&gt;

&lt;p&gt;But when you design for a 22 TPS burst with heavy downstream dependencies, the entire architectural conversation shifts.&lt;/p&gt;

&lt;p&gt;Thread pool sizing — your reconciliation worker can no longer be a single-threaded job. You have to reason about concurrency, resource contention, and memory footprints before writing a single line of code.&lt;/p&gt;

&lt;p&gt;Idempotency is non-negotiable — at burst load, network hiccups happen and retries are guaranteed. If your database upsert logic isn't strictly idempotent, you will be reconciling duplicates at 3 AM wondering why the ledgers don't balance.&lt;/p&gt;

&lt;p&gt;Downstream capacity limits — the external banking APIs you are calling have their own strict rate limits. Your 22 TPS ambition is irrelevant if the counterparty throttles you at 5 TPS. This forces you to design token-bucket rate limiters and robust client-side queuing — and to invest in building an in-house downstream simulator that covers all failure paths, not just the happy path.&lt;/p&gt;

&lt;p&gt;Pragmatic architecture over greenfield hype — when faced with a compressed burst window and complex recovery paths, a trendy event-driven microservices approach might introduce massive operational complexity. If the external bank operates on a secure, file-based exchange model, a robust file-chunking batch architecture might actually be the more defensible, resilient choice for Phase 1. It's boring, but it's correct.&lt;/p&gt;

&lt;p&gt;The Questions an Interviewer Actually Wants You to Ask&lt;br&gt;
Whenever an interviewer hands you a vague requirement like "Design a system that handles X hundred thousand requests a day," don't immediately start drawing boxes on the whiteboard.&lt;/p&gt;

&lt;p&gt;Stop. And ask the single question that separates senior practitioners from theorists:&lt;/p&gt;

&lt;p&gt;"What does the peak traffic look like, and over what specific window does it occur?"&lt;/p&gt;

&lt;p&gt;From there, you want to understand whether specific business events — market close, midnight clearing, payroll batches — concentrate the load into a predictable window, and what the latency and rate limits of your downstream dependencies look like precisely during that window.&lt;/p&gt;

&lt;p&gt;When you ask these questions, the interviewer knows you have actually operated systems at scale. You are no longer guessing; you are gathering the constraints required to build a real system.&lt;/p&gt;

&lt;p&gt;Closing Thought&lt;br&gt;
Daily transaction volume is a marketing number.&lt;/p&gt;

&lt;p&gt;Peak TPS over a compressed burst window is an engineering number.&lt;/p&gt;

&lt;p&gt;The next time you are in the hot seat, ignore the aggregate daily volume. Figure out the shape of the peak, calculate the true strain on the system, and design for that instead.&lt;/p&gt;

&lt;p&gt;If this kind of thinking is what you want to sharpen before your next system design round, I work with senior engineers on exactly this — translating real production experience into interview-ready depth. You can find me at topmate.io/aliasgar_kantawala.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backendengineering</category>
      <category>interviewprep</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Have you ever built a scale web app, testing it everything works well, only for it to break when there are many visits on the site? 

Here below is a guide to prevent this from happening while using Django.</title>
      <dc:creator>CodeXmingle</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:13:28 +0000</pubDate>
      <link>https://dev.to/codexmingle_community/have-you-ever-built-a-scale-web-app-testing-it-everything-works-well-only-for-it-to-break-when-1561</link>
      <guid>https://dev.to/codexmingle_community/have-you-ever-built-a-scale-web-app-testing-it-everything-works-well-only-for-it-to-break-when-1561</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/codexmingle_community/why-your-django-application-becomes-slow-and-how-experienced-developers-prevent-it-3bm7" class="crayons-story__hidden-navigation-link"&gt;Why Your Django Application Becomes Slow — And How Experienced Developers Prevent It&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/codexmingle_community" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3134876%2Fdac0f1f3-b1e5-49b0-a845-b551067f2aa8.png" alt="codexmingle_community profile" class="crayons-avatar__image" width="96" height="96"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/codexmingle_community" class="crayons-story__secondary fw-medium m:hidden"&gt;
              CodeXmingle
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                CodeXmingle
                
              
              &lt;div id="story-author-preview-content-3965384" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/codexmingle_community" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3134876%2Fdac0f1f3-b1e5-49b0-a845-b551067f2aa8.png" class="crayons-avatar__image" alt="" width="96" height="96"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;CodeXmingle&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/codexmingle_community/why-your-django-application-becomes-slow-and-how-experienced-developers-prevent-it-3bm7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/codexmingle_community/why-your-django-application-becomes-slow-and-how-experienced-developers-prevent-it-3bm7" id="article-link-3965384"&gt;
          Why Your Django Application Becomes Slow — And How Experienced Developers Prevent It
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/django"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;django&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/database"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;database&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/codexmingle_community/why-your-django-application-becomes-slow-and-how-experienced-developers-prevent-it-3bm7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>django</category>
      <category>performance</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automate creation of Amazon CloudWatch alarms</title>
      <dc:creator>Jonas Barros</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:13:10 +0000</pubDate>
      <link>https://dev.to/jonasbarros/automate-creation-of-amazon-cloudwatch-alarms-43o1</link>
      <guid>https://dev.to/jonasbarros/automate-creation-of-amazon-cloudwatch-alarms-43o1</guid>
      <description>&lt;p&gt;Recently I developed a new feature for this &lt;a href="https://github.com/marketplace/actions/automate-dashboards" rel="noopener noreferrer"&gt;Github Action&lt;/a&gt; to automate the creation of AWS Cloudwatch alarms.&lt;br&gt;
Next steps I will show you the settings you need to add to your project to automate creation of CloudWatch alarms. &lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;1- Your project should use the github actions&lt;/p&gt;

&lt;p&gt;2- Your user must have permissions to create an OpenID Connect IDP, policies, and roles in your AWS account.&lt;/p&gt;

&lt;p&gt;3- AWS CLI installed on your computer to make it easier to create IAM policies, roles, and a new IDP to connect to the GitHub account.&lt;/p&gt;

&lt;p&gt;More informations about other features and prerequisites is available at &lt;a href="https://dev.to/jonasbarros/using-the-github-actions-to-automate-monitoring-dashboards-2koe"&gt;Automate Dashboards Quick Start&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;Before starting, add the code snipet to your Github Actions file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#... before code&lt;/span&gt;

&lt;span class="c1"&gt;# *** ADD THE CODE SNIPET BELOW ***&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AssumeRoleAndCallIdentity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Add this step to authenticate on the AWS account &lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure aws credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v1.7.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::AWS_ACCOUNT_ID:role/to_enable_creating_dashbaords&lt;/span&gt;
          &lt;span class="na"&gt;role-session-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub_to_AWS_via_FederatedOIDC&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AWS_REGION }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Add this step to load the github action&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create dash&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
          &lt;span class="na"&gt;ACTION_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ACTION_NAME }}&lt;/span&gt; &lt;span class="c1"&gt;# Create a new environment in your repository with the SNS ARN value&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JonasBarros1998/automate-dashboards@latest"&lt;/span&gt;

&lt;span class="c1"&gt;#... after code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full Github workflow file should look similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File location: .github/workflows/action.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Connect to an AWS role from a GitHub repository and install the action to create dashbaord in the CloudWatch&lt;/span&gt;

&lt;span class="c1"&gt;# Execute the action when the user opens a new issue&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Change the region to your current region&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1"&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="c1"&gt;# *** ADD THE CODE SNIPPET BELOW ***&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AssumeRoleAndCallIdentity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Add this step to authenticate with AWS account &lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure aws credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v1.7.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::AWS_ACCOUNT_ID:role/to_enable_creating_dashbaords&lt;/span&gt;
          &lt;span class="na"&gt;role-session-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub_to_AWS_via_FederatedOIDC&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AWS_REGION }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Add this step to load the Github Action&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create dash&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
          &lt;span class="na"&gt;ACTION_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ACTION_NAME }}&lt;/span&gt; &lt;span class="c1"&gt;# Create a new environment in your repository with the SNS ARN value&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JonasBarros1998/automate-dashboards@latest"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;${{ secrets.ACTION_NAME }}&lt;/code&gt;: Add a new repository secret to your Github repository with your SNS ARN value. Create an SNS topic to send notifications to you when an alarm status is &lt;strong&gt;triggered&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;**If your project is public, we highly recommend creating a Github repository secret to safely store the ARN value of your SNS topic.&lt;/p&gt;




&lt;h3&gt;
  
  
  How to execute the action to create alarms in AWS CloudWatch alarms
&lt;/h3&gt;

&lt;p&gt;To automate AWS CloudWatch Alarms, you need to &lt;strong&gt;open a new issue&lt;/strong&gt; with the title &lt;strong&gt;Create Dashboard&lt;/strong&gt;. In body of the issue, add the JSON configuration specifyng the settings for your new alarms.  &lt;/p&gt;

&lt;p&gt;For example, you can send this json if you want to create a new alarm for an AWS Lambda.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dashboard-services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serviceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"change-data-capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"alarms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
          &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Average"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invocations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
          &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LessThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Errors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
          &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wish to add 2 or more services, use the JSON format below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dashboard-services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"change-data-capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"alarms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Duration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Average"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invocations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LessThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Errors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dashboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dynamodb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"alarms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ConsumedReadCapacityUnits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ConsumedWriteCapacityUnits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-topic-dashboards"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SNS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"alarms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NumberOfNotificationsFailed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NumberOfMessagesPublished"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yout opened issue should should look similar to this &lt;a href="https://github.com/JonasBarros1998/automate-dashboards/issues/4" rel="noopener noreferrer"&gt;example&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  More informations about the JSON attributes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;If set `true`, it enables the creation of a new CloudWatch dashboard, but if to set `false`, the action will create a new CloudWatch alarm instead.&lt;/span&gt; 
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
  &lt;span class="na"&gt;accept values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The metric name for the CloudWatch alarm&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="na"&gt;accept values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NumberOfObjects, BucketSizeBytes, NumberOfMessagesSent, NumberOfMessagesReceiver, NumberEmptyMessages, NumberOfNotificationsFailed, NumberOfMessagesPublished, Duration, Invocations, Errors, ConsumedReadCapacityUnits, ConsumedWriteCapacityUnits, CPUUtilization, StatusCheckFailed_Instance.&lt;/span&gt;

&lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;To monitoring period specified in seconds&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Integer&lt;/span&gt;
  &lt;span class="na"&gt;requiriment values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Any value greather than 0. For example 600 seconds is equivalent to 10 minutes&lt;/span&gt;

&lt;span class="na"&gt;statistic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The metric statistic&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="na"&gt;requiriment values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;We currently accept the `Sum` value.&lt;/span&gt; 

&lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;decsription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The alarm condition. If the condition is met, the alarm triggers and sends a notification to the specified SNS topic.&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt; 
  &lt;span class="na"&gt;requiriment values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GreaterThanThreshold"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LessThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GreaterThanOrEqualToThreshold"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LessThanLowerThreshold"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have completed all steps, create your issue and monitor the workflow execution. &lt;/p&gt;

&lt;p&gt;The issue format should follow the example provided below.&lt;br&gt;
&lt;a href="https://github.com/JonasBarros1998/automate-dashboards/issues/4" rel="noopener noreferrer"&gt;Example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open a &lt;a href="https://github.com/JonasBarros1998/automate-dashboards/issues" rel="noopener noreferrer"&gt;new issue&lt;/a&gt; if you search some problem after executed the workflow.&lt;/p&gt;




&lt;h3&gt;
  
  
  Currently supported AWS services for CloudWatch alarm automation:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;AWS Lambda&lt;/li&gt;
&lt;li&gt;AWS Dynamodb&lt;/li&gt;
&lt;li&gt;AWS EC2&lt;/li&gt;
&lt;li&gt;AWS SNS&lt;/li&gt;
&lt;li&gt;AWS SQS&lt;/li&gt;
&lt;li&gt;AWS S3
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>sre</category>
      <category>githubactions</category>
      <category>github</category>
    </item>
    <item>
      <title>Cron jobs sin Celery, sin Redis, sin Beat: cómo Fitz mete un scheduler distribuido adentro del lenguaje</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:10:31 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/cron-jobs-sin-celery-sin-redis-sin-beat-como-fitz-mete-un-scheduler-distribuido-adentro-del-bj9</link>
      <guid>https://dev.to/martin_palopoli/cron-jobs-sin-celery-sin-redis-sin-beat-como-fitz-mete-un-scheduler-distribuido-adentro-del-bj9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Para correr una task cada 5 minutos en Python necesitás Celery + Redis + Celery Beat + worker process + Dockerfile dedicado. En Fitz es un decorador. Con retry, timezone, persistencia y catch-up adentro del lenguaje.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  La historia de siempre
&lt;/h2&gt;

&lt;p&gt;Tu cliente está contento con la API. Está corriendo. Entonces te pide tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"¿Podemos limpiar sesiones vencidas cada noche?"&lt;/li&gt;
&lt;li&gt;"¿Podés mandar el email de bienvenida en background así el signup no espera?"&lt;/li&gt;
&lt;li&gt;"El reporte diario tiene que correr a las 9 AM hora de Buenos Aires."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tres pedidos chicos. Si estás en Python, la respuesta honesta es: "ok, dame dos días para meter Celery."&lt;/p&gt;

&lt;h3&gt;
  
  
  El stack típico de Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;celery redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;celery_app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Celery&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;celery.schedules&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;

&lt;span class="n"&gt;celery_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Celery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://redis:6379/0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://redis:6379/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beat_schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cleanup-sessions-nightly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.cleanup_old_sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&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="n"&gt;minute&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="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily-report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp.tasks.generate_daily_report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&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="c1"&gt;# 9am Buenos Aires = 12:00 UTC
&lt;/span&gt;        &lt;span class="c1"&gt;# ojo: si DST cambia, esto te llega tarde
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;celery_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tasks.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.celery_app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;celery_app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_session&lt;/span&gt;

&lt;span class="nd"&gt;@celery_app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autoretry_for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="n"&gt;retry_backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cleanup_old_sessions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM sessions WHERE expires_at &amp;lt; now()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@celery_app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autoretry_for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SMTPError&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="n"&gt;retry_backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;api.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fire-and-forget vía broker
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvicorn api:app --host 0.0.0.0&lt;/span&gt;
  &lt;span class="na"&gt;celery-worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;celery -A celery_app worker --loglevel=info&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;celery-beat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;celery -A celery_app beat --loglevel=info&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un &lt;code&gt;supervisord.conf&lt;/code&gt; o systemd unit que mantenga viva la cosa cuando crashee.&lt;/li&gt;
&lt;li&gt;(Opcional) &lt;code&gt;flower&lt;/code&gt; para visibility — 4to proceso.&lt;/li&gt;
&lt;li&gt;Conversiones de timezone hechas a mano (Celery beat trabaja en UTC, vos tenés que calcular el offset).&lt;/li&gt;
&lt;li&gt;Cuando el worker crashea entre que pegó al broker y completó la task: ¿se reintenta? ¿queda colgada? Depende de cómo te configuraste el &lt;code&gt;acks_late&lt;/code&gt; y &lt;code&gt;task_reject_on_worker_lost&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuatro procesos en producción. Tres librerías nuevas. Un broker. Convenciones nuevas. Un día de setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo mismo en Fitz
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 3 * * *", tz="UTC")
async fn cleanup_old_sessions(db: DbConn) {
    db.exec("DELETE FROM sessions WHERE expires_at &amp;lt; now()").await
}

@cron("0 9 * * *", tz="America/Argentina/Buenos_Aires")
async fn daily_report(db: DbConn) {
    // ...
}

@background
async fn send_welcome_email(email: Str) {
    // cosa cara
}

@post("/signup")
fn signup(creds: Credentials) -&amp;gt; User {
    let user = create_user(creds)
    spawn(send_welcome_email(user.email))  // fire-and-forget tipado
    return user
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo. Un binario. Un proceso. Sin broker. Sin worker dedicado. Sin beat. El scheduler corre adentro del proceso de Fitz.&lt;/p&gt;

&lt;h2&gt;
  
  
  La tabla cruda
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Python (Celery + Redis + Beat)&lt;/th&gt;
&lt;th&gt;Fitz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup inicial&lt;/td&gt;
&lt;td&gt;4 archivos + 3 servicios + 1 broker&lt;/td&gt;
&lt;td&gt;1 decorador&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;crontab(hour=3, minute=0)&lt;/code&gt; en config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@cron("0 3 * * *")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timezone&lt;/td&gt;
&lt;td&gt;UTC + offset manual&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tz="America/Argentina/Buenos_Aires"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry/backoff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;autoretry_for=(...)&lt;/code&gt; + &lt;code&gt;retry_backoff=True&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;retry={max=3, backoff="exponential", initial_secs=1, max_secs=30}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistencia de runs&lt;/td&gt;
&lt;td&gt;Redis result backend + visualización con Flower&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;store=db&lt;/code&gt;, tabla auto-creada en Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catch-up de runs perdidos&lt;/td&gt;
&lt;td&gt;No nativo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;catch_up=true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background fire-and-forget&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.delay(arg)&lt;/code&gt; con broker&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;spawn(fn_call)&lt;/code&gt; directo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type checking de args&lt;/td&gt;
&lt;td&gt;Ninguno (todo serializado vía JSON)&lt;/td&gt;
&lt;td&gt;Static —&lt;code&gt;spawn&lt;/code&gt; exige &lt;code&gt;@background&lt;/code&gt; y refina a &lt;code&gt;Future&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Procesos en prod&lt;/td&gt;
&lt;td&gt;api + worker + beat + redis&lt;/td&gt;
&lt;td&gt;api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Imagen Docker&lt;/td&gt;
&lt;td&gt;3 (api, worker, beat) + redis&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Persistencia opt-in con &lt;code&gt;store=db&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Cuando querés history de runs para auditoría:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 3 * * *", store=db, retry={max=3, backoff="exponential"})
async fn cleanup_old_sessions(db: DbConn) {
    db.exec("DELETE FROM sessions WHERE expires_at &amp;lt; now()").await
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Al boot, Fitz crea (si no existe) &lt;code&gt;fitz_cron_jobs&lt;/code&gt; y &lt;code&gt;fitz_cron_runs&lt;/code&gt;. Cada attempt queda persistida con &lt;code&gt;started_at&lt;/code&gt;/&lt;code&gt;finished_at&lt;/code&gt;/&lt;code&gt;status&lt;/code&gt;/&lt;code&gt;attempt&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt;. Lo consultás con SQL plano:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;job_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fitz_cron_runs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin instalar Flower. Sin webhook para Sentry. Tu DB que ya tenés.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catch-up
&lt;/h3&gt;

&lt;p&gt;Si el binario estuvo caído entre las 3 AM y las 7 AM y el cron era a las 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Celery beat&lt;/strong&gt;: la oportunidad se pierde. La task no se vuelve a disparar hasta la próxima medianoche siguiente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fitz con &lt;code&gt;catch_up=true&lt;/code&gt;&lt;/strong&gt;: al boot, calcula que hubo un run perdido entre &lt;code&gt;last_run_at&lt;/code&gt; y &lt;code&gt;now&lt;/code&gt;, ejecuta UN run inmediato (no N — evita spam), y vuelve al schedule normal.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 3 * * *", store=db, catch_up=true)
async fn cleanup_old_sessions(db: DbConn) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retry con backoff configurable
&lt;/h2&gt;

&lt;p&gt;Tres modos de backoff: &lt;code&gt;exponential&lt;/code&gt; (1s, 2s, 4s, 8s...), &lt;code&gt;linear&lt;/code&gt; (1s, 2s, 3s, 4s...), &lt;code&gt;constant&lt;/code&gt; (1s, 1s, 1s...). Con cap &lt;code&gt;max_secs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("*/10 * * * *",
    retry={ max=5, backoff="exponential", initial_secs=1, max_secs=60 })
async fn sync_external_api(db: DbConn) {
    // 1s → 2s → 4s → 8s → 16s (capeado a 60s si llegara más alto)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En Python con Celery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@celery_app.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;autoretry_for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;retry_backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retry_backoff_max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retry_jitter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_external_api&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Equivalente. Pero notá que Celery tenés que enumerar las excepciones que disparan retry, y el backoff es un bool en lugar de un kind enum. Fitz hace retry ante &lt;strong&gt;cualquier&lt;/strong&gt; error que devuelva la fn (consistente con el modelo Result del lenguaje), y el modo de backoff es explícito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timezone real, no offset hardcoded
&lt;/h2&gt;

&lt;p&gt;Las DST changes son el bug que te despiertan a las 4 AM. Celery beat trabaja en UTC y vos tenés que recordar que entre marzo y noviembre tu cron de las 9 AM Buenos Aires se corre a las 12 UTC, pero el resto del año cambia. Tu cliente está en otro huso. Tu app vende a tres países más.&lt;/p&gt;

&lt;p&gt;Fitz acepta IANA timezones directamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 9 * * *", tz="America/Argentina/Buenos_Aires") async fn buenos_aires() { ... }
@cron("0 9 * * *", tz="America/New_York")              async fn new_york()      { ... }
@cron("0 9 * * *", tz="Asia/Tokyo")                    async fn tokyo()         { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada uno corre a las 9 AM &lt;strong&gt;local&lt;/strong&gt; de su tz, con DST manejado por la lib subyacente (&lt;code&gt;chrono-tz&lt;/code&gt;). No conversiones a mano.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background jobs sin broker
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;send_welcome_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# serializa args → Redis → worker
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fitz
@background
async fn send_welcome_email(email: Str) {
    smtp.send(email, "Welcome!")
}

@post("/signup")
fn signup(creds: Credentials) -&amp;gt; User {
    let user = create_user(creds)
    spawn(send_welcome_email(user.email))  // tokio::spawn nativo, tipado
    return user
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El compilador exige que la fn esté decorada con &lt;code&gt;@background&lt;/code&gt; para autorizar el &lt;code&gt;spawn(...)&lt;/code&gt; — sin esto, el callsite tira error de tipo en build time. Y refina el retorno a &lt;code&gt;Future&amp;lt;Null&amp;gt;&lt;/code&gt; con el tipo concreto del target, no &lt;code&gt;Any&lt;/code&gt;. Si &lt;code&gt;send_welcome_email&lt;/code&gt; retorna &lt;code&gt;Result&amp;lt;()&amp;gt;&lt;/code&gt;, el &lt;code&gt;spawn(...)&lt;/code&gt; te lo da tipado.&lt;/p&gt;

&lt;p&gt;¿Cuándo NO usar &lt;code&gt;@background&lt;/code&gt; y volver a Celery?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si necesitás que los jobs sobrevivan crashes del proceso → persistencia explícita con &lt;code&gt;store=db&lt;/code&gt; cubre cron jobs; para &lt;code&gt;@background&lt;/code&gt; con persistencia es deuda residual.&lt;/li&gt;
&lt;li&gt;Si necesitás distribuir jobs en N workers en N nodos → Celery con un broker compartido sigue siendo la respuesta. &lt;code&gt;@background&lt;/code&gt; corre en el proceso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para el 90% de servicios (cleanup nocturno, email transaccional, recálculo de KPIs, sync de cache) el modelo de Fitz alcanza y sobra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cron-only mode para systemd
&lt;/h2&gt;

&lt;p&gt;Para servicios que SOLO tienen jobs (sin HTTP), &lt;code&gt;fitz build&lt;/code&gt; produce un binario que arranca el scheduler y bloquea con ctrl+c:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 3 * * *") async fn cleanup() { ... }
@cron("*/15 * * * *") async fn sync() { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Scheduled jobs for myapp&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/myapp-jobs&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cero brokers. El binario es 5 MB. &lt;code&gt;systemctl restart myapp-jobs&lt;/code&gt; y listo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paridad bit-a-bit &lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Esto es lo que vuelve esto entregable: el binario que producís con &lt;code&gt;fitz build&lt;/code&gt; ejecuta el mismo scheduler que &lt;code&gt;fitz run&lt;/code&gt;, con el mismo cron expression parser, los mismos retries, la misma timezone. Misma sintaxis, mismas semánticas, sin "ah esto solo funciona en producción si configurás Celery".&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que Fitz NO te da (todavía)
&lt;/h2&gt;

&lt;p&gt;Soy honesto sobre dónde no llega:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distribuir jobs en N workers/nodos compartiendo cola&lt;/strong&gt;. &lt;code&gt;@cron&lt;/code&gt; corre en el proceso. Si corrés dos binarios con el mismo &lt;code&gt;@cron&lt;/code&gt;, ambos disparan — bug, no feature. Para horizontal scaling de jobs, sigue siendo Celery (o NATS JetStream, o Temporal). Hay deuda explícita en el roadmap sobre locks distribuidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI tipo Flower&lt;/strong&gt;. Los datos están en &lt;code&gt;fitz_cron_jobs&lt;/code&gt; y &lt;code&gt;fitz_cron_runs&lt;/code&gt;. Si querés dashboards, dashboards externos (Grafana, Metabase) cubren.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@background&lt;/code&gt; con persistencia entre restarts&lt;/strong&gt;. Para &lt;code&gt;@cron&lt;/code&gt; está en v0.11.2 (cierre 9.w.3.iter2). Para &lt;code&gt;@background&lt;/code&gt; arranca el spawn pero no sobrevive crash — diferido a iter3 si entra demanda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancelación de jobs en flight&lt;/strong&gt;. Hoy no hay API para "cancelar todos los runs en cola de X job". El proceso muere → los runs en flight mueren con él.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si estás en uno de esos casos, Fitz no es la herramienta hoy. Si no, el modelo de un solo binario cubre el caso real con menos partes móviles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;El argumento para sumar Celery a una API en Python típicamente es: "pero después escala mejor." En el 90% de los servicios que escribí en la última década, ese "después" nunca llegó — la app pasó toda su vida útil con menos de 100 jobs por hora y nunca necesitó un cluster de workers.&lt;/p&gt;

&lt;p&gt;Para ese 90%, Fitz reemplaza 4 procesos + 3 librerías + 1 broker con un decorador. Cuando llegues al otro 10% donde necesitás scaling horizontal real, Celery sigue ahí — &lt;code&gt;from python import celery&lt;/code&gt; también está disponible, podés hacerlo tú mismo.&lt;/p&gt;

&lt;p&gt;Pero arrancar el proyecto con el modelo más simple posible y subir la complejidad solo cuando lo necesitás es el ciclo de feedback que Fitz quiere darte.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Próximo post de la serie&lt;/strong&gt;: &lt;strong&gt;"Auth con JWT, RBAC y token blacklist sin pegar 5 librerías: Fitz vs FastAPI + python-jose + passlib + Redis blacklist"&lt;/strong&gt; — el flow completo de auth, lado a lado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/Thegreekman76/fitz" rel="noopener noreferrer"&gt;github.com/Thegreekman76/fitz&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Capítulo 30 de la guía&lt;/strong&gt; (Jobs sin Celery): &lt;a href="https://thegreekman76.github.io/fitz/guide/" rel="noopener noreferrer"&gt;docs/guide.md&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
