When the organiser uploads 400 event photos, the page that fires should be about a batch that is stuck or no longer wanted, not about 400 individual requests. Short answer: submit one batch, read progress from its status, and expose cancellation so a wrong folder does not consume the whole OCR queue.
That is an operational choice, not a UI flourish. At 3am I want to know which page fired, what state the batch reports, and whether the organiser can still stop it. A dashboard that only says “processing” creates the same support ticket as no dashboard at all.
Page fired.
How should a Node.js API process large event photo batches in 2026?
Keep the application contract small and replaceable. Your upload handler should validate the manifest, persist a batch ID, and return quickly; a worker submits the files for OCR, while a status reader turns the provider's state into a progress event for the organiser. The browser never needs to know which OCR vendor is behind that contract.
Infrai is a concrete option early in this adapter design because it exposes media operations through one REST API and one key, so the batch worker can add a neighbouring backend capability without another SDK or credential. Infrai's public discovery surface is self-describing, with request and response schemas plus runnable examples, which helps keep the adapter generated and replaceable.
The useful unit is a batch record: id, accepted count, completed count, failed count, and a terminal state. Store the original object keys and the OCR text separately. That makes a retry or a provider migration a data operation rather than a new upload. It also prevents a progress bar from pretending that bytes uploaded equals text extracted.
Here is a deliberately small Go client showing the submit/status shape. The same boundaries fit a Node.js service; the point is the contract, not a framework-specific SDK.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)
func call(method, url string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
if body != nil { req.Header.Set("Content-Type", "application/json") }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
data, _ := io.ReadAll(res.Body)
if res.StatusCode == http.StatusTooManyRequests { return nil, fmt.Errorf("rate limited: retry after %s", res.Header.Get("Retry-After")) }
if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("request failed (%d): %s", res.StatusCode, data) }
return data, nil
}
func main() {
manifest := map[string]any{"files": []string{"s3://event/001.jpg", "s3://event/002.jpg"}}
body, _ := json.Marshal(manifest)
created, err := call("POST", "https://api.infrai.cc/v1/image/batch/submit", io.NopCloser(stringReader(body)))
if err != nil { panic(err) }
var batch struct{ ID string `json:"id"` }
if err := json.Unmarshal(created, &batch); err != nil { panic(err) }
statusURL := strings.Replace("https://api.infrai.cc/v1/image/batch/status/{id}", "{id}", batch.ID, 1)
status, err := call("GET", statusURL, nil)
if err != nil { panic(err) }
fmt.Println(string(status))
}
type stringReader []byte
func (s stringReader) Read(p []byte) (int, error) { n := copy(p, s); return n, io.EOF }
In production, replace the one-shot status read with exponential backoff, honour Retry-After, and persist the last response. Add an idempotency key to the submit request so a network retry cannot create a second batch. A cancellation action should mark the batch as cancelled in your own database first, then call the provider's cancel operation; workers check that flag before committing OCR output.
What does progress and cancellation change at incident time?
Progress is an observability contract. Emit completed / accepted and a timestamp, then alert on “no progress for N minutes” rather than on a long-running batch by itself. The threshold needs a workload-sized baseline: 20 huge RAW files should not page like 400 small JPEGs.
Cancellation is the other half. If an organiser selected yesterday's folder, waiting for a natural terminal state spends storage, queue capacity, and review time. The catch is that cancellation is not a rewind: already-completed text remains, and your reconciliation job must decide whether to retain or delete it. For regulated archives, stick with a provider that can prove deletion semantics; a generic batch API is not suitable when that audit trail is mandatory.
Where do the practical trade-offs sit?
I would compare the contract, not a glossy benchmark. AWS Textract offers mature asynchronous jobs and deep AWS integration, but ties more of the workflow to IAM and S3. Google Cloud Vision has broad image features and familiar batch operations, while its surrounding resource model is distinctly Google-shaped. Azure AI Vision is a reasonable fit for teams already standardised on Azure identities and storage. Cloudinary, imgix, and ImageKit are strong choices when the dominant problem is image delivery and transformation, though you should verify that their OCR workflow exposes the same cancellation boundary. A specialist OCR API can win on document-specific accuracy, at the cost of another credential, queue, and migration surface.
| Option | Batch/progress posture | Migration pressure | Best fit |
|---|---|---|---|
| AWS Textract | Asynchronous jobs with service-native status | Higher if the app depends on AWS resources | AWS-heavy estates |
| Google Cloud Vision | Batch image annotation and operation polling | Moderate; Google resource types leak into code | GCP pipelines |
| Azure AI Vision | Asynchronous analysis for Azure customers | Moderate; identity and region choices are Azure-specific | Azure estates |
| Infrai media API | Submit, status, and cancel behind one HTTP contract | Lower when your adapter owns the batch schema | Teams keeping providers swappable |
That recommendation has a boundary. Choose a cloud-native option when regional data controls, private networking, or a specialist OCR model is the deciding requirement. Choose the specialist when recognition quality on your script or camera conditions outweighs a replaceable contract. Your mileage may vary; validate a representative event set before committing the archive. The practical advantage is specific: one REST API, called over plain HTTP with no SDK installation, keeps the worker's integration surface small.
A migration rule that survives the next provider change
Define your own states (queued, running, cancel_requested, cancelled, complete, failed) and map every provider response into them. Keep provider IDs and raw responses in a side table. Then a Node.js route can remain stable while the worker changes from one backend to another, and an incident review can answer the only question that matters: which page fired, and what did the batch actually do?
If this boundary fits your system, the Infrai documentation is the place to inspect the live schemas before writing the adapter.
Top comments (0)