<?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: Ryan - building OwnStack</title>
    <description>The latest articles on DEV Community by Ryan - building OwnStack (@ownstackhq).</description>
    <link>https://dev.to/ownstackhq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4030038%2Fa17d6fb6-67c8-4635-b532-ac8d69cfcd42.png</url>
      <title>DEV Community: Ryan - building OwnStack</title>
      <link>https://dev.to/ownstackhq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ownstackhq"/>
    <language>en</language>
    <item>
      <title>You baked the model into the image. One env var can silently un-bake it</title>
      <dc:creator>Ryan - building OwnStack</dc:creator>
      <pubDate>Sat, 01 Aug 2026 02:05:53 +0000</pubDate>
      <link>https://dev.to/ownstackhq/you-baked-the-model-into-the-image-one-env-var-can-silently-un-bake-it-4dno</link>
      <guid>https://dev.to/ownstackhq/you-baked-the-model-into-the-image-one-env-var-can-silently-un-bake-it-4dno</guid>
      <description>&lt;p&gt;On serverless GPU, cold start is image pull plus model load. The single biggest win is usually to stop downloading weights at boot: download them at &lt;em&gt;build&lt;/em&gt; time so they ship inside the image and load from local disk instead.&lt;/p&gt;

&lt;p&gt;The setup is two lines. Point the cache at a path inside the image, then fetch during the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; HF_HOME=/opt/huggingface&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from huggingface_hub import snapshot_download; snapshot_download('&amp;lt;repo&amp;gt;', revision='&amp;lt;pinned&amp;gt;')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;snapshot_download&lt;/code&gt; writes into &lt;code&gt;HF_HOME&lt;/code&gt;, that directory becomes an image layer, and at runtime the loader finds everything already on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that doesn't look like a failure
&lt;/h2&gt;

&lt;p&gt;Here's the part worth knowing. The loader resolves weights through &lt;code&gt;HF_HOME&lt;/code&gt; &lt;strong&gt;at runtime&lt;/strong&gt;. So if anything in your runtime environment sets &lt;code&gt;HF_HOME&lt;/code&gt; to a different path — a network volume, a mount, a leftover template variable — the loader looks there, finds an empty cache, and quietly falls back to downloading from the network.&lt;/p&gt;

&lt;p&gt;Nothing errors. Your image still contains the weights, sitting in a directory nobody reads anymore. You just paid to bake them, and you're paying the download on every cold start anyway. The only symptom is that cold starts went back to what they were before you did the work, which is easy to blame on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make it hard to get wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Attach no volume at all.&lt;/strong&gt; If the worker has no volume, there's no tempting path to point the cache at. In my setup the endpoints run with volume size zero for exactly this reason: everything the worker needs is in the image, so a volume is not an optimization, it's a way to reintroduce the network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify at boot, not by feel.&lt;/strong&gt; Watch a cold worker's log. If you see download progress for weights, the bake is being bypassed, full stop. A baked image reaching the network for weights is always a misconfiguration, never a normal path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the cache path as part of the build contract.&lt;/strong&gt; The Dockerfile that baked the weights and the runtime env that reads them have to agree. That agreement is invisible in both files individually, so write it down where whoever edits the template env will see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth saying out loud
&lt;/h2&gt;

&lt;p&gt;Baking has real costs: the image gets large, builds take much longer, and updating the model means rebuilding and redistributing the whole thing. If your weights are small or your traffic keeps workers warm, a volume or a runtime fetch may genuinely be the better trade. This is about the case where you already chose to bake — because then paying the build cost &lt;em&gt;and&lt;/em&gt; the download cost is the worst of both.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>machinelearning</category>
      <category>serverless</category>
    </item>
    <item>
      <title>RunPod serverless: jobs stuck in queue forever after changing MODEL_ID</title>
      <dc:creator>Ryan - building OwnStack</dc:creator>
      <pubDate>Sat, 01 Aug 2026 01:42:25 +0000</pubDate>
      <link>https://dev.to/ownstackhq/runpod-serverless-jobs-stuck-in-queue-forever-after-changing-modelid-47g5</link>
      <guid>https://dev.to/ownstackhq/runpod-serverless-jobs-stuck-in-queue-forever-after-changing-modelid-47g5</guid>
      <description>&lt;p&gt;Symptom: you submit a job, it sits in &lt;code&gt;IN_QUEUE&lt;/code&gt; and never moves. The endpoint looks healthy in the console. Workers keep starting and dying, so it reads like a queue problem rather than a crash.&lt;/p&gt;

&lt;p&gt;The usual advice is "check your handler logs," which is right but doesn't tell you what to look for. Here is a specific failure mode I lost time to and haven't seen written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that causes it
&lt;/h2&gt;

&lt;p&gt;If you bake model weights into your image, you probably also pin a revision so an upstream change can't silently swap them under you. Reasonable. The trap is that the repo id and the revision usually end up managed in &lt;strong&gt;two different places&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the repo id comes from an env var on the RunPod template&lt;/li&gt;
&lt;li&gt;the revision is pinned in code, next to the loader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change only the repo id, say you point &lt;code&gt;MODEL_ID&lt;/code&gt; at the original repo instead of the quantized mirror you actually baked, and you have created a combination that does not exist: a commit hash that only lives on the mirror, requested against a different repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens at boot
&lt;/h2&gt;

&lt;p&gt;The load misses the local cache (that repo was never baked), falls back to the network, asks for a revision that repo never had, and 404s during startup. The worker dies before your handler is ever reached.&lt;/p&gt;

&lt;p&gt;From the outside this looks nothing like a crash. The endpoint reports healthy, workers cycle, and the job just sits in the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check
&lt;/h2&gt;

&lt;p&gt;Open a &lt;strong&gt;worker's&lt;/strong&gt; log, not the endpoint status. Look for a fetch attempt at boot: if a baked image is reaching the network for weights at all, something already broke. Then compare the repo id in the template env against the exact repo &lt;em&gt;and&lt;/em&gt; revision your Dockerfile downloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If weights are baked, treat repo id and revision as one unit. Pin both in the same place, or set neither at runtime. Splitting them across a template env and a code constant is how you end up asking for a combination that has never existed anywhere.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>ai</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Character consistency isn't a seed trick: a 2-stage image pipeline that actually locks the face</title>
      <dc:creator>Ryan - building OwnStack</dc:creator>
      <pubDate>Thu, 23 Jul 2026 04:00:36 +0000</pubDate>
      <link>https://dev.to/ownstackhq/character-consistency-isnt-a-seed-trick-a-2-stage-image-pipeline-that-actually-locks-the-face-p76</link>
      <guid>https://dev.to/ownstackhq/character-consistency-isnt-a-seed-trick-a-2-stage-image-pipeline-that-actually-locks-the-face-p76</guid>
      <description>&lt;p&gt;If you're building an app that generates the &lt;em&gt;same&lt;/em&gt; character across many scenes, you've probably hit the wall already: seeds drift, LoRA training is heavy and slow, and "same character, new pose" prompting quietly changes the face. The approach that actually holds up in production is a &lt;strong&gt;2-stage pipeline&lt;/strong&gt; — generate one canonical &lt;strong&gt;base image&lt;/strong&gt;, then &lt;strong&gt;edit &lt;em&gt;from that base&lt;/em&gt; as the reference&lt;/strong&gt; for every new scene. Consistency comes from the reference, not the seed.&lt;/p&gt;

&lt;p&gt;Below: why the common approaches drift, how the 2-stage pipeline works, the async job queue that makes it deployable, and the serverless-GPU setup that keeps it affordable. There's a free, runnable slice of the whole transport layer at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious approaches drift
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Seeds.&lt;/strong&gt; A seed pins the &lt;em&gt;noise&lt;/em&gt;, not the &lt;em&gt;identity&lt;/em&gt;. Re-use a seed with the same prompt and you get the same image — but that's reproduction, not consistency. The moment you change the prompt ("now she's in a café"), the denoising path changes and the face re-rolls with it. Seeds give you determinism for identical inputs; they give you nothing for &lt;em&gt;new scenes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt-only ("the same woman as before").&lt;/strong&gt; The model has no memory. Every generation is a fresh sample from the distribution your words describe. "Same face as last time" isn't in the prompt vocabulary — there is no last time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LoRA per character.&lt;/strong&gt; This one actually works — that's why everyone suggests it — but look at what it costs in an app context: curate 15–40 images per character, run a training job per character, store and load adapter weights per character, and repeat all of it whenever a user creates someone new. For a personal project, fine. For an app where &lt;em&gt;users&lt;/em&gt; create characters on demand, you just signed up to run a training farm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2-stage pipeline
&lt;/h2&gt;

&lt;p&gt;The fix is embarrassingly direct once you see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stage 1 — CAST          Stage 2 — RE-SCENE (repeat forever)
text-to-image           image-edit model
"describe character" →  base image + "put them in a café" → scene 1
     = base image       base image + "walking in the rain" → scene 2
                        base image + "reading by a window"  → scene 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stage 1&lt;/strong&gt; runs a text-to-image model once (I use Z-Image-Turbo) to &lt;em&gt;cast&lt;/em&gt; the character — one clean, canonical image. This is the identity. Store it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2&lt;/strong&gt; runs an image-&lt;em&gt;edit&lt;/em&gt; model (Qwen-Image-Edit-2511) for every scene after that. The crucial part: the input isn't a text description of the character — it's the &lt;strong&gt;base image itself&lt;/strong&gt;, plus an instruction describing only &lt;em&gt;what should change&lt;/em&gt; (pose, setting, lighting). The model's job is no longer "imagine this person" but "keep this person, change the scene."&lt;/p&gt;

&lt;p&gt;Consistency stops being a property you &lt;em&gt;hope&lt;/em&gt; the sampler preserves and becomes a property of the &lt;strong&gt;conditioning&lt;/strong&gt;. The reference image is in the input every single time, so the face holds — across twenty scenes, fifty scenes, whatever.&lt;/p&gt;

&lt;p&gt;Two details that matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Describe only the change in Stage 2.&lt;/strong&gt; If your scene prompt re-describes the character ("a woman with long dark hair…"), you're inviting the edit model to re-interpret identity. Prompt the &lt;em&gt;delta&lt;/em&gt;: location, pose, lighting. Identity comes from the pixels, not the words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This isn't people-only.&lt;/strong&gt; The reference-edit mechanic doesn't care what the subject is — the same pipeline holds a consistent dog, robot, or anime character. Anything you can cast in Stage 1, you can re-scene in Stage 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part nobody tells you: you can't call a GPU from a web request
&lt;/h2&gt;

&lt;p&gt;Here's where most "it works in my notebook" projects die in deployment. A generation takes 10–40 seconds. An HTTP request does not want to live that long — serverless platforms cap request duration, browsers give up, and users retry and double-charge themselves.&lt;/p&gt;

&lt;p&gt;The pattern that survives production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client → POST /api/generate
             └── create Job row (status=queued) → return job id immediately
worker   picks up job → calls the GPU endpoint → writes image to storage
             └── Job row: status=completed, resultUrl
client → GET /api/status/:id   (poll)
             └── completed? render the image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request that starts the job returns in milliseconds. The GPU work happens out-of-band. The client polls — a webhook or SSE is the usual alternative, but polling needs zero extra infrastructure (no public URL, identical local and deployed), so that's what I use. A few hard-won rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The job row is the source of truth&lt;/strong&gt;, not the GPU vendor's job id. Vendors lose webhooks; your DB shouldn't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make completion idempotent.&lt;/strong&gt; Two racing polls — or a retry after a timeout — will happily complete the same job twice unless the finalize path claims the row atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store results in object storage and serve URLs&lt;/strong&gt;, not bytes through your app server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hosted API or self-host? (honest version)
&lt;/h2&gt;

&lt;p&gt;Let me be straight before the cost section, because "self-hosting is cheaper" as a blanket claim is false and you'd be right to call it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For most low-volume projects, a hosted image API is the correct choice.&lt;/strong&gt; You skip GPU ops entirely, you pay per image, and at a few hundred generations a month the math favors the API — my setup would lose that comparison. If that's you, wrap the API and ship.&lt;/p&gt;

&lt;p&gt;Self-hosting wins in three specific situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume.&lt;/strong&gt; A hosted API's per-image price has the vendor's margin baked in — that margin is your cost floor, forever, on every image. Self-hosted, your unit cost is raw GPU-seconds; it doesn't inflate as you grow. Somewhere in the thousands-of-images-a-month range the lines cross, and past it the gap only widens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control.&lt;/strong&gt; The vendor picks your model, your price, your rate limits, and what content gets filtered — and can change any of them under you. Self-hosted, an upstream deprecation or price hike is someone else's news.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle cost.&lt;/strong&gt; Serverless GPU that scales to zero means a quiet app costs ~nothing to keep alive, which is what makes a side project survivable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade is real: you take on packaging, cold starts, and ops. The rest of this section is how to make that trade cheap. (A follow-up post works through the actual break-even math with public prices — API per-image rates vs GPU-seconds — so you can find your own crossover point.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it cheap: serverless GPU, scale-to-zero, bake the model
&lt;/h2&gt;

&lt;p&gt;Self-hosting the pipeline on serverless GPU (I run RunPod) means you pay GPU-seconds only while a job runs, and workers &lt;strong&gt;scale to zero&lt;/strong&gt; when idle.&lt;/p&gt;

&lt;p&gt;The catch is &lt;strong&gt;cold start&lt;/strong&gt;. When a worker wakes up, it has to get the model into VRAM before it can do anything. Three things cut that down hard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bake the model into the container image.&lt;/strong&gt; Downloading tens of GB of weights at boot is where cold starts go to die. Weights in the image = pull once, cached on the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slim the runtime.&lt;/strong&gt; Build toolchains and dev dependencies don't belong in an inference image. Every GB you remove is faster pulls and faster boots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model per endpoint.&lt;/strong&gt; A fat "does everything" image pays everyone's cold start. Per-endpoint images only load what that endpoint runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic — it's just deliberate packaging. The difference between a hobby deployment and one you can put real users on is mostly these three decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the transport layer (free, zero infra)
&lt;/h2&gt;

&lt;p&gt;I extracted the whole async transport — queue, worker, polling, serving — into a free slice you can run on a laptop in about two minutes: &lt;strong&gt;SQLite instead of Postgres, a stub worker instead of the GPU&lt;/strong&gt;, and the real RunPod dispatch code included read-only so you can see exactly how the live version differs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ownstackhq/ai-image-job-queue
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx prisma db push &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type a prompt, watch the job go &lt;code&gt;queued → processing → completed&lt;/code&gt; while the original request has long since returned. The 2-stage engine itself (the Stage 1/Stage 2 logic above, running on real GPU) lives in the full kit at &lt;a href="https://ownstackhq.com" rel="noopener noreferrer"&gt;ownstackhq.com&lt;/a&gt;, along with the deploy scripts and the cost-tuning chapter.&lt;/p&gt;

&lt;p&gt;I'd genuinely like to hear where this approach breaks for your use case — especially if you've pushed reference-edit consistency further than twenty scenes, or found a case where per-character LoRA is still worth the training farm.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not just reuse a seed?&lt;/strong&gt;&lt;br&gt;
A seed fixes the sampling noise, so the same prompt reproduces the same image. Change the prompt to get a new scene and the whole denoising path changes with it — including the face. Seeds give reproducibility for identical inputs, not identity across different ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not train a LoRA per character?&lt;/strong&gt;&lt;br&gt;
LoRA holds identity well, but it's a training job per character: dataset curation, GPU training time, and adapter storage, repeated for every character your users create. Reference-based editing needs one generated image and no training, which is what makes it viable inside an app rather than a workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you keep GPU costs down?&lt;/strong&gt;&lt;br&gt;
Serverless GPU workers that scale to zero, model weights baked into the container image (no boot-time downloads), a slim runtime image, and one model per endpoint. Idle costs approach zero and cold starts drop dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work for non-human characters?&lt;/strong&gt;&lt;br&gt;
Yes. The pipeline conditions on a reference image, not on a human-specific representation — the same cast-then-edit loop holds a consistent animal, robot, or anime character across scenes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
