Products, 2 GB of RAM, and One Rule: The Filesystem Is the Database
I lost track of which files were already posted.
It was a plain text file. posted.txt. It started accurate. Then I ran a batch, the script crashed halfway, I restarted it, and about 200 rows went up a second time. Only a warning that time, no suspension, but I understood the shape of the problem: the thing that fails on a stock channel is not bad art. It is running the same batch twice.
That was the day I stopped treating the filesystem as storage and started treating it as the database.
The filesystem is the database
There is no Postgres in this pipeline. There is a folder of files and one rule: the filename, plus a state file, is the source of truth for everything.
Every asset gets one name. Every name lives in exactly one ledger. upload_tracker.py reads that ledger and answers one question per file: done or not done.
python upload_tracker.py pending items.txt # shows what is NOT done yet
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
A re-run can only ever pick up the leftovers. Double-posting is not a discipline problem any more, it is structurally impossible.
The layout that keeps this sane:
project/
assets/ raw output, one file per asset
output/ ready-to-submit files + metadata.csv
packs/ finished product zips + covers
research/ notes and sources
scripts/ the tools
I deleted research/ once, to "clean up". Those scrape logs and notes were the raw material for the next two products. They are gone. Keep that folder forever.
The headers are the review
I made 1,700 assets before I understood that most rejections never reach a human. They are CSV validation.
Three channels, three shapes, all strict:
- Adobe: 5-column metadata CSV
- Vecteezy: 4-column
- Dreamstime: 15-column
Wrong header, wrong column count, wrong order, and the whole file bounces instantly. No partial import. You do not get file 1 through 40 and an error at 41. You get nothing.
So make_metadata.py writes the header the platform demands and nothing else:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv --ext .jpg
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv --ext .jpg
Two of my four scripts use the standard library alone. No install, no dependency, no version drift. make_metadata.py and upload_tracker.py need nothing but Python. Only pack_product.py needs Pillow and only md2pdf.py needs reportlab.
Pass --titles titles.json when you have real titles and keywords. Without it the script title-cases the filename and uses that. That is a first pass, not a submission.
1 vCPU, 2 GB RAM, no GPU
This whole pipeline ran on a single core with 2 GB. Not as a stunt. Because that is what a $5 box gives you and the work fits.
Four habits made it fit:
- One process at a time. Never fan out image jobs on one core. Parallel work on 1 vCPU does not finish faster, it just swaps.
-
Free memory between items.
gc.collect()after each asset. Boring, and it keeps the RSS flat across a 500-item run. -
Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrssgives you the real ceiling. Average memory tells you nothing about the item that killed the process. - One file per subprocess for heavy renders, so memory returns to the OS instead of sitting in the parent.
- Render at the resolution you need. Checking a preview at 60 dpi does the job. Rendering it at 300 dpi to look at it wastes the box.
This matters because every generated image costs money. A process that dies at item 200 of 500 does not just waste an hour, it wastes the spend on those 200 items.
Packaging is half the product
A zip with a real cover sells. A bare file does not.
python pack_product.py my-product "My Product" "A short subtitle" 9
That one command writes the zip, a 1280x720 cover, and a 600x600 thumbnail into _system. If you want it somewhere else, use --dir or the PACK_ROOT environment variable. No reason to edit the script for a path.
md2pdf.py does the same job for written products:
python md2pdf.py content.md product.pdf "My Product Title"
Headings, tables, lists, links, styled. Content you already wrote becomes a thing with a cover and a price.
Where it actually stands
Real numbers, because the honest ones are more useful than the round ones:
- 1,700 assets made
- 58 live on Adobe
- first sale: $0.99
That is the whole scoreboard. 58 live out of 1,700 generated is the part nobody puts in a post. Review is slow, keywords matter more than volume, and one sale at $0.99 will not pay for anything.
What made the difference was not art quality. It was that the pipeline stopped wasting its own output. Nothing double-posted, nothing silently dropped, nothing lost to a crash on a 2 GB box.
The kit
The four scripts above are the packaging layer of a larger system. In the full pipeline they sit under a free stack: an orchestrator that schedules jobs and retries failures, and a local proxy for model access, both at zero cost. The parent project is around 19,000 lines and sits on top of 300+ pages of research notes, most of which exist because a platform rejected something and I had to find out why.
I put the working parts into digital asset factory, the full system built from this work. Not a tutorial that ends at "here is how you would do it". The scripts run, the headers are the ones the platforms accept, and the RAM notes come from a box that really did have 2 GB.
If you only take one thing: stop trusting your memory of what you posted. Put it in a file, and let the file decide.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)