On this page
- Separate three kinds of work
- Interactive streaming generation
- Finite cleanup
- Durable remote work
- Most ordinary apps should expect suspension
- Persist state before you need it
- Persist partial output incrementally
- The partial message needs an explicit finish state
- Backgrounding is not user cancellation
- Do not blame the provider for lifecycle suspension
- Short background execution extensions are for bounded critical work
- Always implement the expiration path
- BackgroundTasks is not an interactive-stream keeper
- Background URL sessions solve file transfers, not arbitrary token streaming
- Long-running AI work is better as remote durable state
- Persist the remote ID before relying on background completion
- Unknown create outcomes need reconciliation
- Webhooks help servers more than purely local mobile clients
- Notifications are separate from execution
- Local models stop when the local runtime stops
- LAN-hosted models have two lifecycles
- On foreground, reconcile before blindly retrying
- Streaming endpoints often require semantic completion
- Cancellation while backgrounding needs policy
- Tool calls make backgrounding more sensitive
- Side-effecting tools require outcome certainty
- Request queues should persist lifecycle transitions
- Deadlines continue while the app is suspended
- Avoid background busy loops
- Observability should include lifecycle context
- UI should tell the truth after recovery
- Preserve drafts/user input independently
- App termination should be part of tests
- Do not promise recovery you cannot implement
- A lifecycle architecture
- A backgrounding checklist
- Where BYOKchat fits
- Further reading
A desktop AI client can often assume that an active process will keep running until the user closes it.
A mobile app cannot.
On iOS, an app that moves to the background may receive limited execution time and can then be suspended. Some specific background modes and system-managed background task APIs allow work under defined conditions, but an ordinary interactive AI stream should not be designed around the assumption that the app will execute indefinitely after the user leaves it.
That changes how a reliable chat client should model generation.
The key principle is:
Treat app lifecycle as a normal interruption boundary, not as an exceptional provider outage.
Separate three kinds of work
AI apps often mix:
interactive streaming generation
finite cleanup/persistence
long-running durable remote work
They need different lifecycle strategies.
Interactive streaming generation
The user expects live tokens while the app is active.
Finite cleanup
The app may need a short bounded period to save state or finish an important transition when moving to the background.
Durable remote work
A provider-side background job can continue even when the app process is suspended or terminated, as long as the client persisted enough identity to reconcile later.
Do not force all three through one networking strategy.
Most ordinary apps should expect suspension
When an iOS app goes to the background, the system can eventually suspend its process unless the app qualifies for specific background execution behavior.
Suspended means your code is not running.
Therefore this design is fragile:
start 5-minute streaming request
user leaves app
assume token callback keeps executing for 5 minutes
Even if it sometimes appears to work during development, it is not a reliable application lifecycle contract.
Persist state before you need it
An active generation should have a durable local record before lifecycle interruption becomes a problem.
Useful fields include:
logical request ID
conversation ID
provider connection ID
model ID
started_at
current phase
partial visible output
provider response/job ID if available
last semantic event
retry/recovery state
Do not wait until termination to save everything.
Mobile processes can be killed without a convenient final callback.
Persist partial output incrementally
For a long stream, keeping the entire answer only in UI memory creates unnecessary data loss.
A practical approach is to checkpoint partial assistant content periodically or at meaningful boundaries.
You do not need a database transaction per token.
Use batching such as:
buffer deltas
flush every small interval or size threshold
flush on important lifecycle transition
finalize on semantic completion
This balances durability and write overhead.
The partial message needs an explicit finish state
If the app is suspended mid-stream, the stored assistant message should not look like a complete answer.
Use states such as:
generating
completed
cancelled
interrupted
failed_after_partial_output
waiting_remote
Then the UI can accurately recover after relaunch.
See How to Resume or Recover an Interrupted AI Generation.
Backgrounding is not user cancellation
The user pressing Home or switching apps does not necessarily mean:
cancel this AI request
Your product can choose behavior, but model it explicitly.
Possible policies:
A. cancel interactive stream on background
B. request limited extra time to finish/checkpoint
C. detach into a supported durable provider job
D. keep connection opportunistically but recover if suspended
Do not label lifecycle interruption as cancelled_by_user unless the user actually cancelled.
Do not blame the provider for lifecycle suspension
Suppose:
stream healthy
app enters background
process suspended
connection later closes
If telemetry reports:
provider stream failure
then provider reliability metrics become contaminated by app lifecycle behavior.
Record a lifecycle cause such as:
interrupted_by_app_background
when that is what happened.
Short background execution extensions are for bounded critical work
UIKit provides APIs that can request additional background execution time for important tasks when the app transitions away from foreground execution.
This is useful for work such as:
checkpoint current state
finish a short save
complete a small critical network transition
cancel/close cleanly
It should not be treated as:
run arbitrary AI streaming forever
The granted time is finite and the app must handle expiration.
Always implement the expiration path
If the system gives extra background execution time, provide an expiration handler that can:
stop nonessential local work
persist current state
cancel or detach safely
release resources
end the background task
Design as though expiration will happen.
If correctness depends on “expiration probably won’t happen,” the architecture is not durable enough.
BackgroundTasks is not an interactive-stream keeper
The BackgroundTasks framework lets the system schedule eligible background processing/refresh work according to platform policy.
That is useful for deferred/reconciliation work such as:
refresh durable provider job status
process completed local work
maintenance/index updates
It is not a guarantee that a user-started streaming request will continue immediately and continuously after they background the app.
The system controls scheduling.
Background URL sessions solve file transfers, not arbitrary token streaming
Apple’s background URL session support is designed around system-managed HTTP/HTTPS uploads and downloads that can continue while the app is suspended or not running.
That is excellent for:
large file upload
large file download
attachment transfer
It is not a drop-in replacement for an interactive data task whose UI consumes incremental SSE/token events in real time.
Keep those workload classes separate.
Long-running AI work is better as remote durable state
If the provider supports background execution, a robust mobile flow can be:
The remote job survives the app process.
See How Long-Running AI Tasks Work.
Persist the remote ID before relying on background completion
Once a provider returns a job/response ID, save it transactionally with the local logical operation.
Otherwise this can happen:
provider created job
app backgrounds/crashes
job ID never persisted
remote work completes
client has no way to find it
Durability depends on linking local and remote identity.
Unknown create outcomes need reconciliation
The hardest case is:
client sends create-background-job
connection disappears
no response ID received
The client cannot know whether the provider created the job.
If the provider supports idempotency or lookup by client operation ID, use it.
Otherwise surface an unknown outcome rather than blindly creating duplicates.
Webhooks help servers more than purely local mobile clients
A provider webhook needs a reachable server endpoint.
A local iPhone app generally cannot expose a reliable public webhook receiver directly.
If your product has no backend, polling/reconciliation on resume may be more appropriate.
If you do operate a backend, preserve the BYOK privacy architecture carefully: adding a webhook service changes which infrastructure sees provider metadata and possibly results.
Notifications are separate from execution
A provider or backend may notify the user that background work completed.
The notification should not be the only source of truth.
On app open:
load durable local operation
query authoritative remote state
reconcile result
Notifications can be delayed, duplicated, or missed.
Local models stop when the local runtime stops
For on-device inference, there is no remote provider continuing independently.
If the app process is suspended, in-process local generation cannot keep executing arbitrarily.
Possible product policies include:
pause/cancel local generation when backgrounded
use supported system execution where workload qualifies
restart generation on foreground
keep partial output and offer regenerate
Be explicit about semantics rather than pretending local generation is a durable server job.
LAN-hosted models have two lifecycles
If the iPhone talks to a model running on a Mac:
iPhone app lifecycle
Mac server lifecycle
The Mac may keep generating after the iPhone is suspended, but whether the result can be recovered depends on the server API.
A plain streaming endpoint may not retain the completed answer for later retrieval.
A job-based API can.
Do not infer recoverability merely because the remote Mac kept running.
On foreground, reconcile before blindly retrying
A good resume flow is:
1. load active/interrupted local operations
2. inspect provider job IDs/state
3. query durable remote jobs where possible
4. finalize completed work
5. mark unrecoverable streams interrupted
6. offer retry/regenerate only when appropriate
This avoids duplicate generations.
Streaming endpoints often require semantic completion
If the app resumes and finds a partially persisted stream but never observed the provider’s semantic completion event, treat it as interrupted unless the API offers another authoritative completion lookup.
Do not mark it completed only because the last visible sentence ends with punctuation.
Cancellation while backgrounding needs policy
If the product chooses to cancel active streams immediately on background:
persist partial output
cancel network task
mark lifecycle cancellation reason
release resources
If the product chooses to keep trying briefly:
begin bounded background execution
continue/checkpoint
on expiration -> persist + cancel/detach
Both can be valid. Inconsistent implicit behavior is the problem.
Tool calls make backgrounding more sensitive
A model can emit a tool call just before the app backgrounds.
Possible states include:
waiting for approval
tool executing
side effect committed
tool result ready
follow-up model round pending
Persist these states explicitly.
Do not auto-approve a tool merely because the UI is no longer foregrounded.
See How to Build Human Approval Into AI Tool Calls.
Side-effecting tools require outcome certainty
If a remote tool is executing when the app is suspended, a local timeout/interruption does not prove the side effect failed.
Use idempotency/reconciliation when possible.
Examples:
email may already have sent
calendar event may already exist
file upload may have completed
See Idempotency for AI Tool Execution.
Request queues should persist lifecycle transitions
When the app backgrounds, active items can move into states such as:
running -> checkpointing
running -> waiting_remote
running -> interrupted_background
A durable queue can resume/reconcile them on relaunch.
Ephemeral items can be cancelled.
See How to Build an AI Request Queue.
Deadlines continue while the app is suspended
A durable operation with a 10-minute maximum runtime should not receive a fresh 10 minutes merely because the app was suspended for 9 minutes.
Persist:
created_at
maximum_runtime/deadline
and evaluate elapsed wall-clock policy on resume.
For in-process timers, use monotonic clocks while running; for durable recovery, persist absolute/declarative timing.
Avoid background busy loops
Do not try to preserve an interactive workflow by repeatedly waking the app or scheduling aggressive polling.
Mobile background execution is intentionally constrained for battery and system health.
For long-running remote work, use durable provider jobs and appropriately spaced reconciliation.
Observability should include lifecycle context
Useful sanitized fields include:
app_state_at_request_start
entered_background_during_request
background_transition_at
resume_at
finish_state
partial_output_present
remote_job_id_present yes/no
recovery_method
This lets you separate provider instability from mobile lifecycle interruptions.
See Observability for Streaming AI Requests.
UI should tell the truth after recovery
Possible messages:
Generation was interrupted while the app was in the background.
Background generation completed while you were away.
The request may have completed remotely, but its result could not be recovered.
Do not silently replace a partial answer with a new regenerated answer and pretend it is the original continuation.
Preserve drafts/user input independently
If the user backgrounds the app while composing rather than generating, drafts should not depend on request lifecycle.
Persist draft input independently so app suspension does not lose it.
This is a local UX concern but belongs to the same resilience mindset.
App termination should be part of tests
Test more than pressing Home in the simulator.
Scenarios include:
background before first token
background during steady stream
background during tool approval
background during tool execution
background after remote job created but before local save
system terminates app
user force-quits app
network changes while backgrounded
provider completes while app absent
resume after request deadline
Expected local state should be deterministic.
Do not promise recovery you cannot implement
If a provider offers only ephemeral streaming with no lookup/continuation API, the honest recovery model may be:
keep partial output
mark interrupted
offer regenerate
That is better than inventing a fake resume mechanism.
When a provider offers durable response/job IDs, use them to improve recovery.
A lifecycle architecture
A backgrounding checklist
- Active generation has stable logical identity.
- Partial output is checkpointed before it can be lost.
- Backgrounding and user cancellation are different finish reasons.
- Provider health metrics exclude lifecycle-caused interruptions.
- Any extra background execution is bounded and has an expiration path.
- BackgroundTasks are used for eligible deferred work, not as an indefinite interactive-stream guarantee.
- Background URL sessions are used for supported upload/download transfers, not as a generic SSE-stream substitute.
- Durable provider jobs persist remote IDs.
- Relaunch reconciles remote state before retrying.
- Tool approvals and side effects remain explicit across lifecycle changes.
- Deadlines do not reset after suspension.
- UI distinguishes completed, interrupted, partial, and unknown outcomes.
Where BYOKchat fits
A local-first multi-provider client can make mobile backgrounding predictable by persisting conversation and request state continuously rather than depending on the process staying alive. Interactive streams can preserve partial responses and recover honestly, while provider APIs that support durable background jobs can survive suspension through persisted remote IDs and later reconciliation.
This keeps the chat history trustworthy without adding a mandatory proxy server merely to keep a stream alive.