<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Zombie Trend AI Dev Notes]]></title><description><![CDATA[Zombie Trend AI Dev Notes]]></description><link>https://zombietrendai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 21:33:14 GMT</lastBuildDate><atom:link href="https://zombietrendai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Running minutes-long AI video jobs on Cloudflare Workers without blocking a single request]]></title><description><![CDATA[How we run minutes-long AI video renders on Cloudflare Workers: quote, reserve, submit, reconcile, copy, settle, plus three launch-week bugs.]]></description><link>https://zombietrendai.hashnode.dev/running-minutes-long-ai-video-jobs-on-cloudflare-workers-without-blocking-a-single-request</link><guid isPermaLink="true">https://zombietrendai.hashnode.dev/running-minutes-long-ai-video-jobs-on-cloudflare-workers-without-blocking-a-single-request</guid><category><![CDATA[cloudflare-worker]]></category><category><![CDATA[serverless]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[AI]]></category><category><![CDATA[side project]]></category><dc:creator><![CDATA[Zombie Trend AI]]></dc:creator><pubDate>Tue, 13 Oct 2026 03:06:16 GMT</pubDate><content:encoded><![CDATA[<p><em>Disclosure: I build <a href="https://zombietrendai.org">Zombie Trend AI</a>, the app this post is about. I wrote it with AI help and checked every detail against our code before publishing.</em></p>
<p>This autumn I shipped a small web app that turns one or two photos into a 15-second AI video for a TikTok trend. The product side is simple: upload photos, pick an effect, wait, download. The engineering side has one hard constraint. A video takes several minutes to render on the model provider's side, and our app runs on Cloudflare Workers, where a request should finish in seconds.</p>
<p>Here is the design we ended up with, plus the three bugs that kept every production render from working on launch week.</p>
<h2>The rule: no request waits for a video</h2>
<p>Every request does one small, fast step and returns. The render itself lives in a database row that moves through a short list of states:</p>
<pre><code>reserved -&gt; submitting -&gt; running -&gt; succeeded / failed
                       \-&gt; uncertain -&gt; running / failed
</code></pre>
<ul>
<li><strong>Quote.</strong> The browser asks for a price first. The server works out the cost from the model, length and format, and the user sees it before paying anything.</li>
<li><strong>Create.</strong> On "Generate", the server reserves that amount from the user's balance and writes the task row in the same transaction. Then it submits the job to the provider.</li>
<li><strong>Poll and reconcile.</strong> The browser polls a cheap status endpoint. A scheduled job also checks tasks that have not moved for a while, so a closed tab never leaves a render stuck.</li>
<li><strong>Settle.</strong> When the provider reports success, we copy the file into our own private storage, then charge the reservation. On failure we release it, so the credits go back automatically.</li>
</ul>
<h3>Why there is an <code>uncertain</code> state</h3>
<p>The interesting failure is not "the provider said no". It is "we sent the job and the connection died before we got an answer". Did the provider accept it or not? If you mark it failed and refund, you might pay for a render you never deliver. If you retry, you might pay twice.</p>
<p>So the task goes to <code>uncertain</code>, and only the reconcile step can move it on, after asking the provider what it actually has. It is one extra state, and it removed a whole class of "I was charged but got nothing" support emails before they happened.</p>
<h2>Money as integers</h2>
<p>The ledger stores amounts as integer micro-dollars, and the users see credits. Every function that touches money checks that its input is a safe, non-negative integer and throws if not. Floating point never gets near a balance.</p>
<h2>Copy the result before you show it</h2>
<p>Provider download links expire. If you store their URL, your users' videos quietly vanish a few days later. We copy every finished file into a private bucket and serve it through short-lived signed links. That way the video stays in the user's library until <em>they</em> delete it.</p>
<h2>Three launch-week bugs worth stealing</h2>
<p><strong>1. A safety cap that blocked everything.</strong> I had added a per-task cost ceiling as a guard against runaway spend. Then the default model got more expensive, and the ceiling sat below the real price of every single render. Nothing errored loudly: tasks just refused to start. Lesson: test your guard rails against the real price list, not the one you had in mind when you wrote them.</p>
<p><strong>2. The provider wanted a file extension.</strong> The photo links we handed to the provider were clean signed tokens with no extension, and its API answered "file type not supported". Every render was rejected before it started. Now each link ends in <code>.jpg</code>, <code>.png</code> or <code>.webp</code> based on the stored content type, and our own route strips the extension again before it checks the token. Lesson: when an upstream API takes a URL, try it with the exact URL shape you will use in production, not a pretty test link.</p>
<p><strong>3. A fixed daily budget punishes your best day.</strong> A hard daily spend cap sounds safe, but on the day traffic finally shows up it starts turning away people who have already paid. Our daily provider budget is now 80% of the credits users currently hold, never above an admin cap. Credits are prepaid, so the budget grows with real demand and still cannot run away.</p>
<h2>Do not wake the database for nothing</h2>
<p>Our Postgres is on a serverless plan that sleeps when idle and bills compute time. A cron that queries it every few minutes keeps it awake forever. So submitting a task writes an "awake until" timestamp to object storage (two hours ahead), and a reconcile that still sees a running task pushes it forward. The cron reads that one small file first and skips the database once the time has passed.</p>
<h2>The result</h2>
<p>From the user's side, they click Generate and see a progress state, and a <a href="https://zombietrendai.org/zombie-hug-ai">zombie hug AI</a> clip shows up in their library a few minutes later. They can leave the page and come back. From the server's side, no request lasts longer than a database write and one API call.</p>
<p>If you are building anything on top of slow AI models (video, long audio, big batch jobs), the pattern carries over: quote, reserve, submit, reconcile, copy, settle. Keep the uncertain state. You will need it.</p>
]]></content:encoded></item></channel></rss>