← All notes
Native iOS Development6 min

A background upload is not a running process

When a field photo or a capture needs to leave the device after the user locks the phone, the instinct is to stretch a background window — another intent, another progress tick, another hope the process stays alive. That is the wrong tool. A background URLSession transfers ownership of the bytes to the system. Your process can die. The upload can continue. The work that remains yours is the queue, the file on disk, and what you show when the system comes back with a result.

LongRunningIntent keeps you alive; URLSession replaces you

We already treat App Intents as a short budget: roughly thirty seconds unless you adopt LongRunningIntent and keep reporting progress. That path is for work that still needs your code — draining a local sync unit, exporting a session, running an on-device pass. The system extends the window because your process is still doing the job.

A large upload is different. The product promise is that the file arrives, not that your app stays resident. URLSessionConfiguration.background(withIdentifier:) hands the transfer to nsurlsessiond. The session can outlive suspension and termination. Completions arrive later, sometimes only when the system relaunches you for events. If you model that as "keep the intent running until percentDone hits 100," you are fighting the platform and burning a budget that was never meant for the bytes.

The practical split on Endeo- and BuilderHelp-shaped work: intents and in-app buttons enqueue durable upload jobs. The background session owns the wire. Progress in a Live Activity can reflect job state from your store, not a fantasy that your process is the only thing moving the file.

Upload from a file URL, or do not claim background

Background sessions support uploadTask(with:fromFile:) and download tasks. They do not support uploading from an in-memory Data blob, and they do not support the closure-based convenience APIs people reach for in foreground code. If the bytes only exist in RAM, you do not have a background upload — you have a foreground call that will die with the process.

That forces a boring pipeline: write the payload to a known path, record the job against that path and a stable id, then create the task from the file. Temp files that vanish when the app is cleaned up are not good enough. Treat the on-disk artifact as part of the job record until the server ack is durable.

One session instance per identifier. Creating a second URLSession with the same background identifier is a crash class, not a style nit. Own the session in a long-lived coordinator, restore it on cold launch with the same id, and route handleEventsForBackgroundURLSession through that owner so the completion handler is always called.

Your state machine has to survive the handoff

The system finishing a task is not the same as your product finishing a job. Delegate callbacks can arrive after relaunch, after the user has moved on, or clustered when several transfers complete while the app was gone. Persist queued / uploading / succeeded / failed against the job id before you resume the task, and make success idempotent on the server side the same way you would for a webhook.

Retries belong in that store. A background task that gets a 500 often stops; discretionary scheduling can delay a transfer for hours if you asked for isDiscretionary. Decide per job whether the user was told "started" — field receipt photos usually are — and prefer non-discretionary for those, while still respecting expensive and constrained networks when the file is large.

Partial server state matters too. Multipart or resumable uploads leave debris if the client dies mid-flight. Prefer APIs that can resume by range or by upload id, and give the backend a way to expire abandoned parts. Blindly restarting a multi-hundred-megabyte put from byte zero after every radio blip is how "background upload" becomes a battery complaint.

How we verify the handoff

Start an upload, background the app, then force-quit it. Confirm the transfer can still complete and that relaunch delivers the event into your coordinator — not only that a spinner looked good while the debugger kept the process warm.

Add the transitions that already break field apps: start on wifi and walk away, lock mid-transfer, toggle airplane after the task is created, return hours later with the app cold. Check that the UI never claims done before the durable ack, and that a failed job is retryable without duplicating a server object.

If the only green path is "leave the app open until the bar fills," you shipped a foreground upload with background branding. Fix the file handoff and the job store before you tune progress chrome.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us