Skip to content
Mobile & High-Liability Systems9 min

Reading the whole file into memory — and billing the user for it

An upload that loads the entire file into RAM and pushes it over whatever network happens to be active. Two failure modes in one line of code: an out-of-memory crash and a reverse-billed data charge the user never agreed to.

Failure modeA media upload reads the full file into memory and transmits it with no network-type check, no size guard, and no resumability — so large files trigger out-of-memory kills and metered-network uploads bleed the user's data bundle, silently and without consent.

Commercial risk

A single 500 MB upload on a metered mobile connection is real money out of the user's pocket — an out-of-bundle charge they never approved, on a connection your app chose for them. In markets where most users are on prepaid data, that one upload can cost more than a month of your subscription.

It is also a crash and a churn event. Loading a large file into memory triggers an out-of-memory kill that reads to the user as “the upload failed for no reason,” so they retry — re-reading and re-sending the whole file, multiplying both the crash risk and the data charge each time.

This is the mobile failure mode the Engineering Authority Playbook calls the reverse-billing out-of-bundle trap, and it hides inside the most innocuous-looking line in the codebase: `let data = try Data(contentsOf: url)`. That single call reads an entire file into RAM, and everything downstream inherits its two liabilities — unbounded memory and unbounded, unconsented data usage.

The reason it survives review is that it works flawlessly for the files developers test with: a few megabytes over office Wi-Fi. The failure only appears with a real user's 2 GB video on a prepaid cellular connection — which is to say, in exactly the conditions you cannot see from your desk.

1. The whole-file-into-memory read

Reading a file into a single in-memory buffer allocates as many bytes as the file is large. A 2 GB video allocates 2 GB of RAM on a device that may have far less available to a single app. The OS fires a memory warning, evicts what caches it can, and when that isn't enough it kills the app outright. From the user's side there is no error dialog and no explanation — the app simply vanishes mid-upload, and the content they thought they were posting never arrives.

The fix is to never hold the whole file. Stream it: hand the upload API a file URL or an input stream and let the networking layer read and transmit in bounded chunks. `URLSession`'s `uploadTask(with:fromFile:)` does exactly this, with a flat memory profile regardless of file size, and it is barely more code than the version that crashes.

// Wrong: entire file resident in memory before a single byte is sent.
let data = try Data(contentsOf: fileURL)   // 2 GB allocation
var request = URLRequest(url: uploadURL)
request.httpBody = data
URLSession.shared.dataTask(with: request).resume()

// Right: stream from disk — flat memory, any file size.
let task = URLSession.shared.uploadTask(with: request, fromFile: fileURL)
task.resume()

2. No network-type check: spending the user's bundle without asking

The second liability is that the upload uses whatever network is active, with no check and no consent. If the user is on cellular, a large upload is charged against their data bundle — and if the bundle is exhausted, against out-of-bundle rates, which are typically punitive. The app made an expensive financial decision on the user's behalf and never told them.

The platform gives you the controls to prevent this; the failure is not using them. `URLSessionConfiguration.allowsCellularAccess = false` for large transfers forces Wi-Fi. `isDiscretionary` with a background session lets the OS wait for favourable conditions. At minimum, check the interface type and prompt before spending a user's data on a large upload. Consent is a one-line gate that turns a silent charge into a user's informed choice.

let config = URLSessionConfiguration.background(withIdentifier: "uploads")
config.allowsCellularAccess = false     // large media: Wi-Fi only by default
config.isDiscretionary = true           // let the OS pick a good moment
config.sessionSendsLaunchEvents = true

// Or, if cellular is genuinely needed, make it the user's decision:
if path.usesInterfaceType(.cellular), fileSize > largeUploadThreshold {
    // Prompt: "This is a 480 MB upload on mobile data. Continue?"
}

3. No resumability: every failure re-bills the whole file

The third liability compounds the first two. Without resumable uploads, a transfer that fails at 99% must start over from zero — re-reading the file into memory (crash risk again) and re-sending every byte (data charge again). A user on a flaky connection can pay for the same 500 MB three or four times before the upload completes or they give up.

Resumable, chunked upload protocols — tus, S3 multipart, or a simple ranged-append endpoint of your own — mean a failure costs only the unsent remainder. Combined with a background session that survives the app being suspended, the upload becomes something the user can start and forget, instead of something they must babysit and pay for repeatedly.

One line — read the whole file into memory and send it over the active network — carries three liabilities: an out-of-memory crash, an unconsented data charge, and a re-billed retry loop. None of them appear in a Wi-Fi demo with a small file, and all of them appear for a real user with a real video on a real prepaid connection.

Stream from disk instead of buffering, default large transfers to Wi-Fi and make cellular the user's explicit choice, and use resumable chunks so a failure costs the remainder and not the whole. The user's data bundle is their money; an upload that spends it silently is a defect, not a feature.

Need this level of structural integrity engineered for your system?

We audit and re-architect systems where a single one of these failure modes costs real money. Fixed scope, fixed price, defined delivery date.

Request a Fixed-Scope Architecture Blueprint