Running a batch job on constrained hardware is a rite of passage for indie developers and asset sellers. One such developer recently shared a post-mortem of a failed pipeline that crashed at product 14 out of 39, losing three hours of work to a MemoryError on a 1 vCPU, 2 GB RAM server. The incident highlighted a common pitfall: assuming a script that works once will scale linearly without defensive engineering. The solution wasn't upgrading hardware, but implementing strict error handling and state management.
Metadata Validation Is Non-Negotiable
The first critical failure wasn't the rendering engine, but the metadata CSV. Different stock platforms enforce rigid header schemas; Adobe requires 5 columns, Vecteezy 4, and Dreamstime 15. A single column mismatch triggers an instant rejection at review. The developer abandoned dynamic header generation in favor of hardcoding exact schemas per platform using a script named make_metadata.py. By enforcing strict column counts via command-line arguments like --platform adobe, the pipeline eliminated human error in configuration. Additionally, relying on filename title-casing was replaced by passing explicit --titles titles.json files to ensure discoverability and avoid review rejections.
Preventing Double-Posts With a Ledger
Perhaps the most dangerous failure mode was successful re-runs leading to double-posting, which can flag contributor accounts. To mitigate this, the developer implemented upload_tracker.py, a plain text ledger using only the Python standard library. Before uploading, the script checks pending items via python upload_tracker.py pending items.txt, and after each success, it marks the asset as done. This approach shifts the source of truth from the developer's memory to a persistent file, making re-runs harmless by construction. It is a simple, boring solution that prevents catastrophic account-level errors.
Managing Memory Without Upgrading Hardware
The 3 a.m. MemoryError taught the developer to measure peak RSS rather than guess. Using resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, they identified that parallel processing on a single core was thrashing resources. The fixes were pragmatic: running one process at a time, calling gc.collect() after each asset to force memory release, and executing heavy renders in isolated subprocesses so the OS could reclaim memory upon child exit. Furthermore, preview inspections were downgraded from 300 dpi to 60 dpi, significantly reducing the memory footprint during the validation phase. These changes allowed the full batch of 39 products, totaling 856 MB of output, to complete on the same 2 GB machine without a GPU.
Structure and Packaging Enforce Reliability
Self-inflicted messes from mixed raw renders and finished zips were solved by a strict folder layout: assets/, output/, packs/, research/, and scripts/. The developer emphasized never deleting the research/ folder, as scrape logs proved essential for rebuilding product variants months later. To address silent failures where products lacked covers, pack_product.py was introduced. This script uses Pillow to render 1280ร720 covers and 600ร600 thumbnails alongside the zip file, ensuring every deliverable meets platform requirements before submission. The entire toolkit relies on just two third-party dependencies: Pillow and reportlab.
Key Takeaways
- Hardcode metadata schemas per platform to avoid review rejections due to column mismatches.
- Use a persistent ledger script to prevent double-posting, making re-runs idempotent.
- Measure peak RSS and isolate heavy tasks in subprocesses to manage memory on low-RAM servers.
- Enforce a strict folder structure and automated packaging scripts to catch silent failures like missing covers.
The Bottom Line
Error handling is not an afterthought; it is the infrastructure that makes constrained hardware viable. Stop relying on willpower and start writing boring, defensive scripts.