Skip to main content

Overview

The Semaphore class provides a counting semaphore for limiting concurrent access to resources. It maintains a pool of permits that tasks can acquire and release.
For most use cases, use scope({ concurrency: n }) instead of creating a Semaphore directly. This automatically applies concurrency limits to all tasks in the scope.

Creation

Semaphores are created using scope.semaphore():
import { scope } from 'go-go-scope'

await using s = scope()

// Create semaphore with 3 permits
const sem = s.semaphore(3)

// At most 3 tasks can hold permits concurrently
initialPermits
number
required
Number of available permits
semaphore
Semaphore
A new Semaphore instance

Core Methods

acquire()

Acquire a permit and execute a function. Automatically releases permit when done.
const sem = s.semaphore(3)

// At most 3 of these will run concurrently
const result = await sem.acquire(async () => {
  // Critical section - limited concurrency
  return await heavyOperation()
})
fn
() => Promise<T>
required
Function to execute with acquired permit
result
Promise<T>
Result of the function
The permit is automatically released even if the function throws an error.

execute()

Alias for acquire(). Same behavior.
const result = await sem.execute(async () => {
  return await operation()
})
fn
() => Promise<T>
required
Function to execute
result
Promise<T>
Result of the function

tryAcquire()

Try to acquire a permit without blocking. Returns true if successful.
const sem = s.semaphore(3)

if (sem.tryAcquire()) {
  try {
    await operation()
  } finally {
    // Must manually release!
    sem.release()
  }
} else {
  console.log('No permits available')
}
acquired
boolean
True if permit was acquired
When using tryAcquire(), you must manually release the permit. Consider using tryAcquireWithFn() instead for automatic cleanup.

tryAcquireWithFn()

Try to acquire a permit and execute a function. Returns undefined if no permit available.
const sem = s.semaphore(3)

const result = await sem.tryAcquireWithFn(async () => {
  return await operation()
})

if (result === undefined) {
  console.log('No permit available, operation skipped')
} else {
  console.log('Operation completed:', result)
}
fn
() => Promise<T>
required
Function to execute if permit acquired
result
Promise<T | undefined>
Function result if permit was acquired, undefined otherwise

acquireWithTimeout()

Acquire a permit with a timeout. Returns false if timeout expires.
const sem = s.semaphore(3)

// Try to acquire for up to 5 seconds
const acquired = await sem.acquireWithTimeout(5000)

if (acquired) {
  try {
    await operation()
  } finally {
    // Must manually release!
    sem.release()
  }
} else {
  console.log('Timeout waiting for permit')
}
timeoutMs
number
required
Timeout in milliseconds
acquired
Promise<boolean>
True if permit was acquired within timeout
When using acquireWithTimeout(), you must manually release the permit. Consider using acquireWithTimeoutAndFn() instead.

acquireWithTimeoutAndFn()

Acquire with timeout and execute a function. Returns undefined if timeout.
const sem = s.semaphore(3)

const result = await sem.acquireWithTimeoutAndFn(5000, async () => {
  return await operation()
})

if (result === undefined) {
  console.log('Timeout or operation failed')
}
timeoutMs
number
required
Timeout in milliseconds
fn
() => Promise<T>
required
Function to execute
result
Promise<T | undefined>
Function result if successful, undefined if timeout

bulkAcquire()

Acquire multiple permits at once.
const sem = s.semaphore(10)

// Acquire 5 permits
const result = await sem.bulkAcquire(5, async () => {
  // Critical section with 5 permits
  return await batchOperation()
})
count
number
required
Number of permits to acquire
fn
() => Promise<T>
required
Function to execute
result
Promise<T>
Function result
Blocks until all requested permits are available. Permits must be available atomically.

tryBulkAcquire()

Try to acquire multiple permits without blocking.
const sem = s.semaphore(10)

if (sem.tryBulkAcquire(5)) {
  try {
    await batchOperation()
  } finally {
    // Must manually release all permits!
    for (let i = 0; i < 5; i++) {
      sem.release()
    }
  }
}
count
number
required
Number of permits to try acquiring
acquired
boolean
True if all permits were acquired

Properties

available

Number of available permits.
const sem = s.semaphore(5)

console.log(sem.available)  // 5

await sem.acquire(async () => {
  console.log(sem.available)  // 4 (inside critical section)
  await operation()
})

console.log(sem.available)  // 5 (permit released)
available
number
Number of available permits

waiting

Number of tasks waiting to acquire a permit.
const sem = s.semaphore(1)

// Start 3 tasks
const t1 = sem.acquire(async () => await sleep(1000))
const t2 = sem.acquire(async () => await sleep(1000))
const t3 = sem.acquire(async () => await sleep(1000))

console.log(sem.waiting)  // 2 (t2 and t3 are waiting)

await Promise.all([t1, t2, t3])

console.log(sem.waiting)  // 0
waiting
number
Number of waiting tasks

totalPermits

Total number of permits (initial capacity).
const sem = s.semaphore(10)

console.log(sem.totalPermits)  // 10
totalPermits
number
Total permit capacity

Patterns

Rate Limiting API Calls

import { scope } from 'go-go-scope'

await using s = scope()

// Limit to 10 concurrent API calls
const apiLimiter = s.semaphore(10)

const results = await s.parallel(
  urls.map(url => async () => {
    return await apiLimiter.acquire(async () => {
      return await fetch(url)
    })
  })
)

Database Connection Pool

import { scope } from 'go-go-scope'

await using s = scope()

// Limit concurrent database queries
const dbLimiter = s.semaphore(20)

const query = async (sql: string) => {
  return await dbLimiter.acquire(async () => {
    const conn = await pool.getConnection()
    try {
      return await conn.query(sql)
    } finally {
      conn.release()
    }
  })
}

const users = await query('SELECT * FROM users')

Worker Pool

import { scope } from 'go-go-scope'

await using s = scope()

const workers = s.semaphore(4)  // 4 worker threads

for (const job of jobs) {
  s.task(async () => {
    await workers.acquire(async () => {
      await processJob(job)
    })
  })
}

Graceful Degradation

import { scope } from 'go-go-scope'

await using s = scope()

const sem = s.semaphore(5)

const processRequest = async (req: Request) => {
  // Try to acquire permit with timeout
  const result = await sem.tryAcquireWithFn(async () => {
    return await expensiveOperation(req)
  })
  
  if (result === undefined) {
    // No permit available - use fallback
    return await cheapFallback(req)
  }
  
  return result
}

Batching with Semaphore

import { scope } from 'go-go-scope'

await using s = scope()

const batchSem = s.semaphore(100)  // 100 items per batch

const items: Item[] = []
const itemsLock = s.semaphore(1)  // Mutex for items array

for (const item of allItems) {
  // Try to add to batch
  const added = await batchSem.tryAcquireWithFn(async () => {
    await itemsLock.acquire(async () => {
      items.push(item)
    })
  })
  
  if (!added) {
    // Batch is full - process it
    await processBatch(items)
    
    // Reset
    await itemsLock.acquire(async () => {
      items.length = 0
    })
    
    // Release all batch permits
    // (In production, track this more carefully)
  }
}

// Process remaining
if (items.length > 0) {
  await processBatch(items)
}

Fair Scheduling

import { scope } from 'go-go-scope'

await using s = scope()

const fairSem = s.semaphore(1)  // Ensure fair ordering

// Multiple tasks acquire in order
const t1 = s.task(async () => {
  await fairSem.acquire(async () => {
    console.log('Task 1')
  })
})

const t2 = s.task(async () => {
  await fairSem.acquire(async () => {
    console.log('Task 2')
  })
})

const t3 = s.task(async () => {
  await fairSem.acquire(async () => {
    console.log('Task 3')
  })
})

// Output: Task 1, Task 2, Task 3 (in order)
await Promise.all([t1, t2, t3])

Examples

Basic Usage

import { scope } from 'go-go-scope'

await using s = scope()

const sem = s.semaphore(3)

// Run 10 tasks with max 3 concurrent
const tasks = Array.from({ length: 10 }, (_, i) =>
  s.task(async () => {
    return await sem.acquire(async () => {
      console.log(`Task ${i} started`)
      await sleep(1000)
      console.log(`Task ${i} finished`)
      return i
    })
  })
)

const results = await Promise.all(tasks)

With Scope Concurrency

import { scope } from 'go-go-scope'

// Scope-level concurrency (recommended)
await using s = scope({ concurrency: 3 })

// All tasks automatically limited to 3 concurrent
const tasks = Array.from({ length: 10 }, (_, i) =>
  s.task(async () => {
    console.log(`Task ${i} started`)
    await sleep(1000)
    return i
  })
)

const results = await Promise.all(tasks)

Timeout Example

import { scope } from 'go-go-scope'

await using s = scope()

const sem = s.semaphore(1)

// First task holds the permit
const t1 = sem.acquire(async () => {
  await sleep(10000)  // Hold for 10 seconds
})

// Second task times out
const result = await sem.acquireWithTimeoutAndFn(1000, async () => {
  return 'success'
})

if (result === undefined) {
  console.log('Timeout - permit not available within 1 second')
}

await t1

Bulk Acquire Example

import { scope } from 'go-go-scope'

await using s = scope()

const sem = s.semaphore(100)

// Process items in batches of 10
const batches = chunk(items, 10)

for (const batch of batches) {
  await sem.bulkAcquire(batch.length, async () => {
    await Promise.all(batch.map(item => processItem(item)))
  })
}

Build docs developers (and LLMs) love