I have a habit: any tool that says "processed locally, nothing uploaded" gets a packet capture before I believe it. This time it was PDF-to-image, and the count from picking a file to four images appearing was 13 requests, none of them POST, none carrying a body. Then I moved the feature off my server, and the bill moved less than I expected — the thing I actually saved turned out to be something else.
How I counted: a clean tab, DevTools Network panel with preserve-log on, recording from the moment the file goes in until the output shows up, on a four-page mixed text-and-image PDF I generated myself at 490 KB. Three of the thirteen are /pdf/to-html-preview-worker.js, /pdf/core.js and /pdf/to-html.js — the parsing and rendering code being fetched on demand, and something has to come down to do the work. The other ten are blob: references to in-memory objects; the panel lists them but they never touch the network interface. Non-GET requests: zero. Requests with a body: zero. This check isn't specific to one tool — look for POSTs, look for request bodies. It's faster than reading a privacy policy and it tells you more.
Then the math. My previous setup rendered these server-side: a 490 KB PDF in, 494 KB of images out, call it 1 MB round trip. Ingress usually isn't billed but holds a connection and some scratch disk; egress is billed, somewhere around $0.08–0.12/GB depending on who you're with. At 500 documents a day — 15,000 a month — egress is roughly 7.4 GB. That's pocket change. Which is awkward, because "saving bandwidth" was the reason I gave myself for doing this in the first place, and it doesn't survive contact with the number. I wouldn't spend an afternoon to save a few dollars.
The CPU is the real line item. Four pages at 216 DPI measured 3.0s, at 300 DPI 6.1s, and that's a core held for the duration. 15,000 × 3 seconds is 12.5 core-hours, which also sounds like nothing until you remember it isn't spread evenly — it lands in a few clusters during the day and you provision for the peak, not the total. My box sat idle most of the time and pinned at lunch and again around nine in the evening. I upgraded the instance once because of those two windows, and then wasted the extra capacity for the other twenty-two hours. Moving the work into the browser takes both lines to zero, and the cost is a few extra JS files on first load. For a one-person product where peak and trough differ by 10×, that trade isn't close.
Multi-page output arrives as a ZIP with the original page numbers zero-padded into the filenames. The padding matters more than it sounds like it should: I once used a tool that emitted image1.png through image10.png, string sort put 10 between 1 and 2, and I spent well over an hour at 2am debugging the wrong layer — I went through my own sort function line by line twice before I thought to look at the filenames. I check for padding first now. One detail I didn't expect: exporting page 2 alone gave 124 KB, and page 2 inside the full export was also 124 KB, identical, so single-page export is the same render rather than a separate path. Re-pulling a page later gives you something that matches the original batch instead of a near-miss.
Two things I'm not going to oversell. This one pipeline is local, which doesn't generalise to every feature — some output formats the browser can't write at all and those go to a server, and I tested PDF-to-image and nothing else. And the browser has a ceiling: I threw an 800×9000pt single-page PDF at it and 216 DPI and 300 DPI produced identical files, 1456×16380 both times, because it hit a size limit and the setting stopped mattering. A server wouldn't have that constraint. Client-side isn't a universal answer, and pretending otherwise would just get me a bug report later.
Tool: https://imging.ai/pdf-to-image/ . The request count takes two minutes to reproduce. Don't take my number for it, count your own.
Top comments (0)