Runtime and Fibers
The Effect runtime is a fiber-based concurrency system. Fibers are lightweight virtual threads - not OS threads - that the runtime schedules cooperatively. Every effect you run executes inside a fiber, and fibers organize hierarchically through scopes.
Architecture Overview
- The runtime owns the scheduler and the root scope
- Each fiber belongs to a scope; when the scope closes, all fibers in it are interrupted
- Finalizers run in reverse order of registration during scope cleanup
Fiber Lifecycle
Creating a Runtime
You rarely create a bare runtime. The common pattern is ManagedRuntime.make from a Layer that wires all your services:
import { Effect, Layer, ManagedRuntime } from "effect"
const MainLive = Layer.empty // your composed layers here
const runtime = ManagedRuntime.make(MainLive)
// Run effects using the runtime
await runtime.runPromise(
Effect.log("Hello from runtime")
)
For simpler cases, you can use the built-in executors directly:
import { Effect } from "effect"
// Sync execution (effect must not be async)
const result = Effect.runSync(Effect.succeed(42))
// Async execution returning a Promise
await Effect.runPromise(Effect.succeed(42))
// Fork execution, get a Fiber handle
const fiber = Effect.runFork(Effect.succeed(42))
Tip: Use
Effect.runForkwhen you need to control a fiber’s lifecycle explicitly - interrupt it, join it, or inspect its status.
Structured Concurrency Model
Fibers fork within a scope. The parent fiber owns the scope, and child fiber lifetimes are bounded by it. When the parent completes or is interrupted, all children are interrupted too.
import { Effect } from "effect"
const program = Effect.gen(function*() {
// Fork a child fiber in the current scope
const fiber = yield* Effect.fork(
Effect.log("Child running...").pipe(Effect.forever)
)
yield* Effect.sleep("2 seconds")
// Interrupt the child
yield* fiber.interrupt
yield* Effect.log("Parent done, child interrupted")
})
You don’t need to manually interrupt children in most cases. Effect.scoped creates a scope, runs your effect, and cleans up all fibers and resources when it finishes:
const program = Effect.scoped(
Effect.gen(function*() {
const fiber = yield* Effect.fork(Effect.never)
yield* Effect.sleep("1 second")
// scope closes here - fiber is automatically interrupted
})
)
Scope-Based Resource Management
Scopes track finalizers. When a scope closes - whether through normal completion, failure, or interruption - finalizers run in reverse registration order. This guarantees cleanup:
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function*() {
yield* Effect.acquireRelease(
Effect.log("Acquiring resource"),
() => Effect.log("Releasing resource")
)
yield* Effect.acquireRelease(
Effect.log("Acquiring second resource"),
() => Effect.log("Releasing second resource")
)
yield* Effect.log("Working with resources...")
// On scope close: releases second, then first
})
)
Gotcha: If you fork a fiber outside a scope (using
Effect.forkDaemon), it won’t be interrupted when the parent scope closes. Use this deliberately for background tasks that should outlive their parent.
Automatic Keep-Alive (v4)
In v4, the runtime automatically keeps the Node.js process alive while fibers are suspended (e.g., waiting on a timer or I/O). You no longer need NodeRuntime.runMain or manual keep-alive management for suspended fibers.
import { Effect, Layer } from "effect"
// v4: no runMain needed, runtime keeps process alive
const program = Effect.gen(function*() {
yield* Effect.sleep("5 seconds")
yield* Effect.log("Done waiting")
})
Effect.runFork(program)
Runtime Configuration
You can configure the runtime with custom services like clocks, random generators, and schedulers. This makes time-based and random effects fully testable:
import { Effect, TestClock, ManagedRuntime, Layer } from "effect"
const TestLive = Layer.mergeAll(
Layer.setClock(TestClock.TestClock),
// other test services...
)
const runtime = ManagedRuntime.make(TestLive)
// Time is controlled, not real
await runtime.runPromise(
Effect.gen(function*() {
yield* Effect.sleep("1 hour") // instant in test
yield* Effect.log("1 hour passed instantly")
}).pipe(Effect.withTestClock)
)
See Scope and Resources for the full resource lifecycle model, and Layer Composition for how services wire together.