When Mobile Connections Break: Recovering Long-Running iOS Workflows With LangGraph and Event-Driven Backends
LangGraph checkpoints and event-driven backends let iOS apps resume long-running workflows after network drops without duplicate work.
Join the DZone community and get the full member experience.
Join For FreeWhen a mobile application starts an agentic workflow that may run for minutes, the network connection is almost guaranteed to be shorter-lived than the computation. Wi-Fi can disappear, cellular routing can change, the device can lock, and iOS can suspend the process after it moves to the background. Apple explicitly states that backgrounded apps are suspended by default, while background URLSession exists for transfers that must continue when the app is inactive.
That distinction matters as a persistent SSE or WebSocket connection can improve foreground responsiveness, but it should not define whether a long-running LangGraph workflow is alive. The reliable design makes workflow execution a backend concern and treats the iOS connection as a detachable observation channel.
The Mobile App Cannot Own the Run
A fragile implementation ties an HTTP request, an agent run, and a UI progress indicator into one lifecycle. The client sends work, the server begins executing it, and the response remains open until completion. Once the TCP connection disappears, every layer has an ambiguous question: did the workflow fail, did only the stream fail, or did the client simply stop listening? Retrying the original request can be even worse because a second run may be created while the first is still executing.
The safer contract separates submission from observation. A start request should create or identify a durable workflow and return a stable handle immediately, commonly with HTTP 202 Accepted, whose semantics explicitly support accepted-but-incomplete processing. The handle can contain an application workflow ID plus the LangGraph thread_id and, where useful, a run ID. An idempotency key prevents the same mobile action from creating duplicate backend work after a timeout.
A compact client-side start path can persist that handle before opening any live stream:
request.setValue(commandId, forHTTPHeaderField: "Idempotency-Key")
let (data, _) = try await URLSession.shared.data(for: request)
let handle = try decoder.decode(WorkflowHandle.self, from: data)
try workflowStore.save(handle)
The important behavior is the ordering. Once the backend accepts the command, the workflow identity is stored locally. A later socket failure loses only live updates, not the ability to locate the computation. For large file inputs, a background URLSession can separately handle upload continuity. Apple documents background downloads and uploads as system-managed transfers that can outlive application suspension, and resumable transfer support can recover from network interruption without restarting all bytes.
Durable State Has to Survive the Socket
LangGraph fits this model because persistence is built around threads and checkpoints rather than a single uninterrupted request. A checkpointer stores thread-scoped graph state, allowing execution to recover after failure or interruption, and production deployments can use a database-backed checkpointer instead of in-memory state. The thread_id is the stable pointer used to load that state.
config = {"configurable": {"thread_id": workflow_id}}
graph.invoke(
{"workflow_id": workflow_id, "input": payload},
config=config,
)
Checkpointing does not make arbitrary side effects exactly-once. LangGraph saves Graph API state at super-step boundaries, and a node that is interrupted or retried can execute again from the beginning. Current LangGraph guidance explicitly recommends idempotency keys, upserts, or read-before-write checks for effects that can repeat. The Functional API similarly warns that a task that started but did not finish may run again after resume.
That behavior should shape node design. External writes belong behind stable operation keys, ideally derived from the workflow and logical step:
@task
def persist_result(workflow_id, result):
return results.upsert(
key=f"{workflow_id}:final-result",
value=result,
)
Checkpoints provide durable progress through the graph as idempotency protects the systems touched by that graph. Reliable recovery requires both.
Events Make Reconnection Deterministic
A durable workflow still needs a durable way to describe progress. Directly forwarding LangGraph tokens or node updates to an iPhone is useful for immediacy, but transient transport data should not be the only record of business-visible state. An event-driven backend can translate meaningful transitions such as accepted, planning, tool_completed, awaiting_approval, completed, and failed into durable events carrying a workflow ID, monotonically increasing sequence, and unique event ID.
The transactional outbox pattern is well suited to this boundary. Debezium documents the pattern as a way to avoid inconsistency between database state and events consumed by other services; application state and the outbox record are written together, then change-data capture publishes the event asynchronously. Debezium also describes propagation as at-least-once, which makes consumer deduplication part of the design rather than an optional optimization.
A backend transition can keep the state change and event creation in one transaction:
with db.transaction() as tx:
tx.execute(
"UPDATE workflows SET status = %s, version = version + 1 WHERE id = %s",
("completed", workflow_id),
)
tx.execute(
"INSERT INTO outbox(event_id, workflow_id, type, payload) VALUES (%s, %s, %s, %s)",
(event_id, workflow_id, "workflow.completed", payload),
)
CloudEvents can provide a standard envelope when events cross service boundaries. Its specification defines interoperable event metadata, while the CloudEvents primer states that an event id is unique within an event source. Those semantics map naturally to deduplication keys in downstream consumers.
LangGraph’s own deployment streaming already demonstrates the same recovery idea. The streaming API supports reconnection using the last event ID, while the newer event-streaming API assigns sequence values and durable event IDs, replays buffered events after reconnect, and deduplicates replays client-side. The newer API also documents an important limit as its per-run replay buffer is bounded, so early events from a very long run can be evicted.
Recovery Must Handle Duplicates and Gaps
That bounded buffer is why an application-level status model should exist beside the live LangGraph stream. A reconnecting iOS client should first obtain the authoritative workflow snapshot, including status, output references, and the latest committed sequence. It can then request events after the locally stored cursor. If old events are no longer available, the snapshot repairs the gap as if events are replayed; event IDs or sequence numbers suppress duplicates. This design extends LangGraph’s resumable-stream model with durable application state rather than assuming an in-memory or bounded event buffer is a permanent event log.
The foreground recovery path can remain small:
let snapshot = try await api.workflow(id: handle.id)
apply(snapshot)
for try await event in api.events(id: handle.id, after: handle.sequence) {
guard event.sequence > workflowStore.sequence(handle.id) else { continue }
apply(event)
try workflowStore.save(sequence: event.sequence, for: handle.id)
}
Cursor persistence should occur only after an event has been applied successfully. That ordering converts a crash between receipt and rendering into a harmless replay rather than a silent gap. The same principle applies on the backend, as command acceptance should follow durable recording of the workflow identity and command so that a successful acknowledgment does not refer to state that disappears after a process failure. The outbox pattern applies the same atomicity principle to workflow state and outbound events.
iOS background facilities remain useful, but they should complement this protocol rather than replace it. Apple’s Background Tasks framework can grant background runtime, yet Apple also emphasizes that backgrounded applications normally receive no CPU time. Background URLSession is appropriate for long-running network transfers, not as a guarantee that an arbitrary agent event stream remains continuously connected.
The Backend Becomes the Workflow Boundary
The strongest recovery design changes the meaning of “connection lost.” It no longer means “workflow state unknown.” It means only “the current observer is detached.” LangGraph checkpoints preserve graph progress, idempotent tasks protect external side effects, a transactional outbox makes business transitions publishable without a dual-write race, and durable event identities make replay safe. The iOS application keeps only the minimum recovery coordinates, workflow identity, and the last applied cursor. These responsibilities align with LangGraph’s checkpoint-based persistence model, its replay and idempotency requirements, and event-streaming support for reconnectable consumers.
Opinions expressed by DZone contributors are their own.
Comments