Async, Concurrency, and Parallelism
One isolate, many concurrent obligations
One V8 isolate executes JavaScript on one owner at a time. That does not mean only one operation can be in progress. Several file reads, socket operations, timers, dynamic imports, and messages can all be pending together.
FuturesUnorderedDriver is the central example. It holds heterogeneous async
ops and yields whichever completes next. Completion order need not match
submission order.
isolate submits A ─┐
isolate submits B ─┼─ pending op set ─→ B ready ─→ settle Promise B
isolate submits C ─┘ → A ready ─→ settle Promise A
This is concurrency: multiple lifecycles overlap. It may happen even if only one thread is executing JavaScript.
Where Tokio fits
Deno’s worker exposes its event loop as a Rust future. Tokio polls that future, and lower-level async I/O registers wakers with the relevant driver. When an op completion, timer, worker-control event, or I/O readiness matters, a wake makes the outer future runnable again.
Tokio provides scheduling and readiness. Deno layers language semantics on top:
- dispatch completed ops;
- resolve or reject promises;
- perform V8 microtask checkpoints;
- advance module evaluation;
- process timers, messages, and rejection events;
- decide whether refed work keeps the program alive.
Event-loop phases are fairness and semantic policy
JsRuntime::poll_event_loop_inner advances explicit phases: timers, pending
work, idle/prepare callbacks, I/O, check/immediates, and close callbacks.
Microtasks run at carefully selected boundaries.
The code deliberately avoids draining unbounded I/O in one turn: under sustained readiness, doing so could starve timers and other work and damage tail latency. “Ready” therefore does not mean “consume everything before yielding.”
What is actually parallel?
- Native I/O can progress independently while the isolate executes JavaScript.
- Tokio’s runtime may use multiple OS threads for
Sendwork. - Blocking operations may run on a separate blocking pool.
- Web Workers create separate V8 isolates with their own event loops and can run JavaScript in parallel.
Two JavaScript callbacks in one isolate do not run simultaneously. A Web Worker does not share arbitrary mutable V8 values with its parent; communication goes through messages and control channels.
Local futures are a feature
The op driver uses Rc, RefCell, Cell, and Deno’s unsynchronized task
support. These pending ops can retain isolate-local state and need not all be
Send. The design avoids pretending isolate-local work may migrate freely
between threads.
That yields a useful rule:
Make a future
Sendonly when its ownership really may cross threads. Use a local executor boundary for futures tied to a single-threaded subsystem.
Backpressure is not one global queue
Deno has several distinct pressure points: pending ops, resource-specific buffers, worker message channels, HTTP bodies, and subprocess streams. A single “concurrency limit” would not express all of them. Each subsystem needs a limit matching the resource it protects.
The event loop’s liveness accounting is another kind of bound. Refed work keeps the program alive; unrefed work may make progress during an active turn but does not independently prevent exit.