Image Upload Pipeline for Mobile Chat
User taps the attach button, selects a photo from the gallery — and waits. If nothing happens, they tap again. This is a common scenario when image upload is implemented without a queue and progress indicator. The result: duplicates in chat, app crash on a slow network, and a one-star review. Our experience shows that even simple photo sending requires attention to detail: a proper pipeline eliminates 90% of performance and UX issues. Contact us to implement reliable image sending in your app.
Why a Naïve Implementation Leads to Errors
The most common mistake is uploading the original image directly. Modern smartphone cameras produce 4–12 MB photos. Sending such a file via multipart/form-data without prior compression means: long wait times, UI freeze on the main thread (if compression is done there), and duplicates when the user retries after a timeout.
On iOS, a typical implementation uses PHPickerViewController (iOS 14+), which provides an NSItemProvider from which you asynchronously obtain a UIImage. If you do this synchronously in a completion handler and immediately call ImageIO for resizing, the UI will freeze for ~300 ms on an iPhone 12, and even longer on an iPhone SE 2nd gen. The correct approach: load data in the background via loadObject(ofClass:), then dispatch to DispatchQueue.global(qos:.userInitiated) for compression using vImage or UIGraphicsImageRenderer with a target size suitable for the receiver's screen. Apple Developer Documentation
On Android, a similar problem occurs with ActivityResultContracts.GetContent(); decoding a Bitmap on the main thread via BitmapFactory.decodeStream() without inSampleSize leads to an OutOfMemoryError on devices with 2 GB RAM when selecting multiple photos in a row.
How We Build a Reliable Upload Pipeline
Our complete pipeline includes the following steps:
-
Selection and validation. On iOS —
PHPickerViewControllerwithfilter: .images, limit viaselectionLimit. We check MIME type usingUTTypebefore loading data. On Android —PhotoPicker API(Android 13+) orIntent(Intent.ACTION_PICK)for older versions; validation viaContentResolver.getType(). In React Native we usereact-native-image-pickerwithmediaType: 'photo'. -
Compression. Target: no more than 1 MB for chat thumbnails. On iOS:
UIGraphicsImageRendererwith target size 1280×1280,jpegData(compressionQuality: 0.75). On Android:Bitmap.createScaledBitmap()+compress(Bitmap.CompressFormat.JPEG, 80, outputStream). In Flutter we useflutter_image_compress— it calls native codec, so it does not block the Dart isolate. -
Upload with progress. Multipart upload via
URLSession.uploadTask(with:from:)on iOS with delegateurlSession(_:task:didSendBodyData:)for progress. On Android —OkHttpwithRequestBody.create()and a customCountingRequestBody. In React Native,axioswithonUploadProgressis convenient, but note: the progress event fires on the JS thread, so state updates must be debounced. -
Optimistic UI. Show a thumbnail immediately after selection, with a "uploading" status while the upload proceeds. If the request fails, do not delete the message; show a retry button instead. Each message must have a local
localIdand a status (pending/sent/failed). -
Full-screen preview. On iOS —
UIScrollView+UIImageViewwith pinch-to-zoom viaUIPinchGestureRecognizer. Lazy loading of the original when opening, usingSDWebImageorKingfisher. On Android —PhotoViewlibrary orZoomableImageViewfromcoil+accompanist.
| Platform | Tool | Resize | Quality | Final size (from 12 MB photo) |
|---|---|---|---|---|
| iOS | UIGraphicsImageRenderer | 1280×1280 | 0.75 | ~800 KB |
| Android | Bitmap.compress + createScaledBitmap | 1280×1280 | 80% | ~900 KB |
| Flutter | flutter_image_compress | maxWidth:1280 | 80 | ~850 KB |
| React Native | react-native-image-resizer | maxWidth:1280 | 80 | ~850 KB |
Contact us to implement this pipeline in your app today.
Storage and CDN
Images are not stored in the database. We upload to an S3-compatible storage (AWS S3, Cloudflare R2, MinIO) and write only the URL to the chat. For thumbnails, we generate them server-side via Lambda/Cloud Function on upload — this frees the client from re-compressing when displaying the message list.
Presigned URLs with a TTL of 1–24 hours are mandatory for private chats. On the client, we cache via NSCache (iOS) or DiskLruCache (Android). We use a CDN with edge caching for fast delivery even to regions with high latency.
Process and Timeline
Basic implementation (selection, compression, upload, thumbnail, full-screen preview) — 2–3 days with a ready backend. If you need multi-select (up to 10 photos), an upload queue with pause/resume, and GIF support — add 1–2 days. Cost is determined individually after analyzing requirements.
What's Included
- API and integration documentation
- Source code of the pipeline with comments
- CDN and presigned URL setup
- Integration with your existing backend (REST/GraphQL)
- Testing on slow networks and edge cases
- Support during App Store and Google Play release
Common Mistakes and Their Solutions
| Mistake | Consequences | Solution |
|---|---|---|
| Uploading original without compression | Long wait, data waste | Compress to 1 MB |
| Synchronous processing on main thread | UI freeze | Asynchronous background processing |
| No progress indicator | Repeated taps, duplicates | Progress bar and button lock |
| No retry on failure | Photo loss, user frustration | Retry with local 'failed' status |
| Ignoring thumbnail caching | Frequent downloads, lag | Cache via Kingfisher/Coil |
| Not using presigned URLs | Security risk for private chats | Signed URLs with TTL |
Get a free consultation on implementing image sending — just contact us.







