DEV Community

Cover image for 39 Products, One 2 GB Server: What Makes a File 'Finished' When the CSV Header Decides"**

39 Products, One 2 GB Server: What Makes a File 'Finished' When the CSV Header Decides"**

I lost forty minutes on a Tuesday because of a CSV header. Not the art, not the keywords, the header. I had submitted a batch of 12 JPGs to Adobe Stock and every single one came back rejected in under a minute. The error was blunt: wrong file format for the metadata. I had written my own column order. Adobe has a fixed 5-column header and it does not negotiate.

That was the moment I stopped treating "finished" as a feeling and started treating it as a checklist. This post is about what that checklist turned out to be, and how it runs on a server with 1 vCPU and 2 GB of RAM.

Finished means the header matches, exactly

There are three upload CSVs I deal with and each has a fixed shape:

  • Adobe Stock: 5 columns. Filename, title, keywords, category, releases. One wrong column name and the whole batch bounces.
  • Vecteezy: 4 columns. Same deal, fewer columns, same instant rejection.
  • Dreamstime: 15 columns. The widest of the three, and the easiest to fumble by hand.

The lesson is boring and absolute: the header is not something you write, it is something you reproduce. I wrote make_metadata.py so it emits the exact header each platform demands and nothing else. It walks a folder of assets, pulls titles and keywords (from a titles.json if you supply one, from the filename if you do not), and writes the CSV. Filename-as-title is fine for a first pass, but replace it before you submit, reviewers read those.

When "done" is defined by a byte-exact header, a file is either finished or it is not. There is no almost.

Finished means it cannot be posted twice

The second failure mode came from my own batch runner. I re-ran an upload script after a partial failure and nearly double-posted a chunk of my catalogue. Double-posting is the fastest way to get flagged on these platforms, and on a 2 GB box with a cron job you will re-run things, that is guaranteed.

So upload_tracker.py is now part of my definition of finished. A file is finished when the ledger says it is done. Three commands:

python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Enter fullscreen mode Exit fullscreen mode

Standard library only, no install. A re-run checks the ledger first. This is the least glamorous script I have written and the one I would keep if I could only keep one.

Finished means it renders, on 2 GB, without dying

My server has 1 vCPU, 2 GB RAM, no GPU. Every image job I run competes with the OS for that memory. The habits that keep the pipeline alive:

  • One process at a time. No parallel image jobs on one core, ever.
  • gc.collect() after every asset, so Python actually hands memory back.
  • Measure peak RSS, not average, with resource.getrusage(RUSAGE_SELF).ru_maxrss. Average lies, the peak is what kills the box.
  • Heavy render steps run one file per subprocess, so memory returns to the OS when the process exits.
  • Inspect previews at 60 dpi, render finals at 300. Rendering at 300 just to check a layout is how you OOM yourself.

A file is finished when it survives the full batch on this machine. If it only works on my laptop, it is not finished.

Finished means it looks like a product

The last gap was packaging. I had folders of good files that sold nothing, because a bare zip with no cover does not read as a product. Two scripts closed that:

  • md2pdf.py turns a Markdown doc into a styled PDF (headings, tables, lists, links) with reportlab. One command: python md2pdf.py content.md product.pdf "My Product Title".
  • pack_product.py zips the files and renders a 1280×720 cover plus a 600×600 thumbnail with Pillow. It writes into a _system folder, and you can redirect it with --dir or PACK_ROOT so nothing gets hardcoded.

Only two third-party packages in the whole kit, Pillow and reportlab. Everything else is standard library, because every dependency is another thing that can break on a small server.

The folder layout that keeps 39 products from becoming a junk drawer

The structure I settled on:

project/
  assets/       raw output
  output/       ready-to-submit files + metadata.csv
  packs/        finished zips + covers
  research/     notes, scrape logs, sources
  scripts/      the tools
Enter fullscreen mode Exit fullscreen mode

One hard rule: never delete research/. I did that once to "clean up" and lost raw material I later needed. The 300+ pages of research behind the rules in these scripts exist because I stopped throwing notes away.

Where this stands

The full pipeline has produced about 1,700 assets and 58 are live on Adobe Stock. The first sale was $0.99, and I am not going to pretend that number is anything other than a start. The 39 finished products from this system total 856 MB of output, packaged by the same four scripts, running on the same 2 GB machine. Around 19,000 lines of pipeline code sit under all of it.

I bundled the four scripts themselves, with the real headers, the ledger, the PDF renderer, and the packager, into a small kit. It is the packaging layer of a larger system I call the digital asset factory. If you want the exact tools instead of a description of them, that is it. No framework, no cloud account, Python 3.9 and up.

Get digital asset factory


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)