Golang · Aug 26, 2026 · 9 min read

Worker Pool in Golang

How to build a worker pool in Golang using goroutines and channels to process tasks concurrently and efficiently.

Worker pool in Go explained with a bank teller analogy: a job channel of tickets feeding a fixed pool of tellers, whose finished receipts go to a results channel.

A worker pool is one of the most common concurrency patterns in Golang. Instead of spawning a new goroutine for every incoming task, we limit the number of workers running at the same time so resource usage stays under control.

1. What is a Worker Pool?

A worker pool consists of a fixed number of goroutines (workers) that pull work from a shared channel concurrently. This pattern is useful when the number of tasks is very large, but you want to cap how many run in parallel so you don't overload the CPU, memory, or an external resource like a database.

2. Analogy: Bank Teller Counter

Imagine a bank with 3 teller counters, and a line of customers waiting to be served.

  • Jobs channel = the queue number (ticket) a customer takes and drops into the queue box
  • Worker = a teller (a limited number, say 3 people)
  • Results channel = the box where completed transactions are placed

Here's how it works:

  • A customer arrives, takes a ticket, and drops it into the jobs channel (the queue box)
  • Whenever a teller (worker) is free, they pick up one ticket from the queue box and serve that customer
  • Once all tickets are gone and the queue box is closed (close(jobs)), an idle teller knows: "there are no more customers, I can go home"
  • The result of the transaction (e.g. a receipt) is placed into the results box (results channel)

3. Building a Simple Worker Pool

Here's an implementation that follows the bank teller analogy above, using goroutines, channels, and sync.WaitGroup to wait for every teller to finish.

main.go
func teller(id int, ticketQueue <-chan int, receipts chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	for ticket := range ticketQueue {
		receipts <- fmt.Sprintf("Ticket #%d served by teller %d", ticket, id)
	}
}

func main() {
	ticketQueue := make(chan int, 100)
	receipts := make(chan string, 100)
	var wg sync.WaitGroup

	// Open 3 teller counters
	for id := 1; id <= 3; id++ {
		wg.Add(1)
		go teller(id, ticketQueue, receipts, &wg)
	}

	// Customers take a ticket and join the queue
	for ticket := 1; ticket <= 9; ticket++ {
		ticketQueue <- ticket
	}
	close(ticketQueue) // no more customers — tellers can leave once the queue is empty

	wg.Wait()
	close(receipts)
}

The ticketQueue channel acts as the customer queue, while receipts holds the outcome from each teller. Closing ticketQueue after every ticket has been handed out tells idle tellers that no new customers are coming.

4. Advantages of a Worker Pool

  • Caps the number of active goroutines, keeping memory usage under control
  • Prevents overloading external resources such as a database or an API
  • More stable throughput, since the number of workers stays fixed even as workload grows
  • Easier to measure and tune compared to spawning unbounded goroutines

5. Practice Exercise: Order Processing Simulation

Try solving this on your own before checking the reference solution.

exercise.go
// Problem: Order Processing Simulation
// Build a program that simulates an order processing system for an online store:
// Requirements:
// 1. There are 15 incoming orders; each order has an OrderID (int) 
//    and an Amount (float64) — you may generate them manually or with a loop.
// 2. Create 5 workers that process the orders in parallel. 
//    "Processing" is just simulated with time.Sleep(200 * time.Millisecond) 
//    per order (treat it as a payment validation delay).
// 3. Whenever a worker finishes processing an order, 
//    send the result as an OrderResult struct containing OrderID, 
//    Status (string, e.g. "processed"), and WorkerID (which worker processed it) 
//    to the result channel.
// 4. After all orders are processed, the program prints the total execution time 
//    (using time.Since), and prints how many orders were processed by each worker 
//    (e.g. Worker 1: 3 orders, Worker 2: 4 orders, ...).
View the reference solution on GitHub

Conclusion

A worker pool helps you use Golang's concurrency efficiently without sacrificing system stability. By sizing the number of workers to match the available resources, you can process many tasks in parallel safely and predictably.

Share this article