Skip to content

Coordination (TxSemaphore, TxReentrantLock, TxDeferred)

Sometimes what fibers contend over isn’t a value but access: a limited number of permits, exclusive use of a resource, or a signal that fires exactly once. The Tx* coordination primitives — TxSemaphore, TxReentrantLock, and TxDeferred — solve these, and because they are built on TxRef their state changes commit atomically alongside the rest of your transaction.

A TxSemaphore holds a fixed number of permits. Acquiring blocks (via Effect.txRetry) until enough permits are free; releasing returns them, capped at the original capacity. The most ergonomic API is withPermit, which acquires one permit, runs an effect, and releases the permit even on failure or interruption.

import { Context, Effect, Layer, TxSemaphore } from "effect"
// A connection pool that lets at most 3 callers run a query concurrently.
class Db extends Context.Service<Db, {
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}>()("app/Db") {
static layer = Layer.effect(Db)(
Effect.gen(function*() {
// Fixed capacity of 3 permits == at most 3 concurrent queries.
const semaphore = yield* TxSemaphore.make(3)
const query = Effect.fn("Db.query")(function*(sql: string) {
// withPermit brackets the work: acquire 1 permit, run, release.
// A 4th concurrent caller waits here until a permit frees up.
return yield* TxSemaphore.withPermit(
semaphore,
Effect.succeed([] as ReadonlyArray<unknown>)
)
})
return Db.of({ query })
})
)
}

The transactional semaphore type. It carries a permitsRef (a TxRef<number>) holding the current available permits and a fixed capacity.

import { TxSemaphore } from "effect"
declare const sem: TxSemaphore.TxSemaphore
// sem.capacity is the fixed total; sem.permitsRef is the live TxRef<number>

Creates a semaphore with a fixed number of permits. Runs inside Effect.tx; a negative permit count dies with a defect.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(3)
return yield* TxSemaphore.available(sem) // => 3
})

Type guard narrowing an unknown value to TxSemaphore.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(1)
TxSemaphore.isTxSemaphore(sem) // => true
TxSemaphore.isTxSemaphore({}) // => false
})

Acquires one permit, retrying the transaction until a permit is available.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(2)
yield* TxSemaphore.acquire(sem)
return yield* TxSemaphore.available(sem) // => 1
})

Acquires n permits, retrying until all n are available. A non-positive n defects; an n greater than capacity waits forever.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(5)
yield* TxSemaphore.acquireN(sem, 3)
return yield* TxSemaphore.available(sem) // => 2
})

Returns one permit. If the semaphore is already at capacity the count is left unchanged.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(2)
yield* TxSemaphore.acquire(sem) // available => 1
yield* TxSemaphore.release(sem)
return yield* TxSemaphore.available(sem) // => 2
})

Returns n permits, capped at capacity. A non-positive n defects.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(5)
yield* TxSemaphore.acquireN(sem, 3) // available => 2
yield* TxSemaphore.releaseN(sem, 2)
return yield* TxSemaphore.available(sem) // => 4
})

Attempts to acquire one permit without waiting. Returns true if a permit was taken, false otherwise.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(1)
const first = yield* TxSemaphore.tryAcquire(sem) // => true
const second = yield* TxSemaphore.tryAcquire(sem) // => false
})

Attempts to acquire n permits without waiting. Returns true only if all n were available. A non-positive n defects.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(3)
const ok = yield* TxSemaphore.tryAcquireN(sem, 2) // => true
const no = yield* TxSemaphore.tryAcquireN(sem, 2) // => false (only 1 left)
})

Runs an effect with one permit acquired beforehand and released afterwards — even on failure or interruption.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(2)
return yield* TxSemaphore.withPermit(sem, Effect.succeed("done")) // => "done"
})

Runs an effect while holding n permits, releasing all of them afterwards. A non-positive n defects; an n over capacity waits forever.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(5)
return yield* TxSemaphore.withPermits(sem, 3, Effect.succeed("batch")) // => "batch"
})

Acquires one permit tied to the current Scope: the permit is released when the scope closes rather than after a single effect.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(3)
yield* Effect.scoped(
Effect.gen(function*() {
yield* TxSemaphore.withPermitScoped(sem) // held until the scope closes
// ... work while holding the permit ...
})
)
return yield* TxSemaphore.available(sem) // => 3
})

Reads the current number of available permits.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(5)
return yield* TxSemaphore.available(sem) // => 5
})

Reads the fixed total permit count, which never changes.

import { Effect, TxSemaphore } from "effect"
const program = Effect.gen(function*() {
const sem = yield* TxSemaphore.make(10)
yield* TxSemaphore.acquire(sem)
return yield* TxSemaphore.capacity(sem) // => 10
})

A TxReentrantLock is a read/write lock with per-fiber ownership tracking. Many fibers may hold a read lock at once, but a write lock grants one fiber exclusive access. It is reentrant: a fiber may re-acquire a lock it already owns (as long as each acquisition is matched by a release), so nested critical sections are safe.

import { Effect, Ref, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
const state = yield* Ref.make(0)
// withWriteLock grants exclusive access for the duration of the effect,
// then releases — even on failure or interruption. Concurrent readers and
// writers wait until the write lock is dropped.
yield* TxReentrantLock.withWriteLock(lock, Ref.update(state, (n) => n + 1))
// withReadLock allows multiple readers to proceed in parallel, but waits
// while any fiber holds the write lock.
return yield* TxReentrantLock.withReadLock(lock, Ref.get(state)) // => 1
})

Prefer the bracketing helpers withReadLock, withWriteLock, and withLock over manual acquireRead / releaseRead (and their write counterparts) — they guarantee the lock is released no matter how the effect ends. If you need lock ownership tied to a Scope rather than a single effect, readLock and writeLock acquire a lock that is released when the scope closes.

The reentrant read/write lock type. Internally it tracks per-fiber reader counts and an optional writer in a single TxRef.

import { TxReentrantLock } from "effect"
declare const lock: TxReentrantLock.TxReentrantLock

Creates a new unlocked reentrant lock. Runs inside Effect.tx.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.locked(lock) // => false
})

Type guard narrowing an unknown value to TxReentrantLock.

import { TxReentrantLock } from "effect"
declare const value: unknown
TxReentrantLock.isTxReentrantLock(value) // => boolean

Acquires a read lock for the current fiber, waiting while another fiber holds the write lock. Returns this fiber’s new read-lock count.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
const count = yield* TxReentrantLock.acquireRead(lock) // => 1
yield* TxReentrantLock.releaseRead(lock)
})

Acquires the write lock for the current fiber, waiting while any other fiber holds a read or write lock. Reentrant for the owning fiber, and upgrades from a read lock the fiber already holds. Returns this fiber’s new write-lock count.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
const count = yield* TxReentrantLock.acquireWrite(lock) // => 1
yield* TxReentrantLock.releaseWrite(lock)
})

Releases one read lock held by the current fiber, returning the remaining count. Releasing when this fiber holds none leaves the lock unchanged and returns 0.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
yield* TxReentrantLock.acquireRead(lock)
return yield* TxReentrantLock.releaseRead(lock) // => 0
})

Releases one write lock held by the current fiber, returning the remaining count. Releasing when this fiber holds none leaves the lock unchanged and returns 0.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
yield* TxReentrantLock.acquireWrite(lock)
return yield* TxReentrantLock.releaseWrite(lock) // => 0
})

Runs an effect while holding a read lock, releasing it afterwards even on failure or interruption. Available data-first and data-last.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.withReadLock(lock, Effect.succeed("read data")) // => "read data"
})

Runs an effect while holding the write lock (exclusive access), releasing it afterwards even on failure or interruption.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("wrote data")) // => "wrote data"
})

A short alias for withWriteLock — runs an effect with exclusive access.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.withLock(lock, Effect.succeed("exclusive")) // => "exclusive"
})

Acquires a read lock tied to the current Scope, released when the scope closes. Returns the fiber’s read-lock count.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
yield* Effect.scoped(
Effect.gen(function*() {
yield* TxReentrantLock.readLock(lock) // held for the scope
})
)
})

Acquires the write lock tied to the current Scope, released when the scope closes. Returns the fiber’s write-lock count.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
yield* Effect.scoped(
Effect.gen(function*() {
yield* TxReentrantLock.writeLock(lock) // held for the scope
})
)
})

Returns the total number of read locks held across all fibers.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
yield* TxReentrantLock.acquireRead(lock)
return yield* TxReentrantLock.readLocks(lock) // => 1
})

Returns the number of write locks held — 0 or the reentrant count of the owning fiber.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.writeLocks(lock) // => 0
})

Returns true if any fiber holds a read or write lock.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.locked(lock) // => false
})

Returns true if any fiber holds a read lock.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.readLocked(lock) // => false
})

Returns true if any fiber holds the write lock.

import { Effect, TxReentrantLock } from "effect"
const program = Effect.gen(function*() {
const lock = yield* TxReentrantLock.make()
return yield* TxReentrantLock.writeLocked(lock) // => false
})

A TxDeferred<A, E> is a transactional, write-once cell — the STM counterpart of a regular deferred. Readers await it from inside a transaction: while it is empty the transaction retries, and once another transaction completes it, every waiter sees the same committed result. Completion is single-assignment, so only the first succeed / fail / done wins; later attempts return false.

import { Effect, Fiber, TxDeferred } from "effect"
const program = Effect.gen(function*() {
// A one-shot handoff: the worker computes a value, the waiter receives it.
const result = yield* TxDeferred.make<number, string>()
const waiter = yield* Effect.forkChild(
// await parks the fiber (transaction retry) until the deferred is
// completed, then resumes with the success value or typed failure.
TxDeferred.await(result)
)
// Complete it exactly once. The waiter wakes with 42.
const first = yield* TxDeferred.succeed(result, 42)
const second = yield* TxDeferred.succeed(result, 99) // ignored
return {
value: yield* Fiber.join(waiter), // 42
first, // true
second // false
}
})

The transactional write-once cell type, parameterized by its success type A and error type E (default never). Its ref holds an Option<Result<A, E>>None while empty.

import { TxDeferred } from "effect"
declare const deferred: TxDeferred.TxDeferred<number, string>

Creates a new empty TxDeferred<A, E>.

import { Effect, Option, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<string, Error>()
const state = yield* TxDeferred.poll(deferred)
return Option.isNone(state) // => true
})

Type guard narrowing an unknown value to TxDeferred.

import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number>()
TxDeferred.isTxDeferred(deferred) // => true
TxDeferred.isTxDeferred("nope") // => false
})

Reads the completed value, retrying the transaction while the cell is empty. Resumes with the success value, or fails with the stored typed error.

import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number>()
yield* TxDeferred.succeed(deferred, 42)
return yield* TxDeferred.await(deferred) // => 42
})

Completes the deferred with a success value. Returns true on the first completion, false if already completed.

import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number>()
const first = yield* TxDeferred.succeed(deferred, 42) // => true
const second = yield* TxDeferred.succeed(deferred, 99) // => false
})

Completes the deferred with a typed failure. Returns true on the first completion, false if already completed.

import { Effect, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number, string>()
const first = yield* TxDeferred.fail(deferred, "boom") // => true
const second = yield* TxDeferred.fail(deferred, "boom2") // => false
})

Completes the deferred with an already computed Result. Returns true on the first completion, false if already completed. Available data-first and data-last.

import { Effect, Result, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number, string>()
const first = yield* TxDeferred.done(deferred, Result.succeed(42)) // => true
const second = yield* TxDeferred.done(deferred, Result.succeed(99)) // => false
})

Inspects the current state without retrying. Returns Option<Result<A, E>>None until the cell is completed, then Some(result).

import { Effect, Option, TxDeferred } from "effect"
const program = Effect.gen(function*() {
const deferred = yield* TxDeferred.make<number>()
const before = yield* TxDeferred.poll(deferred)
Option.isNone(before) // => true
yield* TxDeferred.succeed(deferred, 42)
return yield* TxDeferred.poll(deferred) // => Some(Success(42))
})
You need to coordinate…Use
A limited number of concurrent usersTxSemaphore
Shared reads with exclusive writesTxReentrantLock
A one-time signal or handoffTxDeferred

For coordinating data rather than access, see the transactional data structures; for the underlying atomic-state mechanics, see transactional state.