<?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>Forem: Sergey Shmakov</title>
    <description>The latest articles on Forem by Sergey Shmakov (@sergeyshmakov).</description>
    <link>https://forem.com/sergeyshmakov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F377612%2F8f5facd4-874b-435e-92f4-05e50e4b722c.jpeg</url>
      <title>Forem: Sergey Shmakov</title>
      <link>https://forem.com/sergeyshmakov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sergeyshmakov"/>
    <language>en</language>
    <item>
      <title>How RunPod FlashBoot Actually Works (4-Request Test)</title>
      <dc:creator>Sergey Shmakov</dc:creator>
      <pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/sergeyshmakov/how-runpod-flashboot-actually-works-4-request-test-49of</link>
      <guid>https://forem.com/sergeyshmakov/how-runpod-flashboot-actually-works-4-request-test-49of</guid>
      <description>&lt;p&gt;&lt;em&gt;Last Updated: 2026-05-27&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re shipping vLLM or any heavy ML model on RunPod Serverless, you’ve probably looked at FlashBoot, ticked the checkbox, and then watched your cold starts still take 60-120 seconds. RunPod’s marketing says “1-second cold starts.” Their docs describe FlashBoot as “pre-loading container images.” Neither of those matches what most ML workloads actually see.&lt;/p&gt;

&lt;p&gt;I ran four cold-start tests on a deployed RunPod endpoint serving a vLLM-backed PDF parser. The wall-clock numbers ranged from 7 seconds to 7 minutes. The point of this post is to explain &lt;em&gt;why&lt;/em&gt; — what FlashBoot actually does at the systems level, when it kicks in, and how to set up your worker so it kicks in more often.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does FlashBoot actually do?
&lt;/h2&gt;

&lt;p&gt;FlashBoot is a CRIU-style process snapshot mechanism. When a worker scales to zero, RunPod captures the full process state (Python interpreter, CUDA VRAM, subprocess tree) into a snapshot on the host’s local storage. When the worker scales back up &lt;em&gt;on the same host&lt;/em&gt;, RunPod restores from that snapshot. The restored process resumes mid-stride: model still in VRAM, vLLM engine subprocess still alive, IPC pipes still connected.&lt;/p&gt;

&lt;p&gt;The key qualifier that RunPod’s docs don’t mention: &lt;strong&gt;snapshots are per (host, image SHA), not per endpoint&lt;/strong&gt;. If the next scale-from-zero lands on a different host, there’s no snapshot to restore from. The worker boots fresh and pays the full warmup cost. Once.&lt;/p&gt;

&lt;p&gt;The TL;DR for an ML workload: &lt;strong&gt;set up an eager warmup at worker boot, then let FlashBoot do its thing.&lt;/strong&gt; Each new host pays the warmup tax once. Subsequent scale-from-zeroes on that same host get the snapshot restore and finish a typical request in single-digit seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do “cold” starts sometimes take 7 seconds and sometimes 110?
&lt;/h2&gt;

&lt;p&gt;Because they’re hitting different parts of the per-host model. Four consecutive requests against the same endpoint, single-page parse on each, with a deliberate scale-to-zero between every one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Wall-clock&lt;/th&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Snapshot?&lt;/th&gt;
&lt;th&gt;What the worker did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;456 s&lt;/td&gt;
&lt;td&gt;A (post-rebuild)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Image pull + fitness checks + warmup (101 s) + parse (5.6 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.6 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A (same as R1)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Snapshot restore + parse (4.7 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;122 s&lt;/td&gt;
&lt;td&gt;B (different host)&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Fitness checks + warmup (101.5 s) + parse (5.6 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;B (same as R3)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Snapshot restore + parse (4.6 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;First hit on a fresh host pays ~110 s for the warmup. Every subsequent restore on that same host is ~7-8 s.&lt;/strong&gt; A new host, when RunPod’s scheduler picks one, starts the cycle over.&lt;/p&gt;

&lt;p&gt;The 456 s on Request 1 included a one-time image pull (the worker image is ~27 GB; this was the first time that physical host had ever seen it). Strip that off and you get ~110 s of actual boot work, which matches Request 3 exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can you tell if a request hit a snapshot restore?
&lt;/h2&gt;

&lt;p&gt;By what’s &lt;em&gt;missing&lt;/em&gt; from the worker logs. A FlashBoot-restored worker skips its boot sequence entirely — no fitness checks, no Python import logs, no vLLM engine initialization, no model load. The first log line is &lt;code&gt;Jobs in queue: 1&lt;/code&gt;, immediately followed by your handler’s “starting job” entry.&lt;/p&gt;

&lt;p&gt;Compare a fresh boot to a snapshot restore for the same request shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh boot (Request 3):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04:45:45 Running 7 fitness check(s)...
04:45:46 All fitness checks passed. (1285.99ms)
04:45:46 [mineru-warmup] starting (backend=vlm-auto-engine ...)
04:45:51 Using vllm-async-engine as the inference engine for VLM.
04:46:23 Initializing a V1 LLM engine (v0.11.2) ...
04:46:47 Model loading took 2.1601 GiB memory and 18.41 seconds
04:47:14 torch.compile takes 22.81 s in total
04:47:17 init engine (profile, create kv cache, warmup model) took 30.66 seconds
04:47:18 get vllm-async-engine predictor cost: 87.26s
04:47:28 [mineru-warmup] done in 101.5s
04:47:28 Jobs in queue: 1
04:47:28 Started.
04:47:28 "starting job" {...}
04:47:34 "done" {...elapsed_seconds: 5.58...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Snapshot restore (Request 4):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04:51:25 Jobs in queue: 1
04:51:25 Started.
04:51:25 "starting job" {...}
04:51:26 Using vllm-async-engine ... (instant — engine handle restored from snapshot)
04:51:30 "done" {...elapsed_seconds: 4.58...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No boot sequence. Three timestamps. The vLLM engine subprocess PID from the previous boot is reused — same &lt;code&gt;EngineCore_DP0 pid=NNN&lt;/code&gt; from the snapshot. If you grep your own worker logs for the gap between &lt;code&gt;Jobs in queue: 1&lt;/code&gt; and the previous activity, you’ll see whether RunPod did a fresh boot or a snapshot restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the FlashBoot snapshot preserve?
&lt;/h2&gt;

&lt;p&gt;Everything that lived in the worker process at snapshot time, mediated by CRIU semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python interpreter state.&lt;/strong&gt; Module imports stay loaded. Globals (job counters, contextvars, signal handlers) keep their values. The &lt;code&gt;MinerU&lt;/code&gt; engine registry returns the same handles it returned before the snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU VRAM.&lt;/strong&gt; Model weights (~2.16 GiB for our VLM), vLLM’s KV cache (~8.17 GiB on a 24 GB card), and captured CUDA graphs (~0.3 GiB) all survive. The first request after restore parses with the same allocations it had before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The subprocess tree.&lt;/strong&gt; vLLM runs its engine in a child process for memory isolation. That subprocess gets captured along with the parent and restored with its IPC pipes intact. The engine PID persists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;torch.compile&lt;/code&gt; cache.&lt;/strong&gt; The JIT-compiled Dynamo / Inductor output stays valid across restore. No 22-second recompile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn’t survive: snapshot lifetime is limited. RunPod doesn’t publish the eviction policy, but obvious triggers include image rebuild (new SHA invalidates the snapshot), and presumably long enough idle on a busy host that the snapshot storage gets pushed out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke before this worked? The asyncio gotcha
&lt;/h2&gt;

&lt;p&gt;The “eager warmup at boot” idea is obvious in principle: run one throwaway parse during worker startup so the model is loaded and warm before any user request arrives. The implementation has one trap.&lt;/p&gt;

&lt;p&gt;vLLM’s &lt;code&gt;AsyncLLMEngine&lt;/code&gt; binds its IPC primitives (transports, queues) to the asyncio event loop that initialized it. If you call &lt;code&gt;asyncio.run(warmup())&lt;/code&gt; followed by &lt;code&gt;runpod.serverless.start()&lt;/code&gt;, your warmup creates loop A, runs the parse, then tears loop A down when &lt;code&gt;asyncio.run&lt;/code&gt; returns. Then &lt;code&gt;runpod.serverless.start()&lt;/code&gt; creates loop B for serving. When the first user request tries to talk to the vLLM engine through loop B, the engine handle is bound to the now-dead loop A. Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"error_type": "EngineDeadError",
"error_message": "EngineCore encountered an issue. See stack trace (above) for the root cause."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine subprocess itself is still alive. It’s only the parent process’s IPC reference that’s broken.&lt;/p&gt;

&lt;p&gt;The fix is to keep the warmup and the serve loop on the same asyncio event loop. RunPod’s &lt;code&gt;runpod.serverless.start()&lt;/code&gt; internally calls &lt;code&gt;asyncio.run(JobScaler.run())&lt;/code&gt;, but &lt;code&gt;JobScaler&lt;/code&gt; (in &lt;code&gt;runpod.serverless.modules.rp_scale&lt;/code&gt;) is constructible directly and its &lt;code&gt;run()&lt;/code&gt; is an awaitable coroutine. So you can compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import asyncio

from runpod.serverless.modules import rp_ping, rp_scale

from runpod.serverless.modules.rp_fitness import run_fitness_checks

config = {"handler": handler, "concurrency_modifier": _concurrency_modifier, "rp_args": {}}

async def _bootstrap():

    await run_fitness_checks()

    await warmup_async() # &amp;lt;- engine binds to THIS loop

    rp_ping.Heartbeat().start_ping()

    await rp_scale.JobScaler(config).run() # &amp;lt;- and stays here

asyncio.run(_bootstrap())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now both phases share one event loop. The engine handle stays valid across the warmup → serve transition. FlashBoot captures a snapshot of a process where the loop, the engine, and the IPC are all alive together. On restore, they come back together too.&lt;/p&gt;

&lt;p&gt;This does reach into runpod-python’s internals (the &lt;code&gt;runpod.serverless.modules.*&lt;/code&gt; submodules aren’t part of the documented public API). Cheap to guard against drift: a unit test that asserts &lt;code&gt;JobScaler&lt;/code&gt; exists with the expected constructor and an awaitable &lt;code&gt;run()&lt;/code&gt; method. If RunPod refactors, CI catches it before production does.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does the warmup pay off and when doesn’t it?
&lt;/h2&gt;

&lt;p&gt;Per host, not per endpoint. The math depends on your traffic pattern.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Likely outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;workers_min ≥ 1&lt;/code&gt; (always-on worker)&lt;/td&gt;
&lt;td&gt;Worker stays on its host. Every request is on a fully warm worker (~5 s parse). No cold starts at all.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-frequency endpoint, workers scale up and down fast&lt;/td&gt;
&lt;td&gt;Same hosts get re-selected. Most cold starts are happy-path restores (~7 s).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiet endpoint, infrequent requests, long idle gaps&lt;/td&gt;
&lt;td&gt;RunPod’s scheduler may pick a different host. Some cold starts will be on new hosts (~110 s).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First request after a rebuild&lt;/td&gt;
&lt;td&gt;Always cold path. Every endpoint’s first request after a fresh image pays ~5-7 min (image pull) + ~110 s (warmup). One-time per worker host.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MINERU_SKIP_WARMUP=1&lt;/code&gt; (warmup off)&lt;/td&gt;
&lt;td&gt;Every cold start is ~110-130 s. No per-host amortization. Don’t do this in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The case that stings is “quiet endpoint with sporadic traffic” — a few requests an hour, 10-minute idle gaps, RunPod bouncing between hosts. Without warmup, every cold start would be ~110-130 s. With warmup, you get a mix: some 7-second restores, some 110-second fresh boots. The mix tilts toward fast as the endpoint warms up across more hosts and RunPod’s scheduler starts re-selecting them.&lt;/p&gt;

&lt;p&gt;If your traffic is sustained enough that you can pin a worker (&lt;code&gt;workers_min=1&lt;/code&gt;), you skip the entire question. You’re paying for the GPU 24/7 but never paying a cold start. For workloads with even modest cost sensitivity, the warmup + FlashBoot path is the better trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you’re shipping vLLM on RunPod
&lt;/h2&gt;

&lt;p&gt;Three takeaways from the live measurements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always set up an eager warmup at worker boot.&lt;/strong&gt; Loading the model on first request is silently worse than it sounds — you don’t just pay 110 s once per cold start, you pay it every time a host doesn’t have a snapshot, AND you forfeit the per-host amortization that makes the second-hit-on-a-host cheap. Without warmup, FlashBoot has nothing to snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose warmup and the serving loop under one &lt;code&gt;asyncio.run()&lt;/code&gt;.&lt;/strong&gt; If you &lt;code&gt;asyncio.run()&lt;/code&gt; the warmup separately, the engine dies at the loop boundary. The fix is straightforward but the failure mode is opaque (&lt;code&gt;EngineDeadError&lt;/code&gt; 75 ms into the first request) — easy to misdiagnose as a vLLM bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t market your cold start as “X seconds” without acknowledging the per-host mix.&lt;/strong&gt; A snapshot-restore cold start is genuinely 7-8 seconds. A new-host cold start is ~110 s. Both are big improvements over the no-warmup baseline (~110-130 s per request, every request). But your users will see the mix, and a too-clean claim makes the bad days look broken.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole investigation was on a 24 GB A5000 / RTX 4090 class GPU running MinerU’s 1.2B VLM via vLLM 0.11.2. The numbers will shift on larger models (more VRAM to snapshot, longer model load on cold path) but the mechanism applies the same way. If your cold start dominates wall-clock latency on a serverless GPU workload, set up boot-time warmup, watch the worker logs for the snapshot pattern, and tune your &lt;code&gt;workers_min&lt;/code&gt; accordingly.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Does FlashBoot snapshot the vLLM engine subprocess?
&lt;/h3&gt;

&lt;p&gt;Yes. The vLLM engine runs as a child process for memory isolation, and FlashBoot’s CRIU-style mechanism captures the full process tree including subprocesses. The engine’s PID persists across snapshot/restore, and its IPC pipes back to the parent stay connected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my cold start take 60-120 seconds even with FlashBoot enabled?
&lt;/h3&gt;

&lt;p&gt;Most likely your model is being loaded lazily on first request rather than at worker boot. FlashBoot only snapshots state that already exists in the worker process when it scales to zero. If your model loads on first request, the snapshot captures a worker without the model, and every cold start has to load the model again. Move the model load to worker boot (before &lt;code&gt;runpod.serverless.start()&lt;/code&gt;) and FlashBoot will start carrying the warm state forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the difference between FlashBoot and a network volume?
&lt;/h3&gt;

&lt;p&gt;A network volume is shared file storage attached to your worker (e.g., for model weights you don’t want to bake into the Docker image). FlashBoot is process-state preservation — it captures the running Python process, including data already loaded from disk into VRAM. They solve different problems and can be used together: a network volume avoids re-downloading model files on image pull; FlashBoot avoids re-loading them into VRAM on cold start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does FlashBoot work for non-GPU workloads?
&lt;/h3&gt;

&lt;p&gt;The mechanism (process snapshot via CRIU or equivalent) doesn’t depend on GPU memory specifically. CPU-bound workloads with significant cold-start cost (heavy library imports, large in-memory indices, JIT compilation) should benefit similarly. The framing in this post happens to use a GPU workload because that’s where the cold-start tax is most painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know if my worker is hitting a snapshot restore vs a fresh boot?
&lt;/h3&gt;

&lt;p&gt;Check the worker logs in the RunPod dashboard. A fresh boot shows fitness checks, framework init logs, and any warmup output. A snapshot restore is silent until the first &lt;code&gt;Jobs in queue: 1&lt;/code&gt; line, then jumps straight to your handler’s request-processing logs. The presence or absence of the boot sequence is the cleanest signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is FlashBoot the same as RunPod’s “Active Workers” tier?
&lt;/h3&gt;

&lt;p&gt;No. Active Workers are a billing tier where you pre-commit to a number of workers that are always on, billed at a discount in exchange for the 24/7 commitment. FlashBoot is a free runtime optimization that applies to flex (scale-to-zero) workers. The two can be combined: an Active Worker on the same endpoint can also benefit from FlashBoot when it cycles, though for a worker that never goes idle there’s nothing to snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will FlashBoot survive a Docker image rebuild?
&lt;/h3&gt;

&lt;p&gt;No. Each image gets its own SHA, and FlashBoot snapshots are scoped to (host, image SHA). When you push a new image, all existing snapshots are invalid. The first request after a rebuild on any host pays the full cold-start cost (image pull + warmup). Once each host has served the new image once, subsequent restores work normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;runpod-mineru&lt;/code&gt; repo wraps all of this into one Docker image: MinerU 3.2.x + the &lt;code&gt;MinerU2.5-Pro-2605-1.2B&lt;/code&gt; VLM, the JobScaler-bypass composition for warmup, structured logging, and the rest. Open source (&lt;a href="https://github.com/sergeyshmakov/mineru-runpod" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;), MIT-licensed, deploys from the RunPod Hub in two clicks.&lt;/p&gt;

&lt;p&gt;If you want the deeper breakdown of which phases of a cold start cost what, the &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/troubleshooting/#flashboot-mechanism-confirmed" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt; has the per-phase timing table from the same test runs. The &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/scaling/#flashboot" rel="noopener noreferrer"&gt;scaling guide&lt;/a&gt; covers when to pair FlashBoot with &lt;code&gt;workers_min ≥ 1&lt;/code&gt; for fully predictable latency.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; RunPod links in this post use a referral code that credits me at no cost to you. The post would read the same without it.&lt;/small&gt;&lt;/p&gt;

</description>
      <category>runpod</category>
      <category>flashboot</category>
      <category>serverless</category>
      <category>vllm</category>
    </item>
    <item>
      <title>RunPod 20 MB Response Cap: Fix NoneType with Cloudflare R2</title>
      <dc:creator>Sergey Shmakov</dc:creator>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/sergeyshmakov/runpod-20-mb-response-cap-fix-nonetype-with-cloudflare-r2-48la</link>
      <guid>https://forem.com/sergeyshmakov/runpod-20-mb-response-cap-fix-nonetype-with-cloudflare-r2-48la</guid>
      <description>&lt;p&gt;&lt;em&gt;Last Updated: 2026-05-27&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your RunPod serverless worker logs say &lt;code&gt;done&lt;/code&gt; but your client raises &lt;code&gt;unexpected handler return type: &amp;lt;class 'NoneType'&amp;gt;&lt;/code&gt;, you’ve hit RunPod’s bidirectional 20 MB payload cap on &lt;code&gt;/runsync&lt;/code&gt;. The handler succeeded. The gateway dropped the response on the way back because the payload was too large.&lt;/p&gt;

&lt;p&gt;The fix is two steps. Set &lt;code&gt;return: "s3"&lt;/code&gt; on the job, and configure four env vars on the endpoint pointing at a Cloudflare R2 bucket. The worker uploads the result to R2 and returns a small presigned URL. Your client downloads from R2 directly. No gateway cap in the path.&lt;/p&gt;

&lt;p&gt;I hit this on an 82-page Cyrillic fiscal report (30 MB input, ~25 MB output with embedded images) running my open-source &lt;a href="https://github.com/sergeyshmakov/mineru-runpod" rel="noopener noreferrer"&gt;mineru-runpod&lt;/a&gt; template. Two retries via &lt;code&gt;return: "inline"&lt;/code&gt; and &lt;code&gt;return: "tarball_b64"&lt;/code&gt; failed the same way. R2 mode worked first try. The rest of this post is the symptom, the env-var recipe, the cost comparison vs S3, and a few gotchas worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does my RunPod worker return NoneType after a successful parse?
&lt;/h2&gt;

&lt;p&gt;The worker handler completed and returned a valid dict. RunPod’s runtime then tried to POST that result back to RunPod’s API via &lt;code&gt;/job-done&lt;/code&gt;, and the API returned HTTP 400 because the payload exceeded ~20 MB. The result was discarded. The SDK saw no output, returned &lt;code&gt;None&lt;/code&gt; to the client, and the client wrapper raised the &lt;code&gt;NoneType&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;The worker logs make the chain explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[mineru-worker] done: elapsed=91.77s phase_ms={'fetch_input': 972, 'mineru_parse': 90789, 'package': 66}
{"requestId": "sync-fdcd03cd-...", "message": "Failed to return job results. | 400, message='Bad Request',

 url='https://api.runpod.ai/v2/&amp;lt;endpoint&amp;gt;/job-done/&amp;lt;worker&amp;gt;/sync-fdcd03cd-...?gpu=NVIDIA+RTX+A5000&amp;amp;isStream=false'"}

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

&lt;/div&gt;



&lt;p&gt;The first line shows the handler finished cleanly: 82 pages parsed in 91.8 s on the worker (this test ran on A5000; on the current 4090 default the warm parse is 2–3× faster). The second line shows the gateway rejecting the result. The handler already returned and never knows the rejection happened. The SDK sees the discarded result and returns &lt;code&gt;None&lt;/code&gt; to your code.&lt;/p&gt;

&lt;p&gt;If you see this &lt;code&gt;NoneType&lt;/code&gt; error on a small doc, the diagnosis is different (worker OOM, crash, timeout). On a multi-page parse that the worker logs as &lt;code&gt;done&lt;/code&gt;, the answer is almost always the 20 MB cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is RunPod’s /runsync response payload limit?
&lt;/h2&gt;

&lt;p&gt;RunPod’s &lt;code&gt;/runsync&lt;/code&gt; gateway caps payloads at roughly &lt;strong&gt;20 MB in both directions&lt;/strong&gt;. The request cap affects &lt;code&gt;file_b64&lt;/code&gt; inline uploads. The response cap affects what the worker can return. Both are independent of execution time and memory budget. A fast, successful parse can hit the response cap simply by producing a large output.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;What triggers it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request → gateway → worker&lt;/td&gt;
&lt;td&gt;~20 MB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file_b64&lt;/code&gt; inline transport for large PDFs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker → gateway → client&lt;/td&gt;
&lt;td&gt;~20 MB&lt;/td&gt;
&lt;td&gt;Multi-page parse outputs with embedded images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The request cap is in &lt;a href="https://docs.runpod.io/serverless/endpoints/operations" rel="noopener noreferrer"&gt;RunPod’s docs&lt;/a&gt; and widely discussed. The response cap is mentioned only in passing. I found three open issues on the runpod-workers repos where other users hit the same symptom and didn’t realise what it was, so this post is partly to make that searchable.&lt;/p&gt;

&lt;p&gt;Practical threshold for mineru-runpod: pure-text PDFs are fine for longer. Image-heavy PDFs with embedded raster output hit the response cap around 50–80 pages on &lt;code&gt;inline&lt;/code&gt; or &lt;code&gt;tarball_b64&lt;/code&gt; transport.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does &lt;code&gt;return: "tarball_b64"&lt;/code&gt; get around the 20 MB cap?
&lt;/h2&gt;

&lt;p&gt;No. &lt;code&gt;return: "tarball_b64"&lt;/code&gt; gzips the output into a single .tar.gz before base64-encoding it. Gzip compresses the JSON and Markdown text well, but the page images inside the tarball are already raster bytes (PNG, JPEG) and barely compress further. Multi-page parses with embedded images keep the tarball over 20 MB.&lt;/p&gt;

&lt;p&gt;I confirmed this on the same 82-page PDF. Same 400 from &lt;code&gt;/job-done&lt;/code&gt;. Same &lt;code&gt;NoneType&lt;/code&gt; in the client. Both &lt;code&gt;inline&lt;/code&gt; and &lt;code&gt;tarball_b64&lt;/code&gt; route through the gateway response, so both inherit the cap. Only &lt;code&gt;return: "s3"&lt;/code&gt; avoids it because the worker uploads out-of-band.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I configure Cloudflare R2 to bypass the RunPod response cap?
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;return: "s3"&lt;/code&gt; in the job input, then add four env vars on the RunPod endpoint pointing at a &lt;a href="https://developers.cloudflare.com/r2/" rel="noopener noreferrer"&gt;Cloudflare R2&lt;/a&gt; bucket. The worker uploads the gzipped tarball directly to R2 and returns a small presigned URL (~1 h TTL). Your client downloads from R2.&lt;/p&gt;

&lt;p&gt;The job input changes one field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{

  "input": {

    "file_url": "https://example.com/big.pdf",

    "return": "s3"

  }

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

&lt;/div&gt;



&lt;p&gt;The four env vars go on the &lt;strong&gt;endpoint&lt;/strong&gt; (not the template — they’re secrets):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Env var&lt;/th&gt;
&lt;th&gt;Cloudflare R2 value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BUCKET_ENDPOINT_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://&amp;lt;account-id&amp;gt;.r2.cloudflarestorage.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BUCKET_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your bucket name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BUCKET_ACCESS_KEY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;R2 API token access key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BUCKET_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;R2 API token secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BUCKET_REGION&lt;/code&gt; &lt;em&gt;(optional)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You generate the access key pair in the Cloudflare dashboard: &lt;strong&gt;R2 → Manage R2 API Tokens → Create API Token → Object Read &amp;amp; Write scoped to the bucket&lt;/strong&gt;. The worker auto-restarts when you save endpoint env vars in RunPod. Test with one small doc before sending production traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why pick Cloudflare R2 over AWS S3 for RunPod output storage?
&lt;/h2&gt;

&lt;p&gt;R2 has &lt;strong&gt;zero egress fees&lt;/strong&gt; , a 10 GB free storage tier, 1M Class A ops and 10M Class B ops per month free, and is fully S3-compatible. AWS S3 charges egress at roughly $0.085/GB plus storage at $0.023/GB/month. For a RunPod pipeline doing dozens of GB of I/O per month, R2’s bill stays near zero while S3 lands in the $5–$15 range.&lt;/p&gt;

&lt;p&gt;A back-of-envelope month for the workload I tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000 multi-page parses, average output 8 MB → 8 GB stored then deleted&lt;/li&gt;
&lt;li&gt;1,000 worker→bucket uploads + 1,000 client→bucket downloads = 2,000 ops&lt;/li&gt;
&lt;li&gt;Storage: free (under 10 GB). Egress: free (R2 doesn’t bill egress). Ops: free (well under 1M Class A).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same workload on S3: ~$0.18 storage + ~$0.68 egress + per-request fees, maybe $1–$3 total. Cheap but R2’s $0 is cheaper.&lt;/p&gt;

&lt;p&gt;S3 still makes sense if you’re already deep in AWS, if you need IAM-controlled access patterns, or if RunPod workers and your AWS region are colocated tightly enough that egress doesn’t apply. For everyone else and especially for solo / indie deploys, R2 is the right default. See &lt;a href="https://developers.cloudflare.com/r2/pricing/" rel="noopener noreferrer"&gt;R2 pricing&lt;/a&gt; for current rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the parse flow look like end-to-end with &lt;code&gt;return: "s3"&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;The worker fetches the input PDF, runs MinerU, gzips the outputs into a tarball, uploads to R2 via the configured &lt;code&gt;BUCKET_*&lt;/code&gt; env vars, and returns a small JSON response with &lt;code&gt;tarball_url&lt;/code&gt;, &lt;code&gt;tarball_url_expires_in&lt;/code&gt; (3600 s), and &lt;code&gt;bucket_key&lt;/code&gt;. Your client follows the URL and extracts the tarball locally. No payload ever crosses RunPod’s 20 MB-capped response path.&lt;/p&gt;

&lt;p&gt;Concrete numbers from the 82-page test (on A5000; current default is 4090):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
result = client.parse_document(

    file_url="https://pub-....r2.dev/report.pdf",

    backend="vlm-auto-engine",

    return_format="s3",

)
# result["tarball_url"] -&amp;gt; presigned R2 URL, valid ~1 h
# result["tarball_url_expires_in"] -&amp;gt; 3600
# result["bucket_key"] -&amp;gt; "report-&amp;lt;hash&amp;gt;.tar.gz"

client.save_s3_tarball(result, "./out/")

# downloads + extracts -&amp;gt; out/report.md, out/report_content_list_v2.json, out/images/, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;End-to-end wall-clock: 211 s for an 82-page doc on a cold worker. Breakdown: ~112 s before MinerU started parsing (worker boot + warmup), ~92 s warm parsing (1.1 s/page on A5000), ~11 s gzip and upload to R2 (the &lt;code&gt;package&lt;/code&gt; phase). The extracted output: 313 KB Markdown plus structured JSON plus per-page images. Roughly 3.5 minutes for a document that previously couldn’t return its output at all.&lt;/p&gt;

&lt;p&gt;The cold-start portion is a separate concern from the response cap. &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-flashboot-mechanism-investigation/" rel="noopener noreferrer"&gt;The FlashBoot mechanism investigation&lt;/a&gt; covers why the ~112 s exists, how the boot-time warmup interacts with RunPod’s snapshot system, and when subsequent cold starts are much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should I watch out for with the R2 bridge?
&lt;/h2&gt;

&lt;p&gt;Four things the docs don’t say loudly. The presigned URL TTL is 60 minutes. R2 doesn’t auto-clean uploaded objects. One bucket can serve input and output. The 20 MB cap applies to &lt;code&gt;/run&lt;/code&gt; (async) too, not just &lt;code&gt;/runsync&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Presigned URL TTL is 60 minutes.&lt;/strong&gt; If your client is slow to download (e.g. a job-queue worker that picks up results minutes later), bump &lt;code&gt;_S3_PRESIGN_TTL_SECONDS&lt;/code&gt; in the handler. Don’t rely on the default in long-tail flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R2 doesn’t auto-clean uploaded objects.&lt;/strong&gt; Add an &lt;a href="https://developers.cloudflare.com/r2/buckets/object-lifecycles/" rel="noopener noreferrer"&gt;R2 lifecycle rule&lt;/a&gt; (e.g. delete after 7 days) so your output bucket doesn’t grow forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One R2 bucket can serve input and output.&lt;/strong&gt; Upload your PDFs to R2 ahead of time, pass &lt;code&gt;file_url&lt;/code&gt; pointing at the R2 public dev URL, and the worker writes outputs to the same bucket at the root. Add &lt;code&gt;BUCKET_PREFIX&lt;/code&gt; env var if you want outputs in a subdirectory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 20 MB cap applies to &lt;code&gt;/run&lt;/code&gt; (async) too.&lt;/strong&gt; Same gateway, same limit. Switching to async polling doesn’t help.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  How do I get the R2 access key for &lt;code&gt;BUCKET_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;BUCKET_SECRET_ACCESS_KEY&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;In the Cloudflare dashboard: &lt;strong&gt;R2 → Manage R2 API Tokens → Create API Token&lt;/strong&gt;. Set permissions to “Object Read &amp;amp; Write” scoped to the specific bucket. Cloudflare shows the access key ID and secret access key once; copy both into your RunPod endpoint env vars immediately. The secret isn’t retrievable later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the presigned URL expire?
&lt;/h3&gt;

&lt;p&gt;Yes. The default TTL is 3600 seconds (one hour). If your downstream client picks up the response asynchronously (job queue, cron, etc.), download promptly or bump &lt;code&gt;_S3_PRESIGN_TTL_SECONDS&lt;/code&gt; in the worker handler before redeploying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I reuse the same R2 bucket for input and output?
&lt;/h3&gt;

&lt;p&gt;Yes. The worker doesn’t care about the bucket layout. Upload your input PDFs to &lt;code&gt;bucket/inputs/&lt;/code&gt; and the worker writes outputs to &lt;code&gt;bucket/&amp;lt;basename&amp;gt;-&amp;lt;hash&amp;gt;.tar.gz&lt;/code&gt; at the root. Add &lt;code&gt;BUCKET_PREFIX&lt;/code&gt; env var if you want outputs pushed into a subdirectory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if I can’t set up R2? Is there a fallback?
&lt;/h3&gt;

&lt;p&gt;Page chunking. Split the parse with &lt;code&gt;start_page&lt;/code&gt; and &lt;code&gt;end_page&lt;/code&gt; into segments small enough that each output tarball stays under 20 MB, then concatenate the &lt;code&gt;.md&lt;/code&gt; files client-side. Slower (you may pay multiple cold starts if the worker scales to zero between calls) and you handle joining yourself, but no infra changes needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the 20 MB cap on &lt;code&gt;/run&lt;/code&gt; too, or only &lt;code&gt;/runsync&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Both. RunPod’s &lt;code&gt;/run&lt;/code&gt; (async) and &lt;code&gt;/runsync&lt;/code&gt; (synchronous) share the same gateway and the same payload limits. Switching to async doesn’t help the response-size problem. The cap is at the gateway layer, not the polling protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does using &lt;code&gt;return: "s3"&lt;/code&gt; add to cold-start time?
&lt;/h3&gt;

&lt;p&gt;No. The S3 upload happens at the end of the parse, not the beginning. The handler’s &lt;code&gt;package&lt;/code&gt; phase grew from ~95 ms (in-memory tarball) to ~11 s (gzip + upload to R2) on an 82-page job, but cold-start is unchanged. The S3 mode adds a small constant to warm-job latency, not a multiplier.&lt;/p&gt;

&lt;h3&gt;
  
  
  How big can the R2-uploaded tarball be?
&lt;/h3&gt;

&lt;p&gt;Effectively unlimited for mineru-runpod workloads. R2 supports multipart uploads up to 5 TB per object. You’ll hit the worker’s &lt;code&gt;executionTimeoutMs&lt;/code&gt; long before you hit R2’s per-object limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does R2 work for input PDFs too, or only output?
&lt;/h3&gt;

&lt;p&gt;Both. The worker accepts &lt;code&gt;file_url&lt;/code&gt; pointing at an R2 public dev URL (or a presigned R2 GET URL for private buckets) and fetches the input from R2. This avoids the inbound 20 MB cap on &lt;code&gt;file_b64&lt;/code&gt; for large PDFs. You can run an R2-in / R2-out setup with one bucket and avoid every payload-size limit RunPod has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to next
&lt;/h2&gt;

&lt;p&gt;If you’ve shipped a multi-page PDF pipeline on RunPod and you’re not using &lt;code&gt;return: "s3"&lt;/code&gt;, you’ll hit the gateway cap eventually. Set it up before you need it. The cost is ten minutes of env-var configuration and possibly zero dollars per month at indie volumes.&lt;/p&gt;

&lt;p&gt;If you’re new to the template, the &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/getting-started/overview/" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; walks through the full deploy in about ten minutes. For the cold-start side of the picture (separate from the response cap covered here), see &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-flashboot-mechanism-investigation/" rel="noopener noreferrer"&gt;the FlashBoot mechanism investigation&lt;/a&gt;. For GPU sizing, &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/choosing-gpu/" rel="noopener noreferrer"&gt;Choosing a GPU&lt;/a&gt; covers when the default &lt;code&gt;ADA_24&lt;/code&gt; (RTX 4090) is enough and when to opt up.&lt;/p&gt;

&lt;p&gt;If this saved you time, the easiest way to say thanks is &lt;a href="https://runpod.io?ref=31jdfpnq" rel="noopener noreferrer"&gt;signing up for RunPod through this link&lt;/a&gt;. Star the &lt;a href="https://github.com/sergeyshmakov/mineru-runpod" rel="noopener noreferrer"&gt;repo on GitHub&lt;/a&gt; for updates.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; RunPod links in this post use a referral code that credits me at no cost to you. The post would read the same without it.&lt;/small&gt;&lt;/p&gt;

</description>
      <category>runpod</category>
      <category>cloudflarer2</category>
      <category>serverless</category>
      <category>mineru</category>
    </item>
    <item>
      <title>Serverless MinerU on RunPod: honest cost math (2026)</title>
      <dc:creator>Sergey Shmakov</dc:creator>
      <pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate>
      <link>https://forem.com/sergeyshmakov/serverless-mineru-on-runpod-honest-cost-math-2026-3abe</link>
      <guid>https://forem.com/sergeyshmakov/serverless-mineru-on-runpod-honest-cost-math-2026-3abe</guid>
      <description>&lt;p&gt;&lt;em&gt;Last Updated: 2026-05-27&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re building a RAG pipeline, a document indexer, or any product that ingests PDFs at scale, you’ve probably hit the same wall I did. Hosted OCR APIs charge pennies per page that compound into thousands per million. CPU parsers are too slow for production volume. A permanent GPU pod is wasteful when traffic comes in bursts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/opendatalab/MinerU" rel="noopener noreferrer"&gt;MinerU 2.5&lt;/a&gt; is genuinely state-of-the-art for PDF → Markdown / structured JSON. Apache 2.0 license. The &lt;a href="https://huggingface.co/opendatalab/MinerU2.5-Pro-2605-1.2B" rel="noopener noreferrer"&gt;&lt;code&gt;MinerU2.5-Pro-2605-1.2B&lt;/code&gt; model&lt;/a&gt; fits comfortably on a 24 GB GPU. RunPod Serverless scales to zero when nothing is calling. Wiring those two together is the obvious move.&lt;/p&gt;

&lt;p&gt;Real numbers from my open-source &lt;a href="https://github.com/sergeyshmakov/mineru-runpod" rel="noopener noreferrer"&gt;mineru-runpod&lt;/a&gt; template, measured on a 24 GB RTX 4090 in May 2026: &lt;strong&gt;~$0.001 per page for warm parses, plus a ~$0.03 fixed tax per cold start&lt;/strong&gt;. The all-in per-page cost depends on how much work you do before the worker scales back to zero. Here’s the deploy, the response shape, and the workload patterns this template is the right fit for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it actually cost to run MinerU on RunPod Serverless?
&lt;/h2&gt;

&lt;p&gt;About &lt;strong&gt;$0.001 per page&lt;/strong&gt; on an RTX 4090 once the worker is warm. Each scale-from-zero adds a &lt;strong&gt;~$0.03 fixed tax&lt;/strong&gt; : roughly 110 seconds of GPU billing for vLLM engine init plus model load. Per-page math depends entirely on amortization. Sparse traffic with one short request per cold start lands closer to $0.005–$0.01 per page.&lt;/p&gt;

&lt;p&gt;Real workload-shape math using &lt;code&gt;ADA_24&lt;/code&gt; (RTX 4090, ~$1.10/hr Flex):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload shape&lt;/th&gt;
&lt;th&gt;Per-page cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000 pages amortized across one cold start&lt;/td&gt;
&lt;td&gt;~$0.001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 pages amortized across one cold start&lt;/td&gt;
&lt;td&gt;~$0.0013&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 pages then idle out&lt;/td&gt;
&lt;td&gt;~$0.004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One short doc per scale-from-zero (worst case)&lt;/td&gt;
&lt;td&gt;~$0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compared to alternatives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool / setup&lt;/th&gt;
&lt;th&gt;Per-page cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hosted OCR APIs (typical)&lt;/td&gt;
&lt;td&gt;$0.001 – $0.01&lt;/td&gt;
&lt;td&gt;vendor lock-in, rate limits, documents leave your stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permanent GPU pod (24 h on A5000)&lt;/td&gt;
&lt;td&gt;$0.001 – $0.003&lt;/td&gt;
&lt;td&gt;24 h of bills whether you use it or not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;mineru-runpod, amortized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.001 – $0.004&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;scales to zero; cold-start tax is real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marker / Nougat on CPU&lt;/td&gt;
&lt;td&gt;$0 cash, $$$ time&lt;/td&gt;
&lt;td&gt;~30 s/page sequential (&lt;a href="https://github.com/datalab-to/marker" rel="noopener noreferrer"&gt;Marker docs&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trick is RunPod’s per-second billing. No worker running, no bill. The catch is every scale-from-zero pays a real fixed cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I deploy MinerU to RunPod Serverless in ten minutes?
&lt;/h2&gt;

&lt;p&gt;Fork the repo, point RunPod’s GitHub auto-build at your fork, create a Serverless Endpoint with &lt;code&gt;ADA_24&lt;/code&gt; (RTX 4090) and FlashBoot enabled, send a request via the included Python client. Total wall-clock from RunPod sign-up to first parse: roughly ten minutes, dominated by the image build (~5–10 min) plus the first cold start (~110 s).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Get a RunPod account
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://runpod.io?ref=31jdfpnq" rel="noopener noreferrer"&gt;Sign up here&lt;/a&gt;. Add $5 of credit. That covers several thousand cold starts plus a few million warm pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fork the repo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
gh repo fork sergeyshmakov/mineru-runpod --clone

cd mineru-runpod

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

&lt;/div&gt;



&lt;p&gt;The repo stays small. &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;handler.py&lt;/code&gt;, a &lt;code&gt;worker/&lt;/code&gt; package, a Python client (&lt;code&gt;mineru_client&lt;/code&gt;), three GitHub Actions workflows, Hub metadata under &lt;code&gt;.runpod/&lt;/code&gt;. MIT licensed, ~30 files.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Wire RunPod’s GitHub auto-build
&lt;/h3&gt;

&lt;p&gt;In the RunPod dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Serverless → Templates → New → Import Git Repository&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Point at your fork. Branch &lt;code&gt;main&lt;/code&gt;, Dockerfile path &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;RunPod clones, builds the image, stores it in its own registry, and gives you a &lt;code&gt;template_id&lt;/code&gt;. The build runs ~5–10 minutes. Watch the log if you want.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Create the endpoint
&lt;/h3&gt;

&lt;p&gt;Dashboard path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Serverless → Endpoints → New&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Template: the one you just created&lt;/li&gt;
&lt;li&gt;GPU pool: &lt;code&gt;ADA_24&lt;/code&gt; (RTX 4090, 24 GB)&lt;/li&gt;
&lt;li&gt;Workers min: &lt;code&gt;0&lt;/code&gt;, max: &lt;code&gt;3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Idle timeout: &lt;code&gt;10&lt;/code&gt; seconds&lt;/li&gt;
&lt;li&gt;FlashBoot: on&lt;/li&gt;
&lt;li&gt;Save, grab the endpoint id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or as code (reproducible across redeploys):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
pip install -e .[deploy]

python deploy.py --template-id &amp;lt;tid&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deploy.py&lt;/code&gt; exposes every endpoint setting as a CLI flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Parse your first PDF
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
from mineru_client import MineruClient

client = MineruClient(

    endpoint_id="&amp;lt;your-endpoint-id&amp;gt;",

    api_key="&amp;lt;your-runpod-api-key&amp;gt;",

)

result = client.parse_document(

    file_url="https://example.com/report.pdf",

    end_page=4, # smoke test on first 5 pages

)

client.save_tarball(result, "./out/doc")

# → ./out/doc/&amp;lt;basename&amp;gt;.md
# → ./out/doc/&amp;lt;basename&amp;gt;_content_list_v2.json
# → ./out/doc/&amp;lt;basename&amp;gt;_middle.json
# → ./out/doc/images/*.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First parse pays a cold start. Subsequent parses on the same warm worker run at ~1–6 s/page on the 4090, content density dependent. After 10 s of idle, the worker scales to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the MinerU response actually contain?
&lt;/h2&gt;

&lt;p&gt;Three structured outputs plus extracted images. &lt;code&gt;&amp;lt;basename&amp;gt;.md&lt;/code&gt; is Markdown with LaTeX equations, HTML tables, and image references. &lt;code&gt;&amp;lt;basename&amp;gt;_content_list_v2.json&lt;/code&gt; is a flat list of typed entries (text, equation, table, image, code) each tagged with &lt;code&gt;page_idx&lt;/code&gt;. &lt;code&gt;&amp;lt;basename&amp;gt;_middle.json&lt;/code&gt; carries the full layout with bounding boxes and reading order. Pick the transport via &lt;code&gt;return&lt;/code&gt;: &lt;code&gt;tarball_b64&lt;/code&gt;, &lt;code&gt;inline&lt;/code&gt;, or &lt;code&gt;s3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a document indexer or RAG pipeline, &lt;code&gt;content_list_v2.json&lt;/code&gt; is the file you’ll spend the most time with. Group entries by &lt;code&gt;level: "title"&lt;/code&gt; boundaries for section-based chunking. Embed each chunk and store &lt;code&gt;page_idx&lt;/code&gt; for citation back to the source.&lt;/p&gt;

&lt;p&gt;The Markdown is for human-readable display. &lt;code&gt;middle.json&lt;/code&gt; has bounding boxes per span when you need page coordinates for hover-to-source UI.&lt;/p&gt;

&lt;p&gt;Transport options on the request: &lt;code&gt;tarball_b64&lt;/code&gt; (default) for outputs under ~20 MB, &lt;code&gt;inline&lt;/code&gt; if you want the markdown directly in the JSON response, &lt;code&gt;s3&lt;/code&gt; for anything that would exceed RunPod’s response cap. See &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-20mb-response-cap-r2-bridge/" rel="noopener noreferrer"&gt;the R2 bridge post&lt;/a&gt; for the &lt;code&gt;s3&lt;/code&gt; setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does mineru-runpod fit your workload, and when doesn’t it?
&lt;/h2&gt;

&lt;p&gt;Good fit: batch ingest jobs, bursty traffic (50 docs in a minute, then quiet), background pipelines, OCR-API replacement. Poor fit: interactive single-document apps (cold starts make users think it’s broken), sparse traffic (one job per cold start dominates the bill), strict latency SLOs without provisioning &lt;code&gt;workers_min ≥ 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I run this template in production for a document indexer. Six months of operation, here’s the honest fit picture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch ingest.&lt;/strong&gt; Drop 500 PDFs into a queue. One cold start amortizes across the whole batch at ~$0.001 per page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bursty traffic.&lt;/strong&gt; A user uploads 50 documents in a minute. One cold start, 49 warm parses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background pipelines.&lt;/strong&gt; Nightly cron processes yesterday’s intake. Cold start cost is rounding error against a multi-hour batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR-API replacement.&lt;/strong&gt; Comparable per-page cost without shipping documents to a third party.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Poor fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactive single-document parsing.&lt;/strong&gt; Your user uploads one PDF and waits two minutes for the cold start. They’ll think it’s broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse traffic (one job every 20–60 min).&lt;/strong&gt; Almost every request is a cold start. The ~$0.03 cold-start tax dominates. Rent a permanent low-tier GPU pod and skip serverless instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict latency SLOs.&lt;/strong&gt; Cold-start latency is partly outside your control. Provisioning &lt;code&gt;workers_min ≥ 1&lt;/code&gt; eliminates cold starts but you pay for the warm worker around the clock.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo’s defaults (&lt;code&gt;workers_min=0&lt;/code&gt;, &lt;code&gt;idle_timeout=10s&lt;/code&gt;) are tuned for batch-with-bursts. The dashboard’s scaling settings are where you tune for other patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s the real cold-start cost on RunPod Serverless?
&lt;/h2&gt;

&lt;p&gt;Roughly &lt;strong&gt;110 seconds&lt;/strong&gt; before MinerU starts parsing your first request after a scale-from-zero. The composition: ~3 s fitness checks, ~20 s vLLM engine config, ~20 s model load, ~25 s torch.compile, ~5 s CUDA graph capture, ~5 s of actual parse. Billed at ~$1.10/hr on the 4090 default, that’s roughly &lt;strong&gt;$0.03 per cold start&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The per-phase breakdown is documented in the &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/troubleshooting/" rel="noopener noreferrer"&gt;troubleshooting guide&lt;/a&gt; if you want to see where the time goes. The boot-time warmup in this template loads MinerU’s model and JIT-compiles vLLM kernels &lt;em&gt;before&lt;/em&gt; the worker accepts requests. When RunPod’s FlashBoot snapshot is available on a subsequent scale-from-zero, the wall-clock drops to ~7–8 seconds because the snapshot captured a warm process. When the snapshot isn’t available (new host, image rebuild), warmup re-runs and you pay the full ~110 s again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-flashboot-mechanism-investigation/" rel="noopener noreferrer"&gt;The FlashBoot mechanism investigation&lt;/a&gt; covers when the fast path applies, with measured numbers across multiple consecutive cold starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should I watch out for before going to production?
&lt;/h2&gt;

&lt;p&gt;Three production gotchas the marketing won’t mention. The 20 MB response cap silently drops large outputs (symptom: &lt;code&gt;NoneType&lt;/code&gt; after a successful parse — covered by the R2 bridge). &lt;code&gt;execution_timeout&lt;/code&gt; defaults to 900 s and won’t cover full books. &lt;code&gt;file_b64&lt;/code&gt; inline payloads cap around 10 MB on the way in. None of these crash the worker; they manifest as confusing client-side errors.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 MB response cap.&lt;/strong&gt; RunPod’s &lt;code&gt;/runsync&lt;/code&gt; gateway drops responses over ~20 MB. Multi-page parses with embedded images hit this around 50–80 pages. Worker logs &lt;code&gt;done&lt;/code&gt;; client gets &lt;code&gt;NoneType&lt;/code&gt;. Fix: &lt;code&gt;return: "s3"&lt;/code&gt; + Cloudflare R2, walked through in &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-20mb-response-cap-r2-bridge/" rel="noopener noreferrer"&gt;the R2 bridge post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-job timeout.&lt;/strong&gt; Repo defaults &lt;code&gt;execution_timeout=900s&lt;/code&gt; (good for ~150–300 pages on 4090). A 5,000-page book is 80–500 minutes depending on content density. Bump &lt;code&gt;execution_timeout&lt;/code&gt; for long jobs; the endpoint upper limit is 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline payload cap on the way in.&lt;/strong&gt; &lt;code&gt;file_b64&lt;/code&gt; requests cap around 10 MB. For bigger files, pass &lt;code&gt;file_url&lt;/code&gt; and let the worker fetch from your storage. R2 public dev URLs work well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start economics.&lt;/strong&gt; “Pennies per page” depends on amortization. Track average pages per cold start in your logs. If it’s under 30, bump &lt;code&gt;idle_timeout&lt;/code&gt; or run &lt;code&gt;workers_min=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to next
&lt;/h2&gt;

&lt;p&gt;The repo ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Typed Python client (&lt;code&gt;MineruClient&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deploy.py&lt;/code&gt; / &lt;code&gt;destroy.py&lt;/code&gt; for endpoint lifecycle automation&lt;/li&gt;
&lt;li&gt;Reference adapter pattern for wrapping MinerU output into domain models&lt;/li&gt;
&lt;li&gt;96 unit tests, CI on every PR&lt;/li&gt;
&lt;li&gt;Commitlint + semantic-release for automated CHANGELOG / GitHub Releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the deeper context that didn’t fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-flashboot-mechanism-investigation/" rel="noopener noreferrer"&gt;How RunPod FlashBoot actually works&lt;/a&gt; — four-request investigation into the cold-start mechanism and the per-host snapshot caveat.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sergeyshmakov.github.io/mineru-runpod/blog/runpod-20mb-response-cap-r2-bridge/" rel="noopener noreferrer"&gt;The R2 bridge for the 20 MB response cap&lt;/a&gt; — fix for &lt;code&gt;NoneType&lt;/code&gt; on multi-page outputs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/choosing-gpu/" rel="noopener noreferrer"&gt;Choosing a GPU&lt;/a&gt; — when 24 GB is enough, when to opt up to 48 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this saved you time, the easiest way to say thanks is &lt;a href="https://runpod.io?ref=31jdfpnq" rel="noopener noreferrer"&gt;signing up for RunPod through this link&lt;/a&gt;. Star the &lt;a href="https://github.com/sergeyshmakov/mineru-runpod" rel="noopener noreferrer"&gt;repo on GitHub&lt;/a&gt; for updates.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How does mineru-runpod compare to hosted PDF APIs?
&lt;/h3&gt;

&lt;p&gt;Per-page cost is in the same ballpark ($0.001–$0.004) when amortizing cold starts across reasonable batches. The differences are control and lock-in. You deploy your own RunPod endpoint, pick your GPU and concurrency, run whichever MinerU version you want, and never send documents to a third party. The trade-off is operating a serverless template instead of consuming a managed API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can MinerU 2.5 handle non-English PDFs?
&lt;/h3&gt;

&lt;p&gt;Yes. The &lt;code&gt;vlm-auto-engine&lt;/code&gt; default backend handles English and Chinese well per &lt;a href="https://huggingface.co/opendatalab/MinerU2.5-Pro-2605-1.2B" rel="noopener noreferrer"&gt;the model card&lt;/a&gt;. For other scripts (Cyrillic, Arabic, Devanagari, Japanese, Korean), the &lt;code&gt;pipeline&lt;/code&gt; backend uses PaddleOCR with script-family models, covering 109 languages. Empirically the Pro VLM also handles Cyrillic correctly even though &lt;code&gt;lang&lt;/code&gt; is ignored on the VLM path. Switch backends per-request via the &lt;code&gt;backend&lt;/code&gt; field.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the difference between &lt;code&gt;vlm-auto-engine&lt;/code&gt;, &lt;code&gt;pipeline&lt;/code&gt;, and &lt;code&gt;hybrid-auto-engine&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;vlm-auto-engine&lt;/code&gt; uses MinerU’s 1.2B VLM via &lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt;. Fastest on English / Chinese, ~1–6 s/page warm. &lt;code&gt;pipeline&lt;/code&gt; uses PaddleOCR plus dedicated layout / formula / table models. Slower (~3–5 s/page) but more memory-predictable (4 GB minimum VRAM) and covers 109 languages. &lt;code&gt;hybrid-auto-engine&lt;/code&gt; routes each page through either backend based on content. Highest quality on mixed-content docs; needs 48 GB on dense layouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the per-page cost include the cold-start tax?
&lt;/h3&gt;

&lt;p&gt;No. The ~$0.001 per page is warm-worker math. Each scale-from-zero adds a roughly $0.03 fixed cost on the 4090 default. Your effective per-page cost is &lt;code&gt;(0.001 × pages) + (0.03 × cold_starts) / pages&lt;/code&gt;. For 100 pages across one cold start, that’s $0.0013 per page. For 10 pages, it’s $0.004.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use mineru-runpod with my own MinerU model?
&lt;/h3&gt;

&lt;p&gt;Yes. Fork the repo and update the Dockerfile’s &lt;code&gt;huggingface_hub.snapshot_download&lt;/code&gt; call to point at your model. Rebuild and redeploy. The handler is model-agnostic; MinerU’s &lt;code&gt;aio_do_parse&lt;/code&gt; resolves whatever model is in &lt;code&gt;HF_HOME&lt;/code&gt; at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  What GPU does the template default to?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ADA_24&lt;/code&gt; (RTX 4090, 24 GB). Switched from &lt;code&gt;AMPERE_24&lt;/code&gt; (A5000) on 2026-05-26 after measuring per-page cost. The 4090 is 2–4× faster per page than the A5000 and cheaper per page despite the higher hourly rate. See &lt;a href="https://sergeyshmakov.github.io/mineru-runpod/guides/choosing-gpu/" rel="noopener noreferrer"&gt;Choosing a GPU&lt;/a&gt; for the full math and when to opt up to 48 GB.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I keep my RunPod endpoint warm to avoid cold starts?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;workers_min=1&lt;/code&gt; on the endpoint. You pay for the always-on worker around the clock (~$0.000306/s on the 4090 default, so ~$26/day or ~$800/month). Worth it if your traffic is steady enough that the warm worker stays busy, or if your latency SLO can’t tolerate the cold-start window. For bursty traffic, &lt;code&gt;workers_min=0&lt;/code&gt; with FlashBoot enabled is usually cheaper.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; RunPod links in this post use a referral code that credits me at no cost to you. The post would read the same without it.&lt;/small&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>runpod</category>
      <category>serverless</category>
      <category>mineru</category>
    </item>
  </channel>
</rss>
