Instagram uploads your photo before you tap Share. Here's how to build that properly

2026-11-15来源 dev.to UXEnglish
Instagram uploads your photo before you tap Share. Here's how to build that properly

Open Instagram, pick a photo, write a caption, tap Share. The post shows up almost instantly, even on a weak connection.

The app isn't uploading faster than anyone else. It started uploading before you tapped Share.

Instagram's co-founder Mike Krieger described this in a 2011 talk, Secrets to Lightning Fast Mobile Design, under a principle he called "move bits when no one is watching". As soon as you pick the photo and move on to the caption screen, the upload starts in the background. By the time you finish typing, the bytes are on the server. Share only has to send a tiny request: "create a post with this upload and this caption". If you back out, the client cancels and the server deletes the data.

Timeline: the naive flow uploads after Share; the speculative flow uploads while the user writes the caption

The idea fits in one sentence. Building it so it is reliable, cheap and respectful of the user's data takes more care. This is how I'd design it today, as a mobile engineer who started out in QA, breaking exactly this kind of flow.

I explained the trick in Arabic in a 25-second short: https://youtube.com/shorts/NXIoam83xdY

1. Split "upload" from "post"

The whole trick depends on one API decision: uploading media and creating a post are two separate calls.

POST /v1/uploads (body: image bytes, header: Idempotency-Key) → 201 { "upload_id": "up_8f3c…", "expires_at": "…" } POST /v1/posts (body: { upload_id, caption, … }) → 201 { "post_id": "…" } 

The upload is an orphan until a post claims it. That gives you three properties:

  • Share is cheap. The request on the critical path is a few hundred bytes of JSON, not megabytes of image.
  • Retries are safe. The Idempotency-Key, a hash of the processed image bytes, makes a retried or duplicated upload return the same upload_id instead of storing the file twice.
  • Abandonment is a server concern, not a client hope (see section 4).

If your backend currently takes the image and the caption in one multipart request, this is the change to make first. Nothing on the client helps until it exists.

2. Decide exactly when to start

"Start early" needs a precise trigger. Start too early and you upload photos the user was only browsing. Start too late and there's no time left to hide.

A good trigger is the moment the media is final enough: the user confirmed the selection and left the crop/filter step. If edits can still happen later, you have two options:

  • Upload the original and send edit parameters with the post. The server renders the final image. One upload, any number of edits.
  • Upload the rendered image and restart on change. Simpler server, but every edit after the trigger cancels the in-flight upload and starts a new one. The content-hash key makes the restart cheap if the user undoes back to a version that was already uploaded.

3. Make the upload survive the real world

The user will lock the phone, switch apps, walk into an elevator. A speculative upload that dies on onPause hides nothing.

  • iOS: use a background URLSession with uploadTask(with:fromFile:). Background sessions only accept uploads from a file, so write the processed image to disk first. The system continues the transfer while your app is suspended and relaunches you to handle completion.
  • Android: enqueue the upload as WorkManager work, expedited, with a network constraint, so it survives process death. Keep the in-memory Deferred for the fast path while the screen is open.
  • Flutter / KMP: keep the orchestration in shared code and delegate the transfer itself to the platform mechanism above (via a plugin or expect/actual). A Dart http call or a Ktor call on its own will not outlive the process.
  • Large media: make it resumable (chunked with offsets, or the tus protocol), so a 40 MB video that reached 80% doesn't restart from zero after a network change.

4. Abandoned uploads are your responsibility

This is the part most write-ups skip. You put the user's photo on your server before they agreed to post it.

  • Cancel is best effort. When the user backs out or picks a different photo, send DELETE /v1/uploads/{id}. Assume this call will sometimes never arrive: app killed, no network, battery died.
  • The server must clean up anyway. Unclaimed uploads expire, e.g. a job that hard-deletes uploads with no post after a few hours:
 DELETE FROM uploads WHERE post_id IS NULL AND created_at < now() - interval '6 hours'; 

The blob in object storage needs the same rule (a lifecycle policy on an unclaimed/ prefix, with the object moved on claim).

  • Treat unclaimed media as untouched. No thumbnails shown to anyone, no indexing, no ML pipelines, no analytics on it until a post claims it. Legally and ethically, an unshared photo is still the user's private file. Say so in your privacy policy.

5. Don't spend the user's data plan on a guess

A speculative upload that gets abandoned is wasted bandwidth, and on a metered connection that's the user's money.

  • Respect Low Data Mode on iOS (allowsConstrainedNetworkAccess = false for the speculative session) and metered networks on Android (ConnectivityManager.isActiveNetworkMetered).
  • On those networks, either wait for Share as before, or speculatively upload a smaller rendition and send the full one only after Share.
  • Measure the abandon rate: if 40% of speculative uploads are never claimed, your trigger is too early.

6. The race when Share comes first

Sometimes the user is faster than the network. Share is tapped while the upload is at 60%.

The post request must wait for the upload, not fail and not start a second one. In Kotlin coroutines the shape is:

class Composer(private val api: Api, private val scope: CoroutineScope) { private var upload: Deferred<UploadId>? = null fun onMediaFinal(file: File) { upload?.cancel() // a different photo replaces the old one upload = scope.async { api.upload(file, idempotencyKey = file.sha256()) } } suspend fun onShare(caption: String): PostId { val id = (upload ?: error("no media")).await() // waits if still running return api.createPost(id, caption) // the only call on the critical path } fun onDiscard() { val pending = upload ?: return upload = null if (pending.isActive) { pending.cancel() // partial upload: the server TTL removes it } else { scope.launch { runCatching { api.deleteUpload(pending.await()) } } // finished: delete now, best effort } } } 

Pair onShare with optimistic UI: insert the post into the feed with the local image immediately and reconcile when the server answers. That's the other Krieger principle, "perform actions optimistically". If the post ultimately fails, show it in place with a retry, never a silent disappearance.

7. How I'd test it

With my QA background, this is the matrix I'd run before trusting it:

Scenario Expected
Share after upload finished Post appears instantly; only one POST /posts
Share at 60% upload UI shows the post immediately; post call waits, then succeeds
Double-tap Share One post (idempotent post creation, button disabled after first tap)
Pick photo A, then switch to B A cancelled + deleted; only B claimed
Back out of composer DELETE sent; if offline, upload expires via TTL
Airplane mode mid-upload, then back Upload resumes, not restarts
App killed mid-upload (iOS + Android) Transfer completes in the background or resumes on next launch
Low Data Mode / metered network No speculative full-size upload
Same photo uploaded twice Same upload_id (content-hash key)
Unclaimed upload after TTL Row and blob both gone

8. Measure the thing users feel

The metric is not upload speed. It is time from Share tap to post visible, at p50 and p95, split by network type. Track it next to the abandon rate and wasted bytes. Then you can tell whether the speculation is paying for itself.

The point

The app isn't faster. It spends time the user was going to spend anyway: picking a filter, writing a caption, thinking. A lot of what feels "fast" in good apps is really about where the waiting goes.

I'm Mohamed Elgohary, a Mobile & Product Engineer (Flutter, Kotlin Multiplatform, Android). I build apps from the idea to the store, and I test them the way a QA engineer would. «خلف الكواليس» is my Arabic short series about the tricks behind apps you use every day.

原文:https://www.a8q9.cn/en/article/instagram-uploads-your-photo-before-you-tap-share-here-s-how-to-build-that-prope-bb0b0a.html

相关文章