ZetCode

Dart Isolates

last modified May 29, 2026

Isolate.run

Isolate.run() was introduced in Dart 2.19 and became the recommended API for one-shot CPU-bound tasks in Dart 3. Unlike Isolate.spawn(), it accepts an ordinary closure, spawns a temporary isolate, runs the closure, forwards the return value as a Future, and tears the isolate down automatically. There are no ports to create, no handshake, and no cleanup code.

run.dart
import 'dart:isolate';

// Expensive computation – will run in a fresh isolate.
int heavyWork(int n) {
  int sum = 0;
  for (int i = 0; i < n; i++) sum += i;
  return sum;
}

void main() async {
  // Isolate.run handles spawn, port wiring, result forwarding, and cleanup.
  final result = await Isolate.run(() => heavyWork(100000000));
  print('Result: $result'); // Result: 4999999950000000
}

The closure can capture local variables as long as the captured values are sendable (primitives, strings, lists, maps). The isolate is discarded once the closure returns, so Isolate.run() is not suitable for tasks that need to stay alive or exchange more than one message.

Isolate.run vs Isolate.spawn

Choosing between the two APIs comes down to the lifetime and communication pattern of the work. The table below summarises the key differences.

Isolate.run()Isolate.spawn()
API levelHigh-levelLow-level
Return valueFuture<T>Future<Isolate>
Port wiringAutomaticManual
CleanupAutomaticManual
ClosuresYesNo (top-level / static only)
Multiple messagesNoYes
Long-lived isolateNoYes
Stream outputNoYes

Use Isolate.run() whenever the task returns a single value and does not need to persist. Switch to Isolate.spawn() when the isolate must stay alive across multiple messages, emit a stream of results, or serve as a reusable background worker.

Isolates with Streams

An isolate can produce a sequence of values over time, but its SendPort is not a Stream. The bridge is a StreamController on the main side: forward every incoming port message to the controller and close it when a sentinel value arrives. The function below wraps this pattern into a typed Stream<int>.

isolate_stream.dart
import 'dart:async';
import 'dart:isolate';

// Generator: emits integers 0..count-1, then sends null as a sentinel.
void generator(List<dynamic> args) {
  final SendPort port = args[0];
  final int count = args[1] as int;
  for (int i = 0; i < count; i++) {
    port.send(i);
  }
  port.send(null); // signals end of stream
}

// Bridges an isolate's output to a Stream<int>.
Stream<int> isolateStream(int count) {
  final controller = StreamController<int>();
  final receivePort = ReceivePort();
  Isolate.spawn(generator, [receivePort.sendPort, count]);
  receivePort.listen((msg) {
    if (msg == null) {
      controller.close();
      receivePort.close();
    } else {
      controller.add(msg as int);
    }
  });
  return controller.stream;
}

void main() async {
  await for (final value in isolateStream(5)) {
    print('Received: $value'); // 0, 1, 2, 3, 4
  }
}

The StreamController is not buffered by default, so values are delivered as fast as the listener consumes them. For a back-pressured variant, use StreamController(sync: false) and pause the ReceivePort when the controller is paused.

Server-Side: Offloading CPU Work from an HTTP Handler

In a Dart HTTP server every request is handled on the main isolate's event loop. A slow synchronous computation inside a request handler blocks all concurrent connections until it finishes. Using Isolate.run() moves the work off the event loop so the server stays responsive.

server.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

// CPU-bound: decode a JSON array and return count + sum of 'value' fields.
Map<String, dynamic> summarise(String body) {
  final List items = jsonDecode(body) as List;
  final total = items.fold<int>(0, (s, e) => s + (e['value'] as int));
  return {'count': items.length, 'total': total};
}

void main() async {
  final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
  print('Listening on http://localhost:8080');

  await for (final HttpRequest req in server) {
    if (req.method == 'POST' && req.uri.path == '/sum') {
      // Read the full request body as a string.
      final body = await req.transform(utf8.decoder).join();

      // Offload JSON work – the event loop handles other requests meanwhile.
      final result = await Isolate.run(() => summarise(body));

      req.response
        ..headers.contentType = ContentType.json
        ..write(jsonEncode(result))
        ..close();
    } else {
      req.response
        ..statusCode = HttpStatus.notFound
        ..close();
    }
  }
}

Test with: curl -X POST http://localhost:8080/sum -d '[{"value":10},{"value":20}]'. The server responds with {"count":2,"total":30} while remaining free to accept new connections during the computation.

CLI: Parallel File Processing

A command-line tool can use one isolate per input file to count lines in parallel. All isolates share the same ReceivePort on the main isolate and send their results back as they finish. The main isolate aggregates once all expected replies have arrived.

cli.dart
import 'dart:io';
import 'dart:isolate';

// Worker: counts lines in the given file, sends [path, lineCount] back.
void countLines(List<dynamic> args) {
  final SendPort port = args[0];
  final String path = args[1] as String;
  try {
    port.send([path, File(path).readAsLinesSync().length]);
  } catch (_) {
    port.send([path, -1]); // -1 signals a read error
  }
}

void main(List<String> arguments) async {
  if (arguments.isEmpty) {
    print('Usage: dart cli.dart <file1> [file2 ...]');
    exit(1);
  }

  final receivePort = ReceivePort();

  // Spawn one isolate per file – all run in parallel.
  for (final path in arguments) {
    await Isolate.spawn(countLines, [receivePort.sendPort, path]);
  }

  int received = 0;
  int total = 0;
  await for (final msg in receivePort) {
    final res = msg as List;
    final int count = res[1] as int;
    if (count < 0) {
      print('  error: ${res[0]}');
    } else {
      print('  ${res[0]}: $count lines');
      total += count;
    }
    if (++received == arguments.length) break;
  }
  receivePort.close();
  print('Total: $total lines in ${arguments.length} file(s)');
}

Run it with dart cli.dart a.txt b.txt c.txt. Because results arrive in completion order rather than argument order, the printed lines may appear in any sequence.

Performance Guidelines

Spawning an isolate allocates a fresh heap and initialises a Dart VM worker thread, which costs roughly 1–5 milliseconds on a typical desktop or server. A computation that finishes in under a millisecond therefore pays more in spawn overhead than it saves in parallelism. For short one-shot tasks Isolate.run() is the right default. For frequently repeated work, keep a long-lived isolate alive with Isolate.spawn() and send jobs to it over a port rather than spawning and tearing down for each job.

Message copy cost scales with payload size. Every value sent through a SendPort is deep-copied, so transferring a 10 MB list can take longer than the computation itself. The escape hatch for large binary buffers is TransferableTypedData: wrap a Uint8List in TransferableTypedData.fromList() before sending, and the buffer is transferred rather than copied. The sender's reference becomes invalid after the transfer, but the receiver gets the data instantaneously regardless of size.

A frequent mistake in server code is spawning a new isolate for every incoming HTTP request. At low traffic this works, but under load the cumulative spawn cost and memory pressure from dozens of simultaneous heaps can degrade throughput more than the parallelism helps. A fixed-size worker pool — a small number of long-lived isolates accepting job messages — amortises the startup cost and caps peak memory consumption. As a rule of thumb: one-shot tasks suit Isolate.run(); sustained high-frequency work suits a pool built on Isolate.spawn().

Source

Dart Isolate.run, Dart Isolate class, Dart concurrency documentation

This tutorial covered Isolate.run() and when to prefer it over Isolate.spawn(), bridging isolate output to a Stream, offloading CPU work in an HTTP server, parallel file processing from the CLI, and practical performance considerations.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.