Sparround

Isolates and concurrency

Dart's parallelism model is built on isolates rather than threads. An isolate is an independent unit of execution with its own memory, its own event loop and its own garbage collector.

The core principle fits in one sentence: isolates share no memory. The consequences:

  • There are no shared variables, and therefore no race conditions, no mutexes, no locks, no `synchronized`. This is the biggest advantage of Dart's concurrency model.
  • Communication happens only by message passing: a SendPort to write and a ReceivePort to read.
  • Sent objects are deep-copied — a change on one side never affects the other. The exceptions are TransferableTypedData and immutable objects (strings, constants), which are passed without copying.

For contrast: async/await is interleaving inside the same isolate on one thread; an isolate can genuinely run on another CPU core.

Question`async`/`await`Isolate
Which problem does it solve?**Waiting** — I/O, network, files, timers**Computing** — CPU-bound work
How many threads?One — concurrency by interleavingA separate one — true parallelism
MemoryShared — every object is reachableSeparate — message passing only
Race-condition riskYes — state can change during an async gapNo — there is no shared state
CostPractically zeroSpawn time (~ms) + copying arguments and results + memory
CancellationNone (a Future cannot be cancelled)`isolate.kill()` — genuine cancellation

Which API do you use?

  • `Isolate.run(fn)` (Dart 2.19+) — modern and the right choice in 95% of cases. It spawns an isolate, runs fn, returns the result and shuts the isolate down. If it throws, the error is rethrown on the main isolate — so an ordinary try/catch works.
  • `compute(fn, arg)` — Flutter's older helper for the same idea. Since Isolate.run landed, compute is essentially built on it; new code should prefer Isolate.run, which has no Flutter dependency and accepts closures.
  • `Isolate.spawn(entryPoint, message)` — the low-level API. You pick it when the isolate must be long-lived and you need ongoing messaging: a worker pool, continuous decoding, game logic. Here you build the ReceivePort/SendPort handshake yourself.

What can and cannot be sent? Sendable: null, num, bool, String, TypedData, List/Map/Set of those, SendPort, Capability, and since Dart 2.15 most ordinary objects (deep-copied) and even top-level/static functions and closures.

Not sendable: a ReceivePort itself, open Socket and File handles, dart:ffi pointers and objects holding native resources. In Flutter, additionally, anything tied to the UI — a BuildContext, an image handle, a platform channel. Attempting to send them raises an ArgumentError.

The honest cost of an isolate. Isolates are not free, and knowing this distinguishes you in an interview:

  • Spawning: a new heap, a new event loop, runtime setup — typically a few milliseconds and roughly 1-2 MB on a mobile device. Isolate.run pays this on every call.
  • Copying: arguments and results are serialised. Sending a 10 MB list over and getting it back is expensive in itself — sometimes more expensive than the computation. Do not decide without measuring.
  • Consequence: an isolate is a net loss for short work. Spending 5 ms spawning and 3 ms copying for 2 ms of work makes no sense.

The rule: reach for an isolate for pure CPU work that takes longer than 10-16 ms. Typical candidates: parsing or encoding large JSON, image processing and resizing, cryptography and hashing, sorting or filtering a large list, CSV or PDF generation, ML inference.

For work that repeats often, keep a long-lived worker (Isolate.spawn plus a port handshake) instead of spawning a fresh isolate each time — the spawn cost is then paid once.

One important exception: a network request does not need an isolate. That is waiting, not computing — await is enough. This mistake shows up constantly in interviews.

Interview tip. The most frequent question: "what is the difference between an isolate and async/await?" Open with one sentence that makes the whole answer memorable: "`async` fixes waiting, an isolate fixes computing." Then explain: waiting for a network response occupies no thread, so await is enough; parsing 10 MB of JSON occupies the CPU, and await does nothing about it — the work still runs on the same thread and the UI freezes.

Second ready answer: "what does it mean that isolates share no memory?" — no race conditions, locks or mutexes; communication only through copied messages; and the price of that, serialising large data.

The two most common mistakes: (1) wanting to run a network request in an isolate; (2) ignoring the cost — for small work an isolate makes performance worse. Add the sentence "I do not decide without measuring; I look at the DevTools timeline to see which work exceeds the frame budget" — a clear signal of practical experience.

📚 Sources and documentation