I Ran a Stock Pipeline on a $7 VPS for 30 Days. The Real Cost Breakdown
The moment that almost ended it was a Vecteezy rejection email at 6 in the morning. I had batch-generated 120 images overnight on the VPS, built the upload CSV, and pushed everything through. The whole batch came back rejected because my CSV header had one extra column. Not wrong data. One extra column. Vecteezy's importer saw a 5-column header where it expects 4 and refused the file before looking at a single image. A full night of compute, wasted, over a header row.
That was day 9 of 30. Here is what the month actually cost, and what I learned about running this on a machine with 1 vCPU and 2 GB of RAM.
The money, itemized
The server was the cheap part. $7 a month for 1 vCPU, 2 GB RAM, no GPU. The pipeline itself ran fine on that. The costs people don't budget for are elsewhere.
AI image generation was the biggest line item. Every asset costs credits or API calls, and when a batch gets rejected for a metadata problem, you don't get those credits back. The images are fine. The CSV was wrong. That distinction matters: on a tiny budget, your failure mode is almost never the model, it's the plumbing around it.
My time was the second cost. I spent hours that first week hand-fixing CSVs and manually checking which files I had already uploaded. That manual checking was doing a job a script should do, and doing it badly, because I still double-posted three files on day 12. Double-posting is not just embarrassing, it is the fastest way to get an account flagged on these platforms. I stopped doing anything by hand after that.
The 30-day total: $7 server, image generation costs, and roughly 20 hours of my time in week one, dropping to about 3 hours a week once the scripts took over. The scripts are the whole point. Compute is cheap, your evenings are not.
Memory is the real constraint, not CPU
Everyone worries about CPU on a cheap VPS. CPU was never my problem. RAM was.
The pipeline is a Python process that renders and packages images. On 2 GB, one careless render can eat your whole allocation and the OOM killer takes down your batch at 3 AM. I learned to measure peak RSS, not average usage, using resource.getrusage(RUSAGE_SELF).ru_maxrss. Average load of 400 MB means nothing if your peak hits 2.1 GB once and the kernel kills you mid-batch.
The habits that made it work:
- One process at a time. Never fan out image jobs in parallel on a 1-core box. Parallelism buys you nothing there and costs you everything in memory.
-
gc.collect()after each asset. Freeing memory between items sounds trivial until you skip it and watch RSS climb 50 MB per file until it dies. - One file per subprocess for heavy render steps, so the OS actually gets the memory back when the step finishes.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 cut one step's memory use by a factor I stopped feeling guilty about.
With those five habits, the whole pipeline, generation through packaging, ran on the $7 box without a single OOM kill in the last three weeks of the month.
Metadata is where batches go to die
Adobe Stock wants a 5-column CSV. Vecteezy wants 4. Dreamstime wants 15. Each importer checks the header first and rejects instantly if it does not match exactly. No partial credit, no warnings, just a rejected file and a wasted generation.
After the day-9 disaster I wrote make_metadata.py to build each platform's CSV from a folder of files, with the exact header each one demands and nothing else. It reads the folder, title-cases filenames as a first pass, and optionally takes a JSON file of real titles and keywords. The filename-as-title fallback is fine for testing, but you should replace it before submitting, reviewers notice lazy titles.
The other half of the problem is knowing what you already posted. upload_tracker.py is a dead-simple ledger: pending shows what is not done, done marks an asset, status gives the overview. A re-run of the batch now skips anything already posted. Since I started using it, zero double-posts. That is the number that keeps accounts alive.
Packaging is what turns files into products
By day 20 I had a folder of assets and no products. The gap between "I have files" and "I have something that sells" is packaging, and it was eating my evenings.
Two scripts closed it. md2pdf.py turns a Markdown doc into a styled PDF with headings, tables, lists and links, one command, which is how the documentation side of each product gets made. pack_product.py bundles everything: it zips the files and renders a 1280x720 cover and a 600x600 thumbnail. A zip with a real cover sells. A bare file does not. That is not a theory, it is what my own listing numbers showed once I started adding covers to older products.
The whole thing depends on exactly two third-party packages, Pillow and reportlab. The metadata and tracker scripts use the standard library alone. On a 2 GB box, a small dependency surface is not a stylistic choice, it is how you keep peak RSS down.
The folder layout that saved me
One mistake I will not repeat: I deleted my research/ folder in week two to "clean up." Those scrape logs and notes were the raw material for the next round of products, and I had to rebuild them from memory. The structure I settled on:
project/
assets/ raw output
output/ ready-to-submit files + metadata
packs/ finished zips + covers
research/ notes and logs, kept forever
scripts/ the tools
Thirty days, 39 finished products, 856 MB of packaged output, all from that single-core server. The parent pipeline behind it is around 19,000 lines of code with 300+ pages of research behind the rules the scripts enforce, but the four tools above are the part that does the daily work.
I packaged those exact scripts into the Pipeline Starter Kit, with the install steps and the folder layout included, because I kept rewriting them for my own batches and figured other solo makers were too. If you want the same four tools without the month of trial and error:
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)