How to Handle One Million Requests per Minute in Go: Scalable Goroutine Pool Design

This article explains how to build a high‑performance Go server capable of processing up to one million requests per minute using efficient goroutine‑pool patterns, detailing the design of two worker structures, their dispatch, start, stop mechanisms, and practical optimizations.

Open Source Linux
Open Source Linux
Open Source Linux
How to Handle One Million Requests per Minute in Go: Scalable Goroutine Pool Design

Large internet applications such as e‑commerce platforms, social networks, and financial transaction systems receive massive request volumes, often requiring the ability to handle a million requests within a minute. To meet this demand, Go’s efficient concurrency model and goroutine pools are employed.

W1

The W1 struct defines five fields:

WgSend – a *sync.WaitGroup that waits for task‑sending goroutines to finish.

Wg – a *sync.WaitGroup that waits for task‑processing goroutines to finish.

MaxNum – the size of the goroutine pool.

Ch – a chan string used to deliver tasks.

DispatchStop – a chan struct{} used to stop task dispatch.

type W1 struct {
    WgSend       *sync.WaitGroup
    Wg           *sync.WaitGroup
    MaxNum       int
    Ch           chan string
    DispatchStop chan struct{}
}

The Dispatch method sends ten times MaxNum tasks to Ch, launching a goroutine for each task and using defer to decrement WgSend. A select statement allows early exit when DispatchStop is closed.

func (w *W1) Dispatch(job string) {
    w.WgSend.Add(10 * w.MaxNum)
    for i := 0; i < 10*w.MaxNum; i++ {
        go func(i int) {
            defer w.WgSend.Done()
            select {
            case w.Ch <- fmt.Sprintf("%d", i):
                return
            case <-w.DispatchStop:
                fmt.Println("exit dispatch job:", fmt.Sprintf("%d", i))
                return
            }
        }(i)
    }
}

The StartPool method creates the channel and wait groups if they are nil, then launches MaxNum worker goroutines that read from Ch and process tasks.

func (w *W1) StartPool() {
    if w.Ch == nil {
        w.Ch = make(chan string, w.MaxNum)
    }
    if w.WgSend == nil {
        w.WgSend = &sync.WaitGroup{}
    }
    if w.Wg == nil {
        w.Wg = &sync.WaitGroup{}
    }
    if w.DispatchStop == nil {
        w.DispatchStop = make(chan struct{})
    }
    w.Wg.Add(w.MaxNum)
    for i := 0; i < w.MaxNum; i++ {
        go func() {
            defer w.Wg.Done()
            for v := range w.Ch {
                fmt.Printf("completed job: %s 
", v)
            }
        }()
    }
}

The Stop method closes DispatchStop, waits for the sending goroutines, then closes Ch and waits for the processing goroutines.

func (w *W1) Stop() {
    close(w.DispatchStop)
    w.WgSend.Wait()
    close(w.Ch)
    w.Wg.Wait()
}

W2

SubWorker

type SubWorker struct {
    JobChan chan string
}

A SubWorker runs a Run method that continuously reads from JobChan, processes jobs, and exits when QuitChan is closed.

func (sw *SubWorker) Run(wg *sync.WaitGroup, poolCh chan chan string, quitCh chan struct{}) {
    if sw.JobChan == nil {
        sw.JobChan = make(chan string)
    }
    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            poolCh <- sw.JobChan
            select {
            case res := <-sw.JobChan:
                fmt.Printf("completed job: %s 
", res)
            case <-quitCh:
                fmt.Printf("consumer exit... 
")
                return
            }
        }
    }()
}

W2

type W2 struct {
    SubWorkers []SubWorker
    Wg         *sync.WaitGroup
    MaxNum     int
    ChPool     chan chan string
    QuitChan   chan struct{}
}

The Dispatch method retrieves a worker channel from ChPool and sends a job, falling back to a log message when all workers are busy.

func (w *W2) Dispatch(job string) {
    jobChan := <-w.ChPool
    select {
    case jobChan <- job:
        fmt.Printf("sent job: %s completed 
", job)
        return
    case <-w.QuitChan:
        fmt.Printf("sender (%s) exit 
", job)
        return
    }
}

The StartPool method initializes ChPool, creates the slice of SubWorker s, and launches a goroutine for each worker.

func (w *W2) StartPool() {
    if w.ChPool == nil {
        w.ChPool = make(chan chan string, w.MaxNum)
    }
    if w.SubWorkers == nil {
        w.SubWorkers = make([]SubWorker, w.MaxNum)
    }
    if w.Wg == nil {
        w.Wg = &sync.WaitGroup{}
    }
    for i := 0; i < w.MaxNum; i++ {
        w.SubWorkers[i].Run(w.Wg, w.ChPool, w.QuitChan)
    }
}

The Stop method closes QuitChan, waits for all workers, then closes ChPool.

func (w *W2) Stop() {
    close(w.QuitChan)
    w.Wg.Wait()
    close(w.ChPool)
    for _, sw := range w.SubWorkers {
        close(sw.JobChan)
    }
}

The entry function DealW2 creates a W2 instance, starts the pool, dispatches many jobs, and finally stops the pool.

func DealW2(max int) {
    w := NewWorker(w2, max)
    w.StartPool()
    for i := 0; i < 10*max; i++ {
        go w.Dispatch(fmt.Sprintf("%d", i))
    }
    w.Stop()
}

Optimized Version (W2New)

An improved implementation adds dynamic scaling via AddWorker and RemoveWorker, uses a dedicated WaitGroup for lifecycle management, and blocks on dispatch when no worker is immediately available.

type SubWorkerNew struct {
    Id      int
    JobChan chan string
}

type W2New struct {
    SubWorkers []SubWorkerNew
    MaxNum     int
    ChPool     chan chan string
    QuitChan   chan struct{}
    Wg         *sync.WaitGroup
}

func NewW2(maxNum int) *W2New {
    chPool := make(chan chan string, maxNum)
    subWorkers := make([]SubWorkerNew, maxNum)
    for i := 0; i < maxNum; i++ {
        subWorkers[i] = SubWorkerNew{Id: i, JobChan: make(chan string)}
        chPool <- subWorkers[i].JobChan
    }
    wg := new(sync.WaitGroup)
    wg.Add(maxNum)
    return &W2New{MaxNum: maxNum, SubWorkers: subWorkers, ChPool: chPool, QuitChan: make(chan struct{}), Wg: wg}
}

func (w *W2New) StartPool() {
    for i := 0; i < w.MaxNum; i++ {
        go func(sw *SubWorkerNew) {
            defer w.Wg.Done()
            for {
                select {
                case job := <-sw.JobChan:
                    fmt.Printf("SubWorker %d processing job %s
", sw.Id, job)
                    time.Sleep(time.Second)
                case <-w.QuitChan:
                    return
                }
            }
        }(&w.SubWorkers[i])
    }
}

func (w *W2New) Dispatch(job string) {
    select {
    case jobChan := <-w.ChPool:
        jobChan <- job
    default:
        fmt.Println("All workers busy")
    }
}

func (w *W2New) AddWorker() {
    newWorker := SubWorkerNew{Id: w.MaxNum, JobChan: make(chan string)}
    w.SubWorkers = append(w.SubWorkers, newWorker)
    w.ChPool <- newWorker.JobChan
    w.MaxNum++
    w.Wg.Add(1)
    go func(sw *SubWorkerNew) {
        defer w.Wg.Done()
        for {
            select {
            case job := <-sw.JobChan:
                fmt.Printf("SubWorker %d processing job %s
", sw.Id, job)
                time.Sleep(time.Second)
            case <-w.QuitChan:
                return
            }
        }
    }(&newWorker)
}

func (w *W2New) RemoveWorker() {
    if w.MaxNum > 1 {
        worker := w.SubWorkers[w.MaxNum-1]
        close(worker.JobChan)
        w.MaxNum--
        w.SubWorkers = w.SubWorkers[:w.MaxNum]
    }
}

func (w *W2New) Stop() {
    close(w.QuitChan)
    for i := 0; i < w.MaxNum; i++ {
        close(w.SubWorkers[i].JobChan)
    }
    w.Wg.Wait()
}

Test Case

func TestW2New(t *testing.T) {
    pool := NewW2(3)
    pool.StartPool()
    pool.Dispatch("task 1")
    pool.Dispatch("task 2")
    pool.Dispatch("task 3")
    pool.AddWorker()
    pool.AddWorker()
    pool.RemoveWorker()
    pool.Stop()
}

The article demonstrates that when the number of tasks exceeds the number of workers, a worker’s JobChan receives multiple tasks and processes them sequentially, highlighting potential bottlenecks and the importance of proper pool sizing and graceful shutdown.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceconcurrencyGothread poolgoroutine poolhigh-concurrencygo-optimization
Open Source Linux
Written by

Open Source Linux

Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.