Dev.to AI 🤖 Ai 👁 0 📖 4 min read

Products, One Core, 2GB of RAM: How Upload Rejections Became My Most Reliable Build Step**

First Adobe Stock batch I ever sent, 140 files, gone in under a minute. No review, no email, just red. Header row was wrong. I had built the CSV with the Vecteezy column set and pointed Adobe at it, because both files li

First Adobe Stock batch I ever sent, 140 files, gone in under a minute. No review, no email, just red. Header row was wrong. I had built the CSV with the Vecteezy column set and pointed Adobe at it, because both files lived in the same output/ folder and I grabbed the wrong one. Adobe wants 5 columns. Vecteezy wants 4. Dreamstime wants 15. A mismatched header gets rejected before any human or model looks at the art.

That was the moment I stopped treating upload as the last step and started treating it as a build step. Same as a compiler. You do not ship a binary and hope. You run the check that fails loud.

Everything below is from one pipeline: 1,860 assets made, 1,077 ready or submitted, 39 finished products, 856 MB of output, all on 1 vCPU, 2 GB RAM, no GPU.

Metadata headers are a schema, not paperwork

The rejections were not about my images. They were about the header row. Each channel demands an exact column set and rejects the whole file on a mismatch.

Adobe: 5 columns. Vecteezy: 4 columns. Dreamstime: 15 columns. Get one label wrong, one column short, one column reordered, and the upload fails instantly. Same as a JSON payload missing a required key.

So make_metadata.py writes the header the platform demands and nothing else. No extra columns for my own bookkeeping, no guessing. One command per platform:

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

By default it title-cases the filename and uses that as the title. That is a first pass, fine for local testing, and you should replace it before you submit with real titles and keywords via --titles titles.json. I am being blunt about that because filename titles do get noticed and they do get rejected on quality review. The script gets the schema right; it cannot write your description for you.

The payoff is boring and real: headers stop being a source of rejection. You move the failure earlier, where you can fix it in seconds instead of losing a batch.

The ledger that makes double-posting impossible

Second mistake I made: running a batch twice. First run uploaded fine, re-run duplicated 90 rows because I had re-joined the folder listing and lost track of what was already done.

Flagging risk on a stock account is the kind of thing you do not get to undo. So upload_tracker.py keeps a plain ledger and answers one question: what is not done yet?

python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status

pending prints only the files with no done entry. The upload loop reads that list, changes one file, marks it done. Re-running it is safe by construction. The ledger is the source of truth, not my memory. Standard library only, no install.

One core, 2 GB, no GPU, and it still ships

The whole pipeline ran on 1 vCPU with 2 GB RAM. Not a container with a soft limit, an actual tiny box. It works because of a few habits, and none of them are clever.

One process at a time. Never fan out image work in parallel on a single core. Parallel jobs on one core do not go faster, they just fight for the same cycles and peak memory at the same time.

gc.collect() after each asset. Cheap insurance when you are rendering hundreds of files in a loop.

Measure peak RSS, not average. resource.getrusage(RUSAGE_SELF).ru_maxrss reports the real ceiling. Average memory tells you nothing about the render that almost killed the box.

One file per subprocess for heavy render steps, so memory returns to the OS when the child exits instead of lingering in the parent.

Render at the resolution you need. Inspecting a preview at 60 dpi beats rendering it at 300 dpi just to look at it. The 300 dpi pass is the one you ship, not the one you debug with.

These are not optimizations. They are the difference between a working job and an OOM kill at item 700.

Every product is a zip plus a cover

A bare file does not sell. A zip with a real cover does. pack_product.py does both in one step: zips the files, renders a cover at 1280x720 and a thumbnail at 600x600, and drops all three into a _system folder. Point it elsewhere with --dir or the PACK_ROOT environment variable, no script edits.

python pack_product.py my-product "My Product" "A short subtitle" 9

Turn a Markdown doc into a product with md2pdf.py, which handles headings, tables, lists and links and outputs a styled PDF. That is how a research file becomes something you can list.

Two third-party packages across the whole kit: Pillow for covers, reportlab for PDF. If you only use metadata and the tracker, you install nothing, they are pure standard library.

What the numbers actually look like

1,860 assets generated. 1,077 ready or submitted. 39 products packaged. About 19,000 lines of pipeline code in the parent project, and 300+ pages of research behind the rules the scripts enforce.

My first paid sale was this system: $2.80. Not a milestone worth a screenshot. Worth mentioning because it is honest. Small numbers, real ones.

The lesson I keep coming back to: the rejections were the cheapest feedback in the whole process. Each one pointed at a rule I could encode. Once encoded, it stopped costing me batches. That is all a build step is.

The four scripts are the packaging layer of what I now call Digital Asset Factory: metadata generator, upload tracker, Markdown to PDF, product packer, plus the folder layout and the tiny-server habits that keep it alive. If you are building the same thing, it is the version of this work that already made the mistakes.

Get digital asset factory

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

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.