DEV Community

Gathmo
Gathmo

Posted on

Resumable Browser Uploads for Crowded Event Networks

Event uploads fail differently from normal office uploads. A wedding guest may move between venue Wi-Fi and mobile data, lock the phone while a video is transferring, or close the browser as soon as the progress bar reaches 100%. Hundreds of devices can share one access point, and users rarely wait around to diagnose an error.

The usual POST request plus optimistic success toast is not enough. A reliable browser flow needs a small protocol that distinguishes local preparation, network transfer, server acceptance, media processing, and final availability.

This article describes a platform-neutral design for that protocol.

The five states users actually experience

Model each file as a durable state machine:

selected
  -> preparing
  -> transferring
  -> accepted
  -> processing
  -> ready
Enter fullscreen mode Exit fullscreen mode

Add terminal or recoverable branches:

preparing    -> rejected_local
transferring -> paused | retryable_error | expired
accepted     -> processing_error
processing   -> ready | processing_error
Enter fullscreen mode Exit fullscreen mode

The key distinction is between transferring and accepted. The browser may have sent every byte while the server has not yet committed the upload. Showing “done” at that boundary creates the most frustrating failure: the guest deletes the original, but the organizer never receives it.

Give every file a client-generated identity

Create an upload ID before the first network request. A UUID is sufficient when combined with the event identifier:

const uploadId = crypto.randomUUID();

const uploadIntent = {
  uploadId,
  eventId,
  name: file.name,
  size: file.size,
  type: file.type,
  lastModified: file.lastModified,
};
Enter fullscreen mode Exit fullscreen mode

Send that identity when creating the server-side upload session. If the browser retries after a timeout, the server returns the existing session instead of creating a duplicate.

This is idempotency at the workflow level. A guest can tap “retry” without having to understand whether the first request reached the server.

Separate the control plane from file bytes

Use a small JSON API for session creation and status, then transfer bytes through a resumable object-storage protocol or chunk endpoint.

POST /events/:eventId/upload-sessions
PUT  /upload-sessions/:uploadId/parts/:partNumber
POST /upload-sessions/:uploadId/complete
GET  /upload-sessions/:uploadId/status
Enter fullscreen mode Exit fullscreen mode

The create response should include:

  • the canonical upload ID;
  • part size;
  • expiry time;
  • already committed parts;
  • limits and accepted media types;
  • a short-lived upload credential;
  • the status polling URL.

Do not expose permanent storage credentials or organizer privileges to a guest browser.

Choose chunks for mobile recovery, not maximum throughput

Very small parts add request overhead. Very large parts make every interruption expensive. For event photos and short videos, starting around 5–10 MB per part is often a sensible compromise, but the server should be able to adjust by file size and network observations.

Limit concurrency on mobile devices:

const maxConcurrentParts = navigator.connection?.saveData ? 1 : 2;
Enter fullscreen mode Exit fullscreen mode

More parallel requests can reduce throughput on a congested access point. Two predictable streams are often better than eight competing streams.

Persist only metadata that is safe to persist

Store the upload session metadata in IndexedDB so a page reload can recover state:

await db.put('uploads', {
  uploadId,
  eventId,
  name: file.name,
  size: file.size,
  committedParts,
  expiresAt,
  state: 'transferring',
});
Enter fullscreen mode Exit fullscreen mode

Browsers cannot always reopen the original File object after a restart. The UI must say so honestly: “Select the same video to resume.” Compare name, size, modification time, and optionally a lightweight fingerprint before attaching the new file handle to the existing session.

Do not persist private event administration links, access tokens with long lifetimes, or unnecessary media metadata.

Treat network changes as normal

navigator.onLine is only a hint. A device can be online while a captive portal blocks the upload host. Use request outcomes and timeouts as the source of truth.

Pause exponential backoff when the page is hidden, but do not cancel a part that is already close to completion. Add jitter so hundreds of guests do not retry at the same instant after venue Wi-Fi recovers:

function retryDelay(attempt) {
  const base = Math.min(30_000, 1_000 * 2 ** attempt);
  return base / 2 + Math.random() * (base / 2);
}
Enter fullscreen mode Exit fullscreen mode

After switching networks, query committed parts before resuming. The last response may have been lost even though the server stored the part.

Confirm server acceptance explicitly

The completion endpoint should verify the part set, expected byte count, content constraints, and checksum where available. It returns an accepted state only after the durable object exists.

{
  "uploadId": "…",
  "state": "accepted",
  "receivedBytes": 48277123,
  "processingStatusUrl": "/upload-sessions/…/status"
}
Enter fullscreen mode Exit fullscreen mode

The browser can now tell the guest that the original is safely received. Processing may still generate previews, normalize video, scan content, or place the item into moderation.

Design the confirmation screen for impatient humans

A useful confirmation has three layers:

  1. Received — the server has the original.
  2. Preparing — previews or video processing are still running.
  3. Available — the organizer workflow can use the item.

Never use only color. Include text, an icon, and a stable upload ID that support can reference. Let the guest add another item without losing the completed receipt.

In an app-free event workflow such as Gathmo's browser upload flow, that clarity matters because a guest may interact with the product only once. There is no installed app to send a later recovery notification.

Protect the event from abandoned sessions

Upload sessions need expiry and cleanup:

  • short-lived credentials;
  • a maximum number of active sessions per guest or network;
  • server-side size and type validation;
  • cleanup of incomplete multipart objects;
  • rate limits that tolerate retries;
  • an abuse path that does not block legitimate crowded-event traffic.

Keep accepted originals separate from uncommitted parts. Cleanup jobs must never infer abandonment from a missing browser heartbeat after acceptance.

Observability without tracking guests

Operational metrics can remain privacy-preserving:

  • sessions created;
  • acceptance rate;
  • median attempts per part;
  • bytes retransmitted;
  • time from selection to acceptance;
  • time from acceptance to ready;
  • failure reason by browser and coarse network type;
  • duplicate intents merged by idempotency key.

Avoid storing full filenames, precise IP history, or persistent cross-event identifiers in analytics when aggregates are enough.

Test the failure boundaries

The happy path is the least interesting test. Automate and rehearse:

  • timeout after the server commits a part but before the browser receives the response;
  • refresh midway through a transfer;
  • captive portal redirect;
  • Wi-Fi-to-mobile transition;
  • expired upload credential;
  • duplicate completion request;
  • file re-selection with the wrong file;
  • processing failure after acceptance;
  • event upload window closing during transfer;
  • browser closure immediately after the last progress event.

For every test, assert both server state and user-visible wording.

The reliability contract

A reliable event upload flow makes four promises it can prove:

  1. Retry does not create accidental duplicates.
  2. “Received” means the server durably accepted the original.
  3. The guest can recover from common mobile-network interruptions.
  4. The organizer can distinguish transferring, accepted, processing, and ready media.

That contract is more valuable than a perfectly smooth progress animation. Event guests forgive a brief retry. They do not forgive a success message for a file that never arrived.

Top comments (0)