Pool
A Pool<A, E> owns a bounded set of expensive, scoped resources — database
connections, HTTP clients, buffers, anything where acquisition is costly and
total concurrency must be capped. Instead of each fiber acquiring and releasing
its own resource, fibers borrow a shared resource from the pool and return
it automatically.
The pool acquires each item with a scoped effect (Effect<A, E, Scope>), keeps
items alive between a minimum and maximum size, and releases everything when the
pool’s own scope closes. Borrowing is done with Pool.get, which checks
out an item scoped to the caller — when that scope closes, the item goes
back into the pool for reuse.
Why a pool
Section titled “Why a pool”Imagine a resource that takes real time and money to set up — a database connection. Without a pool, 100 concurrent requests would open 100 connections. A pool lets those 100 requests share, say, 10 connections: the 11th request waits for one of the first 10 to be returned.
import { Effect, Pool, Scope } from "effect"
interface Connection { readonly execute: (sql: string) => Effect.Effect<ReadonlyArray<string>> readonly close: Effect.Effect<void>}
// `acquire` is a *scoped* effect: it acquires a connection and registers its// own release. The pool runs this once per item it keeps alive.const acquireConnection: Effect.Effect<Connection, never, Scope.Scope> = Effect.acquireRelease( Effect.sync(() => ({ execute: (sql: string) => Effect.succeed([`executed: ${sql}`]), close: Effect.void })), (conn) => conn.close )Creating a fixed-size pool and borrowing
Section titled “Creating a fixed-size pool and borrowing”The simplest pool is fixed-size: Pool.make keeps exactly size
items. Borrow one with Pool.get. Because get returns an
Effect<A, E, Scope>, the borrowed item is returned to the pool when the
borrowing scope closes — you never call a release yourself.
import { Effect, Pool } from "effect"
const program = Effect.gen(function*() { // `make` itself needs a Scope: closing that scope shuts the pool down. const pool = yield* Pool.make({ acquire: acquireConnection, size: 10 })
// Borrow a connection. `Effect.scoped` opens a scope for the checkout, so the // connection is returned to the pool the moment this block finishes. const rows = yield* Effect.scoped( Effect.gen(function*() { const conn = yield* Pool.get(pool) return yield* conn.execute("select * from users") }) )
return rows // => ["executed: select * from users"]})
// `Effect.scoped` here owns the *pool's* lifetime: connections close on exit.Effect.runPromise(Effect.scoped(program))Bounding concurrent use per item
Section titled “Bounding concurrent use per item”concurrency controls how many fibers may use a single item at once
(default 1). A pool with size: 4 and concurrency: 2 can serve up to 8
simultaneous checkouts before callers start waiting. This is useful when a
single resource (such as an HTTP client with connection multiplexing) safely
supports several concurrent operations.
import { Effect, Pool } from "effect"
const pool = Pool.make({ acquire: acquireConnection, size: 4, concurrency: 2 // up to 4 * 2 = 8 concurrent checkouts})Dynamic sizing with makeWithTTL
Section titled “Dynamic sizing with makeWithTTL”When load is spiky, a fixed size is wasteful (idle resources) or limiting (too
few under burst). Pool.makeWithTTL grows the pool up to max
under load and reclaims excess idle items after a timeToLive, shrinking
back toward min.
import { Duration, Effect, Pool } from "effect"
const program = Effect.gen(function*() { const pool = yield* Pool.makeWithTTL({ acquire: acquireConnection, min: 2, // always keep at least 2 connections warm max: 20, // never exceed 20 timeToLive: Duration.seconds(60) // reclaim idle excess after 60s })
const conn = yield* Pool.get(pool) return yield* conn.execute("select 1")})
Effect.runPromise(Effect.scoped(program))timeToLiveStrategy chooses how the TTL is measured:
"usage"(default) — excess items expire relative to overall pool usage."creation"— each item expires a fixedtimeToLiveafter it was created.
const pool = Pool.makeWithTTL({ acquire: acquireConnection, min: 2, max: 20, timeToLive: Duration.minutes(5), timeToLiveStrategy: "creation"})targetUtilization (a value in (0, 1], default 1) decides how eagerly the
pool grows. At 1, new items are created only once existing items are fully
utilized; at 0.5, new items are created when existing ones reach 50%
utilization, trading more resources for lower latency under load.
Invalidating a broken resource
Section titled “Invalidating a broken resource”If you discover a borrowed item is unhealthy — a dropped connection, a failed
health check — tell the pool to discard it with Pool.invalidate.
The item is finalized once it is no longer checked out, and the pool allocates a
replacement on demand.
import { Effect, Pool } from "effect"
const useConnection = Effect.gen(function*() { const pool = yield* Pool.make({ acquire: acquireConnection, size: 10 })
yield* Effect.scoped( Effect.gen(function*() { const conn = yield* Pool.get(pool) const result = yield* conn.execute("ping").pipe( Effect.catchCause(() => // Connection looks broken: discard it instead of returning it. Effect.as(Pool.invalidate(pool, conn), ["error"] as ReadonlyArray<string>) ) ) return result }) )})Real-world: a pooled service via Layer
Section titled “Real-world: a pooled service via Layer”In an application you rarely thread a Pool around by hand. Instead, build it
once inside a Context.Service and expose it
behind a Layer.
Layer.effect runs construction in the layer’s own scope, so the pool (and all
its connections) is torn down when the layer closes. Expose a withConnection
helper, written with Effect.fn since it returns an Effect.
import { Context, Effect, Layer, Pool, Scope } from "effect"
interface Connection { readonly execute: (sql: string) => Effect.Effect<ReadonlyArray<string>> readonly close: Effect.Effect<void>}
const acquireConnection: Effect.Effect<Connection, never, Scope.Scope> = Effect.acquireRelease( Effect.sync(() => ({ execute: (sql: string) => Effect.succeed([`executed: ${sql}`]), close: Effect.void })), (conn) => conn.close )
class Database extends Context.Service<Database, { // `withConnection` borrows a connection for the duration of `use`, // then returns it to the pool automatically. readonly withConnection: <A, E, R>( use: (conn: Connection) => Effect.Effect<A, E, R> ) => Effect.Effect<A, E, R>}>()("app/Database") { // `Layer.effect` provides a Scope for construction and removes it from the // result, so the pool lives exactly as long as this layer. static layer = Layer.effect( Database, Effect.gen(function*() { const pool = yield* Pool.makeWithTTL({ acquire: acquireConnection, min: 2, max: 10, timeToLive: "1 minute" })
return Database.of({ withConnection: Effect.fn("Database.withConnection")( (use) => Effect.scoped(Effect.flatMap(Pool.get(pool), use)) ) }) }) )}
// Callers depend on the service, never on the pool directly.const query = Effect.gen(function*() { const db = yield* Database return yield* db.withConnection((conn) => conn.execute("select * from orders"))})
Effect.runPromise(query.pipe(Effect.provide(Database.layer)))// => ["executed: select * from orders"]API reference
Section titled “API reference”Every public export of the Pool module, with a runnable snippet.
Creates a fixed-size pool that keeps exactly size items (min equals max,
no growth or shrink). Optional concurrency and targetUtilization tune
per-item sharing. Returns Effect<Pool<A, E>, never, R | Scope> — it needs a
Scope whose closure shuts the pool down.
import { Effect, Pool } from "effect"
const program = Effect.gen(function*() { const pool = yield* Pool.make({ acquire: acquireConnection, size: 5 }) return yield* Effect.scoped( Effect.flatMap(Pool.get(pool), (conn) => conn.execute("select 1")) ) // => ["executed: select 1"]})
Effect.runPromise(Effect.scoped(program))makeWithTTL
Section titled “makeWithTTL”Creates an elastic pool with min/max bounds that grows under load and
reclaims excess idle items after timeToLive. timeToLiveStrategy is
"usage" (default) or "creation".
import { Duration, Effect, Pool } from "effect"
const program = Effect.gen(function*() { const pool = yield* Pool.makeWithTTL({ acquire: acquireConnection, min: 1, max: 8, timeToLive: Duration.seconds(30) }) return yield* Effect.scoped( Effect.flatMap(Pool.get(pool), (conn) => conn.execute("select now()")) ) // => ["executed: select now()"]})
Effect.runPromise(Effect.scoped(program))makeWithStrategy
Section titled “makeWithStrategy”Creates a pool driven by a custom Strategy, giving you full
control over background resizing and item reclamation. make and makeWithTTL
are both thin wrappers over this (a no-op strategy and TTL strategies,
respectively). Use it only when the built-in policies are insufficient.
import { Effect, Pool } from "effect"
// A strategy that does no background resizing and never reclaims —// equivalent to what `make` uses internally.const noopStrategy: Pool.Strategy<Connection, never> = { run: () => Effect.void, onAcquire: () => Effect.void, reclaim: () => Effect.succeed(undefined)}
const program = Effect.gen(function*() { const pool = yield* Pool.makeWithStrategy({ acquire: acquireConnection, min: 2, max: 4, strategy: noopStrategy }) return yield* Effect.scoped( Effect.flatMap(Pool.get(pool), (conn) => conn.execute("ping")) ) // => ["executed: ping"]})
Effect.runPromise(Effect.scoped(program))Borrows one item from the pool as a scoped effect: Effect<A, E, Scope>. The
item is returned to the pool when the surrounding scope closes. If the pool is
at capacity, the effect waits for an item; if acquisition fails, the effect
fails with E (a later get can retry).
import { Effect, Pool } from "effect"
const program = Effect.gen(function*() { const pool = yield* Pool.make({ acquire: acquireConnection, size: 2 })
yield* Effect.scoped( Effect.gen(function*() { const conn = yield* Pool.get(pool) // borrowed here yield* conn.execute("select 1") }) // connection returned to the pool here )})
Effect.runPromise(Effect.scoped(program))invalidate
Section titled “invalidate”Marks a checked-out item as unusable so the pool discards and (lazily) replaces
it instead of returning it for reuse. Returns Effect<void, never, Scope>. The
item is matched by reference equality.
import { Effect, Pool } from "effect"
const program = Effect.gen(function*() { const pool = yield* Pool.make({ acquire: acquireConnection, size: 3 })
yield* Effect.scoped( Effect.gen(function*() { const conn = yield* Pool.get(pool) // Decide the connection is stale and should not be reused. yield* Pool.invalidate(pool, conn) }) )})
Effect.runPromise(Effect.scoped(program))invalidate is also dual, so it pipes:
import { Effect, Pool } from "effect"
declare const pool: Pool.Pool<Connection>declare const conn: Connection
// data-firstPool.invalidate(pool, conn)// data-last (pipeable)pool.pipe(Pool.invalidate(conn))isPool
Section titled “isPool”Type guard that narrows an unknown value to Pool<unknown, unknown>.
import { Effect, Pool } from "effect"
Effect.gen(function*() { const pool = yield* Pool.make({ acquire: acquireConnection, size: 1 }) console.log(Pool.isPool(pool)) // => true console.log(Pool.isPool({})) // => false})The pool’s value, configuration, and strategy interfaces. Most code only ever
touches the Pool value via get/invalidate; the rest are exposed for
inspection and for writing a custom Strategy.
The pool value: Pool<A, E>, where A is the item type and E is the error a
get may fail with. It exposes its normalized Config and
runtime State, and is Pipeable.
import type { Pool } from "effect"
declare const pool: Pool.Pool<Connection>
pool.config.maxSize // numberpool.state.items.size // current item countConfig
Section titled “Config”The read-only, normalized configuration backing a pool: the scoped acquire
effect, concurrency (permits per item), minSize/maxSize, the resizing
strategy, and targetUtilization (clamped into [0.1, 1]).
import type { Pool } from "effect"
declare const pool: Pool.Pool<Connection>const config: Pool.Config<Connection, never> = pool.config
config.minSize // e.g. 2config.maxSize // e.g. 10config.concurrency // permits per itemconfig.targetUtilization // 0.1 .. 1Mutable runtime state of a pool — its scope, the items/available/
invalidated item sets, the bounding semaphores, the availability Latch, the
waiters count, and the isShuttingDown flag. Read it for diagnostics; prefer
the high-level operations for control.
import type { Pool } from "effect"
declare const pool: Pool.Pool<Connection>const state: Pool.State<Connection, never> = pool.state
state.items.size // total acquired itemsstate.available.size // items free to hand outstate.invalidated.size // items marked for removalstate.waiters // fibers currently waiting for an itemstate.isShuttingDown // true once the pool scope is closingPoolItem
Section titled “PoolItem”The internal record for one managed value: its acquisition exit, a
finalizer, the current refCount, and a disableReclaim flag set when the
item is invalidated. You receive PoolItems when implementing a custom
Strategy.
import type { Pool } from "effect"
declare const item: Pool.PoolItem<Connection, never>
item.exit._tag // "Success" | "Failure"item.refCount // how many fibers currently hold ititem.disableReclaim // true when invalidatedStrategy
Section titled “Strategy”The lifecycle contract a pool uses for background resizing and reclamation, with three callbacks:
run(pool)— long-running background work (e.g. a TTL sweep loop), forked by the pool.onAcquire(item)— invoked each time an item is allocated.reclaim(pool)— returns a reusablePoolItemto recycle instead of allocating a fresh one, orundefined.
import { Effect, Pool } from "effect"
// Minimal strategy: no background work, no reclamation.const strategy: Pool.Strategy<Connection, never> = { run: (_pool) => Effect.void, onAcquire: (_item) => Effect.void, reclaim: (_pool) => Effect.succeed(undefined)}
const pool = Pool.makeWithStrategy({ acquire: acquireConnection, min: 1, max: 4, strategy})See also
Section titled “See also”- Scope and finalizers — the lifetime model that governs both the pool and each checkout.
- Acquire and release — how to write
the scoped
acquireeffect a pool needs. - Semaphore and Latch — the concurrency primitives a pool is built from.
- Resource — for caching a single refreshable value rather than pooling many interchangeable ones.