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
SendPortto write and aReceivePortto read. - Sent objects are deep-copied — a change on one side never affects the other. The exceptions are
TransferableTypedDataand 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 interleaving | A separate one — true parallelism |
| Memory | Shared — every object is reachable | Separate — message passing only |
| Race-condition risk | Yes — state can change during an async gap | No — there is no shared state |
| Cost | Practically zero | Spawn time (~ms) + copying arguments and results + memory |
| Cancellation | None (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 ordinarytry/catchworks. - `compute(fn, arg)` — Flutter's older helper for the same idea. Since
Isolate.runlanded,computeis essentially built on it; new code should preferIsolate.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/SendPorthandshake 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.runpays 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
- Concurrency in Dartofficialdart.dev
- Isolate classofficialapi.dart.dev