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.
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:
- Faster uploads
- Lower data consumption
- 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.

Latest tech news and coding tips.