Dart makes asynchronous programming look simple:
final response = await fetchData();
However, several different mechanisms are working behind that small piece of code.
To understand Dart concurrency properly, you need to understand:
- Isolates
- Event loop
- Event queue
- Microtask queue
- Futures
asyncandawait- Streams
- CPU-bound vs I/O-bound work
- Multiple isolates
The most important idea is this:
Dart can handle many asynchronous operations concurrently even when Dart code inside a single isolate executes one operation at a time.
Concurrency does not automatically mean that multiple pieces of Dart code are executing in parallel.
Let’s understand how it actually works.
What Is Concurrency?
Concurrency means managing multiple tasks whose execution can overlap in time.
Imagine an application doing several things:
API Request
Animation
Button Tap
Database Request
Timer
File Read
The application should not freeze while waiting for one operation to finish.
Conceptually:
Task A ───── waiting ───────── result
Task B ── work ── waiting ─── result
Task C ─────── work ───────────────
The tasks overlap in their lifetimes.
That is concurrency.
Concurrency vs Parallelism
These two terms are related but different.
Concurrency
Concurrency means multiple tasks can make progress during overlapping periods.
For example:
Task A
↓
Waiting for network
Task B
↓
Can continue
One task does not necessarily need to finish before another task can progress.
Parallelism
Parallelism means multiple computations are actually executing at the same time.
Conceptually:
CPU Core 1 → Task A
CPU Core 2 → Task B
A single Dart isolate primarily uses event-loop-based concurrency.
Multiple isolates can allow Dart computations to execute independently and potentially in parallel.
The Dart Isolate
The first concept to understand is the isolate.
A Dart isolate is an independent Dart execution environment.
Each isolate has its own:
Memory
Event loop
Event queue
Microtask queue
Execution state
A typical Flutter application starts with a main isolate.
Conceptually:
Flutter Application
↓
Main Isolate
↓
┌─────────────────┐
│ Event Loop │
│ Event Queue │
│ Microtask Queue │
│ Dart Heap │
└─────────────────┘
Most application Dart code initially executes there.
Dart Code in One Isolate Executes Sequentially
Within one isolate, Dart does not normally execute two Dart callbacks simultaneously.
Suppose you have:
void taskA() {
print('Task A');
}
void taskB() {
print('Task B');
}
Within the same isolate, execution is conceptually:
Task A
↓
Finish
↓
Task B
not:
Task A ─────┐
├── Simultaneous Dart execution
Task B ─────┘
This makes ordinary Dart code easier to reason about because you generally do not have multiple callbacks modifying the same isolate’s state simultaneously.
Then How Can Dart Handle Multiple Tasks?
Through the event loop.
The event loop continuously processes scheduled work.
A simplified model is:
Microtask Queue
↓
Event Queue
↓
Event Loop
↓
Execute Callback
↓
Repeat
This allows asynchronous operations to complete without forcing the Dart isolate to synchronously wait for them.
Understanding the Event Loop
Consider:
void main() {
print('A');
Future(() {
print('B');
});
print('C');
}
You might initially expect:
A
B
C
But the result is:
A
C
B
Why?
Because:
print('A');
executes immediately.
Then:
Future(() {
print('B');
});
schedules asynchronous work.
Dart continues:
print('C');
Only after the current synchronous work completes can the event loop process the scheduled Future callback.
Conceptually:
Call Stack
print A
Schedule Future
print C
Finish
Event Queue
Future callback
↓
print B
The Event Queue
The event queue contains asynchronous events waiting to be handled by the isolate.
Examples can include work associated with:
Timers
Future callbacks
I/O completion
User interaction
Network completion
Other asynchronous events
A simplified flow:
Event arrives
↓
Event Queue
↓
Event Loop
↓
Callback executes
The event loop processes these events one at a time within the isolate.
The Microtask Queue
Dart also has another queue:
Microtask Queue
Microtasks have higher priority than regular event-queue events.
You can schedule one explicitly:
scheduleMicrotask(() {
print('Microtask');
});
Example:
import 'dart:async';
void main() {
print('Start');
Future(() {
print('Future');
});
scheduleMicrotask(() {
print('Microtask');
});
print('End');
}
Typical output:
Start
End
Microtask
Future
Why?
Because after synchronous execution completes, Dart processes pending microtasks before moving to the next event.
Simplified Dart Event Loop Order
A useful mental model is:
Run synchronous code
↓
Current stack becomes empty
↓
Process microtasks
↓
Process next event
↓
Process newly queued microtasks
↓
Process next event
↓
Repeat
Therefore:
Synchronous code
↓
Microtasks
↓
Event
↓
Microtasks
↓
Next Event
This ordering is extremely important when understanding Dart concurrency.
Real Queue Example
Consider:
import 'dart:async';
void main() {
print('1');
Future(() {
print('2');
});
scheduleMicrotask(() {
print('3');
});
Future(() {
print('4');
});
scheduleMicrotask(() {
print('5');
});
print('6');
}
Output:
1
6
3
5
2
4
The execution can be understood as:
Synchronous
-----------
1
6
Microtask Queue
---------------
3
5
Event Queue
-----------
2
4
So the final order becomes:
1
6
3
5
2
4
Why Does Dart Have Microtasks?
Microtasks are useful for work that should happen:
After the current synchronous operation finishes, but before the next regular event is processed.
However, developers should avoid filling the microtask queue with unnecessary work.
Consider:
void runAgain() {
scheduleMicrotask(runAgain);
}
If microtasks continuously schedule more microtasks, regular event processing may be delayed.
This is sometimes called event starvation.
Conceptually:
Microtask
↓
Microtask
↓
Microtask
↓
Microtask
↓
Microtask
↓
Regular Event
↓
Still waiting...
Therefore, scheduleMicrotask() should be used intentionally.
Where Do Futures Fit Into Dart Concurrency?
A Future<T> represents an asynchronous operation that eventually completes with either:
Value
or:
Error
For example:
Future<String> fetchName() async {
await Future.delayed(
const Duration(seconds: 2),
);
return 'Flutter';
}
Calling:
final name = await fetchName();
does not mean Dart blocks the entire isolate for two seconds.
Instead, the asynchronous function suspends while waiting.
Other events can be processed.
How async and await Work Conceptually
Consider:
Future<void> loadData() async {
print('Start');
final data = await fetchData();
print(data);
}
Conceptually:
Enter loadData()
↓
print Start
↓
Start fetchData()
↓
await
↓
Suspend this function
↓
Return control to event loop
↓
Other events can run
↓
Future completes
↓
Continuation scheduled
↓
Resume loadData()
↓
print data
This is one of the foundations of Dart concurrency.
await Does Not Block the Entire Isolate
This is important.
Consider:
await Future.delayed(
const Duration(seconds: 5),
);
Dart does not normally sit doing this:
Wait...
Wait...
Wait...
Wait...
Wait...
while preventing everything else from running.
Instead:
Start Timer
↓
Suspend async function
↓
Event loop continues
↓
Other work runs
↓
Timer completes
↓
Function continues
That is why Flutter can remain responsive while waiting for an API request or timer.
async Does Not Create a New Thread
This is one of the most common Dart misconceptions.
Consider:
Future<void> calculate() async {
heavyCalculation();
}
Adding:
async
does not move:
heavyCalculation();
to another thread or isolate.
If heavyCalculation() is synchronous and CPU-intensive, it still executes on the current isolate.
Therefore:
async
≠
new thread
await
≠
new isolate
Future Does Not Automatically Mean Background Execution
This is also important:
Future(() {
heavyCalculation();
});
does not automatically make CPU-heavy Dart code run on another isolate.
It schedules the callback asynchronously, but the callback still executes on the same isolate.
Conceptually:
Main Isolate
Current Work
↓
Future scheduled
↓
Current work finishes
↓
Future callback runs
↓
Heavy calculation
↓
Main isolate blocked during calculation
Therefore, Futures solve asynchronous coordination.
They do not automatically solve CPU-intensive computation.
I/O-Bound vs CPU-Bound Work
This distinction explains when normal Dart concurrency is enough.
I/O-Bound Work
Examples:
HTTP requests
Database calls
File I/O
Network communication
Timers
These operations spend much of their time waiting.
Use:
async
await
Future
Stream
as appropriate.
CPU-Bound Work
Examples:
Large JSON parsing
Image processing
Encryption
Compression
Huge sorting operations
Complex calculations
Large data transformations
These operations require significant CPU execution.
If they run synchronously on the main isolate, they can block it.
For expensive CPU-bound work, consider:
Isolate.run()
or another appropriate isolate architecture.
Example: API Request
Consider:
final response = await http.get(url);
The application spends much of the operation waiting for network I/O.
Conceptually:
Send Request
↓
Wait for Network
↓
Event Loop continues
↓
Response arrives
↓
Continue function
You usually do not need to create another isolate simply for a normal HTTP request.
Example: Large JSON Parsing
Now consider:
final data =
jsonDecode(response.body);
JSON decoding is CPU work.
For a small response:
2 KB
the work may be negligible.
For a huge response containing tens of thousands of objects, parsing may take enough time to affect responsiveness.
Then:
final users = await Isolate.run(() {
return parseUsers(
response.body,
);
});
may be appropriate.
The architecture becomes:
Main Isolate Worker Isolate
HTTP Request
│
▼
Response
│
├────────────────────► Parse JSON
│ │
UI continues │
│ │
◄──────────────────── Result
Isolates and Parallel Execution
Every isolate has independent state and its own event loop.
Conceptually:
Isolate A
─────────
Memory A
Event Loop A
Queue A
Isolate B
─────────
Memory B
Event Loop B
Queue B
They do not simply share normal mutable Dart objects.
Instead, isolates communicate by sending messages.
Isolate.run()
For one-off CPU-heavy tasks, Dart provides:
Isolate.run()
Example:
final result = await Isolate.run(() {
var total = 0;
for (var i = 0; i < 100000000; i++) {
total += i;
}
return total;
});
Conceptually:
Main Isolate
│
│ Start computation
▼
Worker Isolate
│
│ Heavy work
▼
Result
│
▼
Main Isolate
This allows the main isolate to remain available for other work while the CPU-intensive computation happens independently.
Isolate.spawn()
For more advanced scenarios, Dart provides:
Isolate.spawn()
This is useful when you want a longer-lived worker.
Conceptually:
Main Isolate
│
├── Task 1 ─────►
├── Task 2 ─────► Worker Isolate
├── Task 3 ─────►
│
◄── Results ─────
Communication commonly uses:
SendPort
and:
ReceivePort
This is more complex than Isolate.run(), but gives you greater control.
Event-Loop Concurrency vs Isolate Parallelism
This distinction is critical.
Within One Isolate
Event A
↓
Event B
↓
Event C
Dart callbacks execute sequentially.
Concurrency comes from asynchronous operations yielding control to the event loop.
Across Multiple Isolates
Isolate A ───── CPU Work
Isolate B ───── CPU Work
The isolates can execute independently.
On platforms with multiple CPU cores, this can allow actual parallel computation.
Streams and Dart Concurrency
A Future represents one eventual result.
A Stream represents multiple asynchronous events over time.
Future:
Start
↓
Result
↓
Done
Stream:
Start Listening
↓
Event
↓
Event
↓
Event
↓
...
Streams integrate naturally with Dart’s event-driven concurrency model.
Stream Example
Stream<int> counter() async* {
for (var i = 1; i <= 5; i++) {
await Future.delayed(
const Duration(seconds: 1),
);
yield i;
}
}
Listen:
counter().listen(
(value) {
print(value);
},
);
The stream produces:
1
2
3
4
5
over time.
The isolate does not synchronously wait for one second between every event while blocking all other event processing.
Future vs Stream vs Isolate
These concepts solve different problems.
| Concept | Purpose |
|---|---|
| Future | One asynchronous result |
| Stream | Multiple asynchronous events |
| async/await | Easier Future-based asynchronous control flow |
| Event loop | Schedules work inside an isolate |
| Microtask queue | High-priority deferred work |
| Event queue | Regular asynchronous events |
| Isolate | Independent Dart execution environment |
A useful mental model:
Future
→ One asynchronous result
Stream
→ Multiple asynchronous events
Isolate
→ Independent execution
Real Flutter Example
Imagine a shopping application.
It performs:
Fetch Products
Listen for Notifications
Track Connectivity
Parse Product Data
Render UI
Different Dart concurrency tools may solve different parts.
Fetch Products
final response =
await http.get(url);
Use:
Future + async/await
Notifications
If notifications/events arrive continuously:
Stream
may represent them.
Connectivity Changes
Again:
Stream
is a natural abstraction because connectivity can change repeatedly.
Huge Product Parsing
If parsing is CPU-heavy:
Isolate.run()
may move it away from the main isolate.
So one application can use:
Future
+
Stream
+
Event Loop
+
Isolates
together.
Complete Mental Model
Imagine this Flutter application:
FLUTTER APP
│
▼
MAIN ISOLATE
│
┌───────────┴───────────┐
│ │
Microtask Queue Event Queue
│ │
└───────────┬───────────┘
▼
Event Loop
│
▼
Dart Callbacks
│
┌─────────────────┼─────────────────┐
│ │ │
Future Stream UI Logic
│ │
└─────────────────┴─────────────────┘
CPU-heavy operation?
│
Yes
│
▼
Worker Isolate
│
Heavy Work
│
▼
Result
│
▼
Main Isolate
This is a useful high-level picture of Dart concurrency.
Example: Multiple Futures
Suppose you need:
User
Products
Notifications
A slow approach may be:
final user =
await fetchUser();
final products =
await fetchProducts();
final notifications =
await fetchNotifications();
If these operations are independent, they run sequentially from the caller’s perspective:
Fetch User
↓
Finish
↓
Fetch Products
↓
Finish
↓
Fetch Notifications
You may instead start them together.
final results = await Future.wait([
fetchUser(),
fetchProducts(),
fetchNotifications(),
]);
Conceptually:
fetchUser() ───────────────►
fetchProducts() ───────────►
fetchNotifications() ──────►
↓
Wait for all
This is concurrency.
It does not mean three Dart callbacks necessarily execute simultaneously on three CPU cores.
The asynchronous operations overlap in time.
Future.wait() Real Example
Future<void> loadDashboard() async {
final results = await Future.wait([
fetchProfile(),
fetchProducts(),
fetchNotifications(),
]);
final profile = results[0];
final products = results[1];
final notifications = results[2];
// Update state.
}
This can reduce total waiting time when the operations are independent and mostly I/O-bound.
Do not use this pattern when later operations depend on earlier results.
Sequential vs Concurrent Requests
Suppose:
Request A = 2 seconds
Request B = 2 seconds
Request C = 2 seconds
Sequentially, the total may be roughly:
A ── 2s
B ── 2s
C ── 2s
≈ 6 seconds
Started concurrently:
A ───────── 2s
B ───────── 2s
C ───────── 2s
≈ 2 seconds + overhead
assuming the requests are independent and the external systems can process them concurrently.
This is a practical benefit of concurrency without requiring multiple Dart isolates.
Race Conditions in a Single Isolate
Because callbacks inside one isolate do not execute simultaneously, traditional shared-memory races are less common than with threads.
However, logical race conditions can still happen.
Consider:
Future<void> search(
String query,
) async {
final result =
await api.search(query);
setState(() {
searchResult = result;
});
}
The user searches:
"flutter"
and immediately:
"dart"
Requests:
Request A → flutter
Request B → dart
Suppose B finishes first:
dart result
Then A finishes later:
flutter result
Your UI may incorrectly display the older result.
That is a concurrency bug even though Dart callbacks did not execute simultaneously.
Solving Stale Async Results
One simple approach is to track the latest request.
int requestId = 0;
Future<void> search(
String query,
) async {
final currentRequest =
++requestId;
final result =
await api.search(query);
if (currentRequest != requestId) {
return;
}
setState(() {
searchResult = result;
});
}
Now an older request cannot overwrite the result of a newer one.
This demonstrates an important point:
Single-isolate execution protects you from simultaneous callback execution, but it does not automatically protect you from asynchronous ordering bugs.
Common Concurrency Mistake: Heavy Loops
Consider:
void calculate() {
for (
var i = 0;
i < 1000000000;
i++
) {
// Heavy calculation
}
}
While this synchronous loop executes:
Event Loop
↓
Heavy Loop
↓
Still Running
↓
Still Running
the isolate cannot process other queued callbacks normally.
The Flutter UI can freeze.
Adding:
async
does not fix it.
Better Approach for CPU-Heavy Work
Move genuinely expensive work to an isolate:
final result =
await Isolate.run(() {
return heavyCalculation();
});
Now:
Main Isolate
│
├── UI
├── Input
└── Other events
Worker Isolate
│
└── Heavy Calculation
This is why understanding CPU-bound vs I/O-bound work is essential.
Common Concurrency Mistake: Overusing Microtasks
Avoid scheduling normal application work as microtasks without a reason.
For example:
scheduleMicrotask(() {
performLargeOperation();
});
This does not move the operation to another isolate.
It simply schedules it in the microtask queue.
If performLargeOperation() takes 500 milliseconds, it can still block the isolate for roughly that duration once executed.
Common Concurrency Mistake: Assuming Future Means Parallel
This:
Future(() {
calculate();
});
does not mean:
CPU Core 1 → UI
CPU Core 2 → calculate()
It means the calculation is scheduled to run later on the same isolate.
For independent CPU execution, isolates are the relevant Dart abstraction.
Common Concurrency Mistake: Sequential Independent Requests
If these requests are independent:
final a = await getA();
final b = await getB();
final c = await getC();
you may be unnecessarily waiting for each request before starting the next.
Consider:
final results = await Future.wait([
getA(),
getB(),
getC(),
]);
when concurrency is safe and appropriate.
Common Concurrency Mistake: Ignoring Errors
Concurrent operations can fail.
For example:
try {
final results =
await Future.wait([
fetchProfile(),
fetchProducts(),
]);
// Use results.
} catch (error) {
// Handle failure.
}
Design error handling according to whether:
All tasks must succeed
or:
Partial results are acceptable
The correct strategy depends on your application.
Concurrency and Flutter UI
Flutter aims to produce frames quickly enough for the display refresh rate.
At 60 Hz, one display interval is approximately:
16.67 ms
At 120 Hz:
8.33 ms
Long synchronous Dart work on the main isolate can delay frame-related processing.
For example:
Button Tap
↓
Parse Huge JSON
↓
200 ms synchronous work
↓
Main isolate occupied
↓
Visible jank / frozen interaction
Move sufficiently expensive CPU work away from the main isolate when profiling shows it is necessary.
Dart Concurrency Architecture in a Flutter App
A practical application might be structured like this:
UI
│
▼
Controller / State
│
▼
Repository
│
├────────────► API
│ │
│ ▼
│ Future
│
├────────────► Database Watcher
│ │
│ ▼
│ Stream
│
└────────────► Heavy Processing
│
▼
Isolate.run()
Each mechanism has a clear purpose.
Future, Stream, and Isolate Example
Imagine a real-time chat application.
Initial Profile
Future<User> fetchProfile()
One request.
One result.
Use a Future.
Chat Messages
Stream<List<Message>>
watchMessages()
Messages continue changing.
Use a Stream.
Heavy Encryption
final encrypted =
await Isolate.run(
() => encryptLargeData(data),
);
CPU-heavy processing.
Consider an isolate.
Now all three mechanisms work together:
Future
→ One-time profile
Stream
→ Real-time messages
Isolate
→ Expensive encryption
Does Dart Use Multiple Threads?
At the Dart programming-model level, isolates are the key abstraction rather than shared-memory threads.
A single isolate processes Dart callbacks sequentially through its event loop.
The Dart runtime and underlying platform may use threads internally for runtime or I/O implementation details, but application developers generally reason in terms of:
Isolates
Events
Futures
Streams
Messages
rather than directly managing shared-memory threads.
Do Isolates Share Memory?
Ordinary mutable Dart state is isolated between isolates.
Conceptually:
Isolate A
│
Memory A
Isolate B
│
Memory B
Communication occurs through messages.
For example:
Isolate A
│
│ Send message
▼
Isolate B
This avoids many shared-memory synchronization problems associated with traditional threading models.
Does async/await Make Flutter Faster?
Not automatically.
async/await improves asynchronous control flow and allows code to yield while waiting for asynchronous operations.
For I/O:
await http.get(url);
this is exactly what you want.
For CPU-heavy synchronous work:
heavyCalculation();
adding async does not make the computation faster or move it elsewhere.
Use the right tool for the right workload.
Dart Concurrency Decision Guide
A useful decision flow is:
What kind of work is it?
│
├── One async result
│ ↓
│ Future
│
├── Multiple async events
│ ↓
│ Stream
│
└── Heavy CPU computation
↓
Isolate
Then for Futures:
Independent operations?
│
Yes
↓
Consider starting them concurrently
↓
Future.wait()
For CPU-heavy work:
One-off task?
│
Yes
↓
Isolate.run()
Repeated/persistent worker?
│
Yes
↓
Isolate.spawn()
Quick Reference Table
| Problem | Dart Tool |
|---|---|
| One asynchronous result | Future |
| Multiple asynchronous events | Stream |
| Wait for Future | await |
| Produce asynchronous Future | async |
| Produce Stream generator | async* |
| Emit Stream event | yield |
| Run independent Futures together | Future.wait() |
| High-priority deferred callback | scheduleMicrotask() |
| One CPU-heavy task | Isolate.run() |
| Persistent worker isolate | Isolate.spawn() |
| Isolate communication | SendPort / ReceivePort |
Frequently Asked Questions
Is Dart single-threaded?
A single Dart isolate executes Dart callbacks sequentially through its event loop. Dart applications can create multiple isolates for independent execution.
What is concurrency in Dart?
Concurrency is Dart’s ability to manage multiple operations whose lifetimes overlap, commonly through Futures, Streams, the event loop, and isolates.
Is async/await concurrent?
async/await helps coordinate asynchronous operations. When a function awaits an incomplete Future, it can yield control so other queued work can proceed.
Does async create a new thread?
No.
Marking a function async does not automatically create a new thread or isolate.
Does Future run in another thread?
Not automatically.
A Future represents asynchronous completion. CPU-heavy Dart callbacks scheduled through Futures still execute on their isolate unless explicitly moved elsewhere.
What is the event loop?
The event loop is the mechanism through which an isolate processes scheduled asynchronous work.
What is the difference between the event queue and microtask queue?
Microtasks are processed before the next regular event. The event queue contains regular asynchronous events waiting to be handled.
What is parallelism in Dart?
For Dart application code, multiple isolates can execute independently and may run in parallel when the platform provides appropriate CPU resources.
When should I use an isolate?
Use an isolate when sufficiently expensive CPU-bound work would otherwise occupy the main isolate long enough to hurt responsiveness.
Should API calls use isolates?
Usually not. Normal network requests are I/O-bound and work well with Futures and async/await.
CPU-heavy processing of the response may be a separate isolate candidate.
Future or Stream?
Use a Future for one eventual result and a Stream for multiple asynchronous events over time.
Final Thoughts
Dart concurrency becomes much easier once you stop thinking of every asynchronous operation as “running in the background.”
A single isolate follows an event-driven model:
Synchronous Dart Code
↓
Microtask Queue
↓
Event Queue
↓
Event Loop
↓
Next Callback
Futures let you represent one asynchronous completion:
Operation
↓
Wait
↓
Result
Streams represent multiple asynchronous events:
Event
↓
Event
↓
Event
↓
...
Meanwhile, isolates solve a different problem:
Main Isolate
│
├── UI / normal Dart work
│
└───────────────┐
│
▼
Worker Isolate
│
CPU-heavy work
│
▼
Result
Therefore, the practical model is:
One asynchronous result
→ Future
Multiple asynchronous events
→ Stream
Independent I/O operations
→ Start concurrently when appropriate
CPU-heavy synchronous computation
→ Isolate
Task ordering inside an isolate
→ Event loop + queues
The biggest mistake is assuming that async, await, or Future automatically moves expensive computation to another thread.
It does not.
Dart gets much of its concurrency from non-blocking asynchronous operations and event-loop scheduling inside an isolate, while multiple isolates provide independent execution for workloads that should not occupy the main isolate.
Once you understand that distinction, Futures, Streams, the event loop, microtasks, and isolates stop looking like separate Dart features. They become different parts of the same concurrency model.




