Dry-Run Your Asset Pipeline: Validate Covers, Zips And CSVs On A 2 GB Server
Last March I lost a full evening to a header row. I had 60 JPGs ready for Adobe Stock, generated the CSV, uploaded the batch, and every single file came back rejected in under a minute. The problem was the header. Adobe
Last March I lost a full evening to a header row. I had 60 JPGs ready for Adobe Stock, generated the CSV, uploaded the batch, and every single file came back rejected in under a minute. The problem was the header. Adobe expects a specific 5-column layout, and I had written a 6th column with a field I thought was useful. One extra column, sixty rejections, zero uploads. That was the night I stopped trusting my own generated CSVs and started validating everything before it touches an upload page.
Since then the pipeline has shipped 39 finished products, about 856 MB of output, all on a 1 vCPU, 2 GB RAM server with no GPU. Here is what I now check before anything goes out the door, and why each check exists.
Validate the CSV headers before you build the rows
The three stock platforms I submit to do not negotiate. Adobe wants a 5-column header, Vecteezy wants 4 columns, Dreamstime wants 15. If the header is wrong, the upload fails instantly and you learn nothing else about your batch. So the first thing my metadata script does is write the exact header the platform demands, and nothing else. No extra fields, no "helpful" additions.
The second failure mode is titles. If you let a script auto-generate titles from filenames, you get "Asset 001 Final V2" as a title, and reviewers notice. My script title-cases the filename as a fallback, which is fine for a dry run, but I always pass a titles file with real titles and keywords before submitting. A dry run with generated titles tells you the structure works. A real submission needs human titles. Do not confuse the two.
The dry run itself is cheap: generate the CSV, open it, check the header matches the platform's column count, check the row count equals your file count, check every row has a title and at least the minimum keywords. Two minutes of reading a CSV beats a rejected batch.
Make double-posting impossible, not unlikely
The fastest way to get a stock account flagged is running your upload batch twice. It happens when you re-run a script after a partial failure and forget which files already went through. Willpower does not fix this. A ledger does.
My tracker is embarrassingly simple: a plain file where done "asset-001.jpg" marks an item complete, and pending shows what is left. Before any upload run, I check pending first. If the ledger says an asset is done, it does not go out again, no matter what the script thinks. Standard library only, no install, no database. The simplicity is the point. A tracker you have to configure is a tracker you skip.
Watch peak RSS, not vibes, on a 2 GB box
The whole pipeline runs on 1 vCPU and 2 GB RAM, which means image work is where things die. The habits that keep it alive:
One process at a time. Never fan out image jobs in parallel on a single core. It does not go faster, it just swaps.
Free memory between items. A gc.collect() call after each asset. Boring, effective.
Measure peak RSS, not average. resource.getrusage(RUSAGE_SELF).ru_maxrss gives you the real ceiling your process hit. Average memory tells you nothing about the spike that kills the box.
One file per subprocess for heavy render steps, so the OS gets the memory back when the render finishes instead of the interpreter holding it.
Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between a 40 MB render and a 4 MB one, multiplied across a batch of 60 files.
When I dry run a new batch, I run it on five files first and check ru_maxrss at the end. If five files spike past what I am comfortable with, sixty files will take the server down halfway through.
Package and open the zip before you sell it
A product is not a folder of files, it is a zip, a cover, and a thumbnail. My packaging script bundles the files and renders a 1280Γ720 cover and a 600Γ600 thumbnail with Pillow, nothing fancier. The dry run here is physical: download the zip, open it, check the contents, look at the cover at actual size. I have caught missing files, a cover with the title cut off, and a thumbnail that looked fine at 600px and like noise at 60px. All of those would have gone to a customer otherwise.
Same for the PDF path. One command turns a Markdown doc into a styled PDF with headings, tables, lists and links via reportlab. The dry run is opening the PDF and clicking the links. Broken links in a paid product are the kind of thing buyers mention in reviews, not emails.
The checklist, condensed
Before any batch goes out: header matches the platform column count exactly, row count matches file count, real titles not filename fallbacks, ledger checked so nothing double-posts, peak RSS measured on a small sample, zip opened and cover viewed at real size. Total time, maybe fifteen minutes for a 60-file batch. It has caught every failure mode that used to cost me evenings.
All of this lives in four standalone Python scripts: the metadata builder for the Adobe, Vecteezy and Dreamstime formats, the upload ledger, the Markdown to PDF converter, and the packager. Two dependencies total, Pillow and reportlab, and half the scripts need nothing but the standard library. They are the same ones that packaged those 39 products, and they sit inside a larger playbook built from roughly 19,000 lines of pipeline code and 300+ pages of research, including the prompt engine behind the vector prompt pack that started all of this.
If you want the scripts and the process around them, they are here:
Building digital products? Free guides & kits for digital product sellers.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.