LLM inference calls can run for minutes when generating long completions or reasoning over large contexts. For iOS apps that need to survive app suspension or termination, you must use ios background urlsession llm requests to keep the call alive. This guide walks through the exact steps to implement that with Swift, from session configuration to parsing the response file.
Why data tasks fail in the background
Standard URLSessionDataTask instances are invalid inside a background session. The system suspends your app shortly after it goes to background, and a data task’s in-memory callback block is discarded. Background executions only support URLSessionUploadTask and URLSessionDownloadTask, both of which are tracked by the system daemon nsurlsessiond. That daemon continues the transfer and relaunches your app when needed.
If your LLM call is a simple POST with a JSON body, you cannot attach the body inline. You must write the request payload to a file and use uploadTask(with:fromFile:). The response body is delivered through delegate methods, not a completion closure.
Background session constraints
Before writing code, internalize the rules:
- The session identifier must be unique and stable across launches.
- Only HTTP/HTTPS endpoints are allowed.
- You cannot use
URLSessionConfiguration.ephemeralordefaultfor background. - The
URLSessionmust have a delegate; completion handlers on tasks are ignored in background mode. isDiscretionaryshould befalseif you need the call to run promptly; otherwise the system may batch it with other power-friendly work.- Resource timeout (
timeoutIntervalForResource) caps total transfer time; default is 7 days, but set it to match your LLM SLA.
Step 1: Configure the background session
Create the configuration once and store the identifier. Recreate the session in AppDelegate when the system wakes you.
let sessionIdentifier = "com.example.llm.background"
func makeBackgroundSession() -> URLSession {
let config = URLSessionConfiguration.background(withIdentifier: sessionIdentifier)
config.sessionSendsLaunchEvents = true
config.isDiscretionary = false
config.timeoutIntervalForResource = 60 * 15 // 15 minutes max
config.httpAdditionalHeaders = ["Authorization": "Bearer \(APIKey.store)"]
return URLSession(configuration: config, delegate: LLMTaskDelegate(), delegateQueue: nil)
}
Step 2: Serialize the LLM request to disk
Define a minimal OpenAI-compatible request struct and encode it to a temporary file. This avoids holding the JSON in memory.
struct ChatCompletionRequest: Encodable {
let model: String
let messages: [Message]
let stream: Bool = false
}
struct Message: Encodable {
let role: String
let content: String
}
func writeRequestFile() throws -> URL {
let req = ChatCompletionRequest(
model: "gpt-4o-mini",
messages: [Message(role: "user", content: "Summarize background URLSession")]
)
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("json")
try JSONEncoder().encode(req).write(to: url)
return url
}
Step 3: Start the upload task
Build the URLRequest and hand the file URL to the upload task. The system reads the file and streams it to the server.
func startLLMRequest() {
let session = makeBackgroundSession()
let bodyURL = try! writeRequestFile()
var request = URLRequest(url: URL(string: "https://api.example.com/v1/chat/completions")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(with: request, fromFile: bodyURL)
task.taskDescription = "llm-completion"
task.resume()
}
When designing ios background urlsession llm requests, remember that the request file must remain on disk until the task completes; deleting it early fails the task.
Step 4: Implement the delegate
You need URLSessionDataDelegate and URLSessionTaskDelegate. Accumulate response bytes into a file per task because the app may be dead when data arrives.
class LLMTaskDelegate: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate {
private var fileHandles: [Int: FileHandle] = [:]
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let path = FileManager.default.temporaryDirectory
.appendingPathComponent("resp-\(dataTask.taskIdentifier).bin")
FileManager.default.createFile(atPath: path.path, contents: nil)
fileHandles[dataTask.taskIdentifier] = try? FileHandle(forWritingTo: path)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
fileHandles[dataTask.taskIdentifier]?.write(data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let id = task.taskIdentifier
fileHandles[id]?.closeFile()
fileHandles.removeValue(forKey: id)
if let error = error {
print("LLM task failed: \(error)")
return
}
// Parse response file named resp-\(id).bin
}
}
Step 5: Reconnect after app termination
If the app was killed, iOS relaunches it in the background and calls application(_:handleEventsForBackgroundURLSession:completionHandler:). You must recreate the session with the same identifier and store the completion handler.
var backgroundSessionCompletion: (() -> Void)?
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
backgroundSessionCompletion = completionHandler
_ = makeBackgroundSession() // delegate will receive events
}
When all tasks for that session are delivered, call the saved completion handler inside urlSessionDidFinishEvents(forBackgroundURLSession:).
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
backgroundSessionCompletion?()
backgroundSessionCompletion = nil
}
}
Common pitfalls and tradeoffs
No streaming. Background tasks deliver the full response only after the server closes the connection. If your product relies on token streaming for UX, you must either keep the app foreground or use BGTaskScheduler for short prefetch, not long generations.
Testing is painful. The simulator does not faithfully simulate background termination. Lock a real device and wait; use xcrun simctl only for basic checks. Add logging to a file you can pull via Xcode organizer.
Auth token exposure. Writing the bearer token in httpAdditionalHeaders is fine, but if you embed it in the request file, anyone with the file system can read it. Prefer header injection at session level.
Provider rate limits. A single background task will not automatically retry against a different model if the first one is rate-limited. If you route ios background urlsession llm requests through a gateway such as n4n.ai, which exposes one OpenAI-compatible endpoint covering 240+ models with automatic fallback when a provider is degraded, you avoid hard-coding failover logic. The gateway returns a final response or a structured error, keeping your background delegate simple.
Timeouts. timeoutIntervalForRequest is per resource wait; timeoutIntervalForResource is total. Set the latter to comfortably exceed your longest expected LLM call.
Minimal end-to-end snippet
// AppDelegate.swift
let identifier = "com.example.llm.bg"
func start() {
let cfg = URLSessionConfiguration.background(withIdentifier: identifier)
cfg.sessionSendsLaunchEvents = true
let session = URLSession(configuration: cfg, delegate: LLMTaskDelegate(), delegateQueue: nil)
let reqURL = FileManager.default.temporaryDirectory.appendingPathComponent("body.json")
try! JSONEncoder().encode(ChatCompletionRequest(model: "gpt-4o", messages: [Message(role: "user", content: "Hi")])).write(to: reqURL)
var r = URLRequest(url: URL(string: "https://api.example.com/v1/chat/completions")!)
r.httpMethod = "POST"
r.setValue("Bearer key", forHTTPHeaderField: "Authorization")
session.uploadTask(with: r, fromFile: reqURL).resume()
}
Production checklist
- Unique session identifier stored in a constant.
- Request body written to
temporaryDirectorywith UUID name. - Delegate implements
didReceive response,didReceive data,didCompleteWithError. -
handleEventsForBackgroundURLSessionrecreates session. - Completion handler called in
urlSessionDidFinishEvents. - Response file parsed and deleted after success.
- Error paths surface to user via local notification.
Building ios background urlsession llm requests is not complex once you accept the file-based model and delegate-driven flow. The payoff is resilient long completions that survive network changes and app switches.