A tool that processes one file and a tool that processes 100 files may share the same conversion function.
They are still different products.
The moment users can drop a large batch into your app, almost every surrounding decision changes.
One-file logic
A one-file flow can be brutally simple:
const result = await processFile(file);
download(result);
The UI can show a spinner.
If the operation fails, show an error.
Done.
Now add 100 files
The naive implementation becomes:
const results = await Promise.all(
files.map(processFile)
);
This looks elegant.
It can also be a terrible idea.
If every file is a large image, you may suddenly decode dozens or hundreds of high-resolution bitmaps simultaneously.
Memory spikes.
The tab freezes.
Mobile devices suffer.
The browser may kill the page.
Bulk support requires resource management.
Controlled concurrency
A better design uses a limited number of active jobs.
Conceptually:
Queue: 100 files
Active:
[1] processing
[2] processing
[3] processing
[4] processing
When [2] finishes:
take next item from queue
This gives you a lever.
You can adjust throughput without creating unbounded pressure.
The correct limit depends on:
- average file size
- image dimensions
- CPU cost
- memory usage
- target device class
The important principle is simple:
Maximum parallelism is not the same thing as maximum useful performance.
Progress becomes a first-class feature
With one file, a spinner is enough.
With 100 files, the user wants answers:
- How many are complete?
- Which file is processing?
- Did any fail?
- Can I cancel?
- Can I download completed outputs?
- Is the app stuck?
A useful progress model might show:
63 / 100 complete
4 processing
31 waiting
2 failed
That is much more informative than an indeterminate loader.
Errors should not kill the batch
Another difference:
If 1 file fails out of 100, should the entire job fail?
Usually not.
Bulk operations need per-item error isolation.
A better result can look like:
✓ photo-001.jpg
✓ photo-002.jpg
✗ broken-file.tiff
✓ photo-004.jpg
Then the user can retry or inspect only the failed items.
This is a general batch-processing principle:
One bad record should not destroy 99 good results unless atomicity is actually required.
Download UX changes too
Downloading 100 files one by one is technically possible.
It is also horrible UX.
Users expect one packaged output.
That is why ZIP generation naturally appears in bulk tools.
The workflow becomes:
process items
↓
collect outputs
↓
package
↓
download once
The packaging step becomes part of the product.
Naming becomes important
Single-file tools can get away with generic output names.
Bulk tools cannot.
If the user uploads:
product-01.jpg
product-02.jpg
product-03.jpg
the outputs should remain recognizable:
product-01.webp
product-02.webp
product-03.webp
Otherwise the user saves time during conversion and loses it again during file cleanup.
Cancellation matters
A 500-file job may take long enough that the user changes their mind.
A production-quality system should think about cancellation.
In browser code, that may involve:
- abort signals
- worker termination
- queue state
- cleanup of temporary objects
The UX should clearly distinguish:
- queued
- active
- completed
- cancelled
- failed
Memory cleanup is part of correctness
Browser image processing often creates:
ImageBitmap- canvas buffers
- object URLs
- blobs
If you keep unnecessary references alive, memory usage can climb during a large batch.
Bulk testing should include long runs, not just successful 5-file demos.
This problem shaped BatchSet
One of the reasons I built BatchSet's Bulk Image Converter was that I wanted the workflow to focus on the batch as a unit.
Not:
"Here's a converter that technically accepts multiple files."
But:
"How should the app behave when the batch itself is the user's job?"
That framing changes the product.
A checklist for bulk tools
If you are adding multi-file support, I would review:
- [ ] controlled concurrency
- [ ] per-item status
- [ ] total progress
- [ ] failure isolation
- [ ] retry behavior
- [ ] cancellation
- [ ] predictable filenames
- [ ] memory cleanup
- [ ] one-click packaged download
- [ ] mobile / low-memory testing
Final thought
"Supports multiple files" is not a checkbox.
It is an architecture decision.
The processing function may be the same.
Everything around it changes.
And that is why the moment your app supports 100 files, it becomes a different product.
Top comments (0)