javascript

Building an Offline-Friendly Image Upload System

Most image upload systems assume one thing: the user has a stable internet connection.

That assumption breaks quickly on mobile devices.

A user might select 10 photos, lose connectivity halfway through the upload, close the app, and reopen it later. A traditional upload system may force them to start everything again.

An offline-friendly image upload system solves this by treating uploads as a queue rather than a single network request.

Learn software development

1. The Core Architecture

A reliable system typically looks like this:

User selects image
       ↓
Compress / resize image
       ↓
Save image locally
       ↓
Create upload queue record
       ↓
Check network connection
       ↓
     Online?
    /       \
  Yes        No
  ↓           ↓
Upload      Wait in queue
  ↓           ↓
Success     Network returns
  ↓           ↓
Delete      Upload
local copy

The important idea is simple:

Never depend on the network to preserve the user’s upload.

The local device should temporarily become the source of truth.

2. Store Images Locally First

Instead of immediately sending the selected image to your server, first save it locally.

For example, an upload queue might contain:

{
  id: "upload_123",
  filePath: "/local/uploads/photo.jpg",
  fileName: "photo.jpg",
  status: "pending",
  attempts: 0,
  createdAt: Date.now()
}

The database can be something like:

  • SQLite
  • IndexedDB
  • Core Data
  • Room
  • AsyncStorage for simple cases

For larger applications, a proper database is preferable because you need reliable queue management.

3. Compress Before Uploading

Images can consume enormous amounts of bandwidth.

A 5 MB camera image may contain far more resolution than your application actually needs.

Before adding the image to the upload queue:

Original image
      ↓
Resize
      ↓
Compress
      ↓
Convert if necessary
      ↓
Save locally
      ↓
Queue for upload

For example, you might resize an image to a maximum width of 1920px and use JPEG/WebP compression.

This provides three benefits:

  1. Faster uploads
  2. Lower data consumption
  3. Less storage usage

Do not blindly compress every image to the same quality. Profile your application’s requirements first.

4. Create an Upload Queue

The queue is the heart of the system.

Each image should have a state.

PENDING
UPLOADING
COMPLETED
FAILED

A more sophisticated system might use:

PENDING
UPLOADING
COMPLETED
RETRYING
FAILED
CANCELLED

The queue allows your application to remember what still needs to be uploaded.

For example:

[
  {
    id: "1",
    filePath: "/uploads/a.jpg",
    status: "completed"
  },
  {
    id: "2",
    filePath: "/uploads/b.jpg",
    status: "pending"
  },
  {
    id: "3",
    filePath: "/uploads/c.jpg",
    status: "retrying"
  }
]

When connectivity returns, the application processes the pending items.

5. Detect Network Connectivity

Your application should monitor connectivity rather than assuming that an HTTP request will succeed.

Conceptually:

if (isOnline()) {
  processUploadQueue();
}

When the device transitions from offline to online:

network.onChange((online) => {
  if (online) {
    processUploadQueue();
  }
});

The exact implementation depends on your platform.

However, remember:

Being connected to Wi-Fi does not necessarily mean the internet is reachable.

Your upload logic should still handle failed requests.

6. Upload With Retries

Networks fail.

Servers timeout.

Users enter elevators.

Mobile connections disappear.

Your upload system should therefore retry temporary failures.

A simple strategy:

async function uploadWithRetry(item) {
  let delay = 1000;

  for (let attempt = 1; attempt <= 5; attempt++) {
    try {
      await upload(item);
      return true;
    } catch (error) {
      await sleep(delay);
      delay *= 2;
    }
  }

  return false;
}

This is called exponential backoff.

Instead of repeatedly hammering your server:

1s → 2s → 4s → 8s → 16s

the client gradually increases the waiting period.

7. Make Uploads Resumable

For large images or videos, retrying the entire file can be wasteful.

Imagine a 100 MB upload reaches 95% and the connection disappears.

Restarting from zero wastes bandwidth.

A better architecture supports resumable uploads:

File
 ↓
Chunk 1 ──→ Server
Chunk 2 ──→ Server
Chunk 3 ──→ Server
Chunk 4 ──→ Server
       X connection lost

Reconnect

Chunk 4 ──→ Server
Chunk 5 ──→ Server

The server keeps track of which chunks have already arrived.

This becomes particularly valuable when dealing with large media files.

8. Prevent Duplicate Uploads

Offline queues introduce another problem:

What happens if the same image is uploaded twice?

Suppose the server successfully receives an image, but the client never receives the success response because the connection disappears.

The client may assume the upload failed and retry.

You can solve this with an idempotency key.

Generate a unique identifier for each upload:

const uploadId = crypto.randomUUID();

Send it with the request:

POST /uploads

Idempotency-Key: upload_123

The server stores the key.

If the same request arrives again, the server recognizes it and avoids creating another copy.

9. Keep the User Informed

Offline-friendly does not mean invisible.

The interface should tell users what is happening.

For example:

3 photos selected

✓ photo1.jpg — Uploaded
↑ photo2.jpg — Uploading...
⏳ photo3.jpg — Waiting for connection

When offline:

You're offline.

Your 4 photos are saved and will upload
automatically when you're back online.

This is much better than showing:

Upload failed

and forcing the user to figure out what happened.

10. Clean Up Local Files

Once the server confirms a successful upload, you can remove the temporary local file.

if (uploadSuccessful) {
  await removeLocalFile(item.filePath);
  await markAsCompleted(item.id);
}

But do this only after confirmation.

Never delete the local file immediately after starting the request.

Otherwise:

Start upload
     ↓
Delete local file
     ↓
Network fails
     ↓
Upload lost

Instead:

Start upload
     ↓
Server confirms success
     ↓
Mark completed
     ↓
Delete temporary file

11. A Practical Client Architecture

A clean implementation can separate responsibilities:

ImagePicker
     ↓
ImageProcessor
     ↓
LocalStorage
     ↓
UploadQueue
     ↓
NetworkMonitor
     ↓
UploadManager
     ↓
API

Each component has one job.

ImagePicker
Selects images.

ImageProcessor
Resizes and compresses them.

LocalStorage
Preserves files while offline.

UploadQueue
Tracks pending uploads.

NetworkMonitor
Detects connectivity changes.

UploadManager
Processes and retries uploads.

API
Receives and validates files.

This architecture is easier to test and maintain than putting everything inside one upload function.

12. Server-Side Considerations

The client is only half of the system.

Your backend should also handle:

  • File type validation
  • File size limits
  • Authentication
  • Unique upload identifiers
  • Duplicate detection
  • Storage failures
  • Partial uploads
  • Virus/malware scanning where appropriate
  • Image metadata validation
  • Object-storage integration

For production systems, images are often better stored in object storage rather than directly on the application server.

For example:

Mobile App
    ↓
Upload API
    ↓
Object Storage
    ↓
CDN
    ↓
Users

The database stores metadata such as:

{
  id: "img_123",
  userId: "user_456",
  url: "...",
  size: 245000,
  mimeType: "image/jpeg",
  uploadedAt: "..."
}

13. The Golden Rule

An offline-friendly image upload system should follow one principle:

Save first. Upload second.

The network should determine when an image is uploaded—not whether the user’s image survives.

A robust system therefore:

  • Saves images locally
  • Maintains an upload queue
  • Detects connectivity
  • Retries failed uploads
  • Uses exponential backoff
  • Prevents duplicates
  • Supports resumable uploads for large files
  • Shows upload progress
  • Cleans up successful uploads
  • Handles failures gracefully

Once you design uploads as a durable queue, rather than a single HTTP request, your application becomes significantly more reliable—especially on mobile networks where connectivity cannot be guaranteed.

Share
Published by
codeflare

Recent Posts

JavaScript Background Sync API

Imagine a user submits a form while their internet connection suddenly disappears. Normally, the request…

10 hours ago

10 Signs Your PC Has Been Hacked

Cybercriminals don't always announce their presence. Many compromises are designed to remain unnoticed for weeks…

1 week ago

What Killed jQuery?

For years, jQuery was everywhere. If you were building websites in the 2010s, there was a…

1 week ago

How to Resolve Git Merge Conflicts

Few things halt a developer’s flow faster than seeing the dreaded word: CONFLICT. A Git merge…

2 weeks ago

How to Create REST APIs in Java Spring Boot

Spring Boot is one of the most popular frameworks for building REST APIs in Java.…

2 weeks ago

Perks of Being a Copy-Paste Developer

Why borrowing code is a skill—when you understand what you're copying. For years, "copy-paste developer"…

4 weeks ago