Skip to content

Dart Isolates — Parallel Execution and Concurrency

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart Isolates. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart isolates are independent workers that run code in parallel with their own memory heap and event loop, communicating with the main isolate through message passing for CPU-intensive operations.

What You Will Learn

  • Understanding isolates vs threads
  • Creating isolates with Isolate.spawn
  • Passing messages between isolates
  • Using ReceivePort and SendPort
  • Computing heavy operations with Isolate.run
  • Flutter's compute function
  • Isolate lifecycle and cleanup

Why It Matters

Dart's event loop handles I/O operations efficiently, but CPU-intensive work (image processing, JSON Parsing of large files, data compression, cryptographic operations) blocks the event loop and makes the application unresponsive. Isolates solve this by running code on separate CPU cores. Unlike threads in other languages, isolates do not share memory, which eliminates race conditions and the need for locks. Each isolate has its own heap and communicates only through messages.

Real-World Use

The DodaTech Flutter app uses isolates for image processing. When a user selects a photo for their profile, the image is resized, compressed, and converted to WebP format in a separate isolate. This prevents UI jank during the transformation. The main app remains responsive, showing a progress indicator while the isolate works.

Learning Path

flowchart LR
  A[Dart Streams] --> B[Dart Isolates\nYou are here]
  B --> C[Dart Extensions]
  style B fill:#f90,color:#fff

Isolates vs Threads

Traditional threading shares memory between threads, requiring locks and synchronization. Isolates do not share memory:

import 'dart:isolate';

void main() async {
  print('Main isolate started');

  // Spawn a new isolate
  var receivePort = ReceivePort();
  await Isolate.spawn(workerFunction, receivePort.sendPort);

  // Receive messages from the worker
  receivePort.listen((message) {
    print('Main received: $message');
  });

  print('Main isolate continues...');
}

void workerFunction(SendPort sendPort) {
  print('Worker isolate started');
  sendPort.send('Hello from worker!');
  sendPort.send('Isolates do not share memory');
}

Output:

Main isolate started
Main isolate continues...
Worker isolate started
Main received: Hello from worker!
Main received: Isolates do not share memory

The main isolate spawns a worker and continues executing. The worker runs in parallel on a separate CPU core. Communication happens through SendPort and ReceivePort.

Sending and Receiving Messages

Messages are copied between isolates. Complex objects must be sendable (primitives, lists, maps, and port references):

import 'dart:isolate';

// A simple sendable data class
class WorkRequest {
  final int id;
  final String data;
  WorkRequest(this.id, this.data);
}

class WorkResult {
  final int id;
  final int length;
  WorkResult(this.id, this.length);
}

void main() async {
  var receivePort = ReceivePort();
  var isolate = await Isolate.spawn(processData, receivePort.sendPort);

  // Send work
  receivePort.listen((message) {
    if (message is WorkResult) {
      print('Result for request ${message.id}: length = ${message.length}');
    } else if (message is SendPort) {
      // Send work to the worker
      message.send(WorkRequest(1, 'Hello, Dart Isolate!'));
      message.send(WorkRequest(2, 'Parallel processing is powerful'));
    }
  });
}

void processData(SendPort sendPort) {
  var receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);

  receivePort.listen((message) {
    if (message is WorkRequest) {
      print('Worker processing request ${message.id}');
      var result = WorkResult(message.id, message.data.length);
      sendPort.send(result);
    }
  });
}

Output:

Worker processing request 1
Result for request 1: length = 21
Worker processing request 2
Result for request 2: length = 32

The two-way communication pattern uses ports to establish a bidirectional channel. The main isolate sends WorkRequest objects, and the worker sends WorkResult objects back.

Using Isolate.run

Dart 2.19 introduced Isolate.run, which simplifies the common pattern of running a function in an isolate:

import 'dart:isolate';

// CPU-intensive function
String processLargeString(String input) {
  // Simulate heavy processing
  var result = input
      .split('')
      .map((c) => c.toUpperCase())
      .join('');
  return result;
}

Future<void> main() async {
  print('Processing on main isolate...');

  var data = 'hello world from dart isolates';

  // Run in a separate isolate
  var result = await Isolate.run(() => processLargeString(data));

  print('Result: $result');
  print('Main isolate was not blocked!');
}

Output:

Processing on main isolate...
Result: HELLO WORLD FROM DART ISOLATES
Main isolate was not blocked!

Isolate.run spawns an isolate, runs the function, captures the return value, and closes the isolate automatically. This is the simplest way to offload heavy work.

Flutter's compute Function

Flutter provides a compute function that works like Isolate.run but is integrated with Flutter's lifecycle:

import 'package:flutter/foundation.dart';

// Top-level function required by compute
int calculateSum(List<int> numbers) {
  // CPU-intensive calculation
  return numbers.fold(0, (a, b) => a + b);
}

class DataProcessor {
  Future<int> processData(List<int> data) async {
    // Offload to background isolate
    return await compute(calculateSum, data);
  }
}

// In a widget:
// var processor = DataProcessor();
// var sum = await processor.processData([1, 2, 3, 4, 5]);
// print('Sum: $sum');

Output: Sum: 15

The function passed to compute must be a top-level function or a static method. It takes one argument and returns a value. The result is delivered as a Future.

Sending Multiple Arguments

To pass multiple values, use a record or a list:

import 'dart:isolate';

// Using records for multiple arguments
String multiplyAndFormat((int, int) args) {
  var (a, b) = args;
  var result = a * b;
  return '$a x $b = $result';
}

Future<void> main() async {
  var result = await Isolate.run(() => multiplyAndFormat((7, 8)));
  print(result);

  // Using a list
  var listResult = await Isolate.run(() {
    var numbers = [10, 20, 30];
    return numbers.map((n) => n * 2).toList();
  });
  print('Doubled: $listResult');
}

Output:

7 x 8 = 56
Doubled: [20, 40, 60]

Records or simple collections work well for multiple arguments. Avoid passing large objects to isolates because they must be copied.

Handling Errors in Isolates

Errors in isolates are delivered as error events on the receive port:

import 'dart:isolate';

void failingWorker(SendPort sendPort) {
  throw Exception('Something went wrong in the worker');
}

Future<void> main() async {
  var receivePort = ReceivePort();
  var isolate = await Isolate.spawn(failingWorker, receivePort.sendPort);

  receivePort.listen(
    (message) => print('Received: $message'),
    onError: (error) => print('Error from isolate: $error'),
    onDone: () => print('Isolate closed'),
  );

  await Future.delayed(Duration(seconds: 1));
}

Output:

Error from isolate: Exception: Something went wrong in the worker
Isolate closed

Uncaught exceptions in isolates terminate the isolate and send an error event. Always handle errors in isolate listeners.

Isolate Lifecycle

Isolates should be properly terminated to free resources:

import 'dart:isolate';

void main() async {
  var receivePort = ReceivePort();
  var isolate = await Isolate.spawn(
    (SendPort sendPort) {
      var counter = 0;
      Timer.periodic(Duration(seconds: 1), (timer) {
        counter++;
        sendPort.send('Tick $counter');
        if (counter >= 5) {
          timer.cancel();
          sendPort.send('Done');
        }
      });
    },
    receivePort.sendPort,
  );

  await for (var message in receivePort) {
    print('Main: $message');
    if (message == 'Done') {
      isolate.kill(priority: Isolate.immediate);
      print('Isolate killed');
    }
  }
}

Output:

Main: Tick 1
Main: Tick 2
Main: Tick 3
Main: Tick 4
Main: Tick 5
Main: Done
Isolate killed

Use isolate.kill() to terminate an isolate early. The priority Isolate.immediate kills it immediately. Isolate.beforeNextEvent lets pending events complete first.

Performance Considerations

Isolates are not free. Spawning an isolate has overhead:

import 'dart:isolate';

void main() async {
  // Measure isolate spawn overhead
  var start = DateTime.now();

  for (var i = 0; i < 10; i++) {
    await Isolate.run(() => i * i);
  }

  print('10 isolates: ${DateTime.now().difference(start).inMilliseconds}ms');

  // Compare to synchronous execution
  start = DateTime.now();
  for (var i = 0; i < 10; i++) {
    i * i;
  }
  print('10 synchronous: ${DateTime.now().difference(start).inMicroseconds}us');
}

Output:

10 isolates: 45ms
10 synchronous: 2us

Use isolates only for operations that take more than ~20ms. For short operations, the cost of spawning an isolate outweighs the benefit.

Comparing Approaches

import 'dart:isolate';
import 'dart:math';

void main() async {
  // Generate large data
  var largeList = List.generate(10_000_000, (i) => i);

  // Synchronous: blocks
  var start = DateTime.now();
  var maxValue = largeList.reduce(max);
  print('Sync: max = $maxValue, took ${DateTime.now().difference(start).inMilliseconds}ms');

  // With isolate: non-blocking
  start = DateTime.now();
  var maxAsync = await Isolate.run(() => largeList.reduce(max));
  print('Async: max = $maxAsync, took ${DateTime.now().difference(start).inMilliseconds}ms');
}

Output:

Sync: max = 9999999, took 45ms
Async: max = 9999999, took 50ms

The synchronous version blocks the main isolate for 45ms. The async version also takes 50ms but runs in a background isolate, leaving the main isolate free to Process UI events and other tasks.

Common Mistakes

  1. Passing non-sendable objects between isolates: Objects must be primitive types, lists, maps, or port references. Custom objects require manual Serialization (e.g., to JSON).

  2. Accessing shared state across isolates: Isolates do not share memory. Every message is copied. Trying to access a global variable across isolates will not work as expected.

  3. Spawning an isolate for trivial work: The overhead of creating an isolate (1-5ms) may exceed the computation time. Use isolates only for operations over ~20ms.

  4. Not terminating isolates: Unreferenced isolates continue running until their main function completes. Kill unused isolates or let them terminate naturally.

  5. Using Isolate.spawn when Isolate.run suffices: Isolate.run is simpler and automatically handles cleanup. Use Isolate.spawn only for advanced patterns like persistent workers.

Practice Questions

  1. How do isolates differ from threads in terms of memory sharing?
  2. What types of objects can be passed between isolates?
  3. When would you use Isolate.spawn instead of Isolate.run?
  4. How does Flutter's compute function relate to Dart isolates?
  5. Challenge: Implement a parallel prime number sieve. Create a worker isolate that finds all primes up to N. Use Isolate.run to perform the calculation and return the result as a list of integers.

Mini Project

Build a parallel image processing system:

  • Generate a list of 100 random integers representing image processing tasks
  • Divide the list into chunks based on available CPU cores
  • Spawn an isolate for each chunk
  • Each isolate processes its chunk (simulate with Future.delayed)
  • Collect and merge results from all isolates
  • Compare total time with single-isolate processing

FAQ

How many isolates can I create?

The practical limit depends on device resources. Creating one isolate per CPU core is typical. Each isolate adds memory overhead (roughly 50-100KB for the heap).

Can two isolates share a file handle?

No. File handles are per-isolate resources. If two isolates need to read the same file, each must open it independently.

Do isolates run on separate CPU cores?

Yes. The Dart runtime schedules isolates on available CPU cores. Multiple isolates can run truly in parallel on multi-core devices.

Can I use isolates in Flutter web?

No. The web platform does not support isolates. Use Web Workers via dart:html for web-based concurrency.

What is the difference between `Isolate.spawn` and `Isolate.spawnUri`?

spawn creates an isolate from a function in the same program. spawnUri creates an isolate from a separate Dart source file.

What is Next

Now that you understand isolates, learn about extensions for adding functionality. Proceed to Dart Extensions for extending existing types with new methods and properties. Then explore Records and Patterns in Dart for destructuring and pattern matching.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro