An upload form that works perfectly on office Wi-Fi can fail spectacularly at a wedding or conference. Hundreds of phones share one access point, mobile reception drops between rooms, and users close the browser as soon as they think the upload is done.
The hard part is not accepting a file. It is designing a flow that remains understandable when every assumption about connectivity is wrong.
This article describes the reliability model I use for browser-based event uploads: explicit client states, direct object-storage uploads, bounded retries, persistent intent, and confirmation that reflects server truth rather than UI optimism.
Start with a state machine, not a spinner
A single isUploading boolean hides the situations that matter most. A useful client model separates at least these states:
-
selected— the browser has access to the local file. -
preparing— metadata validation, optional image resizing, and checksum work are running. -
authorizing— the client is requesting an upload URL or session from the API. -
transferring— bytes are moving to object storage. -
confirming— the application API is registering the completed object. -
complete— the server has acknowledged the media record. -
retryable_error— the operation can continue without selecting the file again. -
terminal_error— the file or policy is invalid and needs user action.
This distinction prevents one of the most damaging UX bugs: showing success after storage accepted the bytes but before the application created the corresponding database record.
The UI should render from the state machine. “Uploading 63%” belongs to transferring; “Finishing…” belongs to confirming; “Added to the album” belongs only to complete.
Upload media directly to object storage
Routing large photos and videos through the application server adds an unnecessary bottleneck. A more resilient path is:
browser -> API: request upload authorization
API -> browser: signed URL + object key + constraints
browser -> object storage: upload bytes
browser -> API: confirm object key and metadata
API -> browser: canonical media record
The API remains responsible for authorization, quotas, file policy, ownership, and the final media record. It does not need to proxy every byte.
Signed upload URLs should be short-lived, bound to one object key, and limited to the expected content type and size where the storage provider supports those constraints. Never let the browser choose an arbitrary bucket key that becomes trusted application data.
Make confirmation idempotent
The final confirmation request is especially vulnerable. The storage upload may have succeeded, but the phone can lose connectivity before receiving the API response. If a retry creates a second record, the user sees duplicate media even though only one object exists.
Give each upload attempt a stable client-generated ID. The confirmation endpoint should treat that ID as an idempotency key:
{
"uploadAttemptId": "01J...",
"objectKey": "events/evt_123/uploads/01J....jpg",
"originalName": "IMG_4821.jpg",
"contentType": "image/jpeg",
"size": 3842017
}
If the same confirmation arrives twice, return the same media record. This turns an ambiguous network timeout into a safe retry.
Retry deliberately
Automatic retries are useful only when they are bounded and visible.
For authorization and confirmation requests, exponential backoff with jitter is usually appropriate. For example: 500 ms, 1.5 s, 4 s, then stop and show a retry button. Do not create an infinite background loop on a guest’s phone.
For the byte transfer, retry policy depends on file size and storage support. Restarting a 2 MB image is reasonable. Restarting a 300 MB video at 95% is not. Large media should use multipart or resumable uploads so the client can continue from completed chunks.
Classify errors before retrying:
- Retry: timeouts, connection resets, and most 5xx responses.
- Refresh authorization, then retry: expired signed URL.
- Do not retry automatically: unsupported type, quota exceeded, authentication failure, or a 413 size rejection.
The error message should say what happens to the selected file. “Connection lost — your photo is still selected” is much more useful than “Something went wrong.”
Preserve upload intent across short disruptions
Mobile browsers are aggressive about suspending pages. At minimum, persist lightweight upload metadata in IndexedDB: attempt ID, object key, state, filename, size, and timestamps.
Persisting the actual file is more complicated. A File object cannot always be recovered after a browser restart, and large Blobs consume meaningful storage. Use this as a progressive enhancement rather than promising offline uploads everywhere.
Even without persisting the bytes, durable metadata helps reconcile uncertainty. On page return, the client can ask the API whether an attempt was confirmed and avoid presenting an already completed item as failed.
Treat progress as an estimate
Upload progress measures bytes sent by the client, not necessarily durable application completion. Reserve the final visual step for server confirmation.
A practical progress mapping is:
- 0–10%: preparing and authorizing
- 10–90%: byte transfer
- 90–99%: confirmation and processing
- 100%: canonical record returned
This is not mathematically pure, but it communicates the real workflow. A bar stuck at 100% while the UI still says “processing” feels broken; a bar at 95% with “Finishing…” is honest.
For multiple files, show both per-item state and batch progress. One failed video should not make twelve successful photos look unsuccessful.
Validate twice
Client validation improves feedback, but server validation is authoritative. Check size, type, count, and basic media constraints before authorization, then verify them again during confirmation or asynchronous processing.
Do not trust a filename extension or browser-provided MIME type. Inspect file signatures server-side, isolate untrusted media, and generate derivatives in a controlled worker process.
For event products, moderation is a separate state from upload success. A guest should see that their file arrived even if it is waiting for host approval. Conflating “not publicly visible” with “upload failed” causes repeated submissions.
Design for abandonment
Most guests are not committed users. They scanned a QR code, chose three photos, and expect to return to the party. Reliability therefore includes reducing the time during which the page requires attention.
Keep the capture route free of account creation, defer nonessential work, and give a clear completion signal. If a transfer can continue safely while the user browses within the page, say so. If closing the tab will cancel it, say that instead.
This constraint shaped the upload flow we use at Gathmo, where guests contribute event photos, videos, and voice messages directly from a mobile browser. The product context is specific, but the engineering lesson generalizes: the network is not a reliable boundary, so every transition needs an observable and recoverable state.
A compact reliability checklist
Before shipping a mobile media flow, test these cases intentionally:
- The signed URL expires before transfer starts.
- Connectivity disappears at 5%, 60%, and immediately after storage success.
- Confirmation is sent twice.
- The browser is backgrounded during transfer.
- One file in a multi-file batch is rejected.
- The user taps upload twice.
- A large video exceeds policy.
- The object exists but confirmation never completed.
- Confirmation exists but the client never received the response.
- Moderation delays public visibility.
The happy path proves that uploads work. These failure paths prove that the product can be trusted at the exact moment users have only one chance to contribute.
Top comments (0)