Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Use a bounded buffered channel for pending jobs and a fixed set of worker goroutines to process them. This gives you backpressure and a firm limit on concurrent work without adding a package dependency. It is an in-memory design: jobs are not preserved if the process exits, and it does not coordinate multiple application instances.

What this queue does—and what it does not

A Go channel can hold pending jobs; workers receive from that channel and call a handler. The channel’s capacity limits how many jobs can wait, while the worker count limits how many handlers run at once. This is suitable for work that may wait briefly inside one running process.

It is not a durable or distributed queue. A process crash loses pending jobs and any work that was in progress. Multiple instances have separate channels and do not share queue state. If you need persisted job state, crash recovery, or coordination across instances, use a broker or durable store with explicit recovery and state-transition logic. The Redis Go job-queue example illustrates pending and processing states, completion and failure paths, recovery after worker failure, and retry and idempotency considerations.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the queue’s behavior before writing it

  • When the queue is full: block the caller, reject immediately, or let the caller’s context cancel the enqueue. Blocking is the simplest direct backpressure policy.
  • How much work can run: choose a fixed worker count. A worker pool bounds simultaneous handler calls; starting a new goroutine for every job does not.
  • What happens to errors: return them to the caller, record them, retry within a limit, or route failed work elsewhere. Do not retry immediately without a bound.
  • How shutdown works: decide whether to drain queued jobs or cancel processing, and stop producers before closing the queue.
  • Whether jobs must survive a crash: if yes, an in-memory channel is not sufficient.

Implement a bounded queue and fixed worker pool

This minimal example uses a string job payload and a handler function. The queue blocks submissions when its buffer is full. Closing it stops workers after they finish jobs already in the buffer. The queue owner must ensure no goroutine can still submit when shutdown begins.

package main

import (
    "context"
    "errors"
    "fmt"
    "sync"
)

type Job struct {
    ID string
}

type Queue struct {
    jobs   chan Job
    handle func(context.Context, Job) error
    workers sync.WaitGroup
}

func NewQueue(workerCount, capacity int, handle func(context.Context, Job) error) (*Queue, error) {
    if workerCount <= 0 {
        return nil, errors.New("worker count must be positive")
    }
    if capacity <= 0 {
        return nil, errors.New("capacity must be positive")
    }
    if handle == nil {
        return nil, errors.New("handler must not be nil")
    }

    q := &Queue{
        jobs:   make(chan Job, capacity),
        handle: handle,
    }
    for i := 0; i < workerCount; i++ {
        q.workers.Add(1)
        go func() {
            defer q.workers.Done()
            for job := range q.jobs {
                if err := q.handle(context.Background(), job); err != nil {
                    // Replace with application logging or error reporting.
                    fmt.Printf("job %s failed: %vn", job.ID, err)
                }
            }
        }()
    }
    return q, nil
}

func (q *Queue) Submit(job Job) {
    q.jobs <- job
}

// CloseAndWait must be called only after all Submit calls have stopped.
func (q *Queue) CloseAndWait() {
    close(q.jobs)
    q.workers.Wait()
}

In this example, handler errors are logged and then discarded; there is no retry, result returned to the submitter, or persistent record. Adapt that policy to the application rather than treating the sample error path as a complete failure strategy. The context passed to the handler is a background context, so this version does not cancel running work during shutdown.

Submit jobs and shut down in the right order

  1. Create the queue with a positive worker count and buffer capacity. Select values based on the application’s workload and acceptable waiting behavior; this example does not prescribe performance numbers.
  2. Submit jobs while producers are active. A send to the buffered channel waits when the buffer is full until a worker receives a job.
  3. During shutdown, stop or join all producers so none can call Submit again.
  4. Call CloseAndWait. Workers ranging over the closed channel finish buffered jobs, then exit; the wait group lets shutdown wait for them.

Only the queue owner should close the channel. Sending to a closed channel panics, so closing it while producers can still submit is unsafe.

Make the full-queue policy explicit

The sample’s blocking Submit is often useful because it pushes pressure back to callers instead of allowing an unbounded backlog. But a blocked caller may need a timeout or cancellation path. For a context-aware send, expose an enqueue method that selects between the channel send and ctx.Done(); for immediate rejection, use a non-blocking send with a default case and return a full-queue error. Choose one contract and make callers handle it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capacity counts waiting jobs, not jobs already assigned to workers. With a capacity of N and W workers, the queue can hold up to N pending jobs while up to W jobs are being handled. A larger buffer absorbs more bursts but also permits more work to wait in memory.

Handle failures and duplicate execution deliberately

An error from a handler does not automatically put a job back in the channel. Decide whether the caller needs the outcome, whether the application should record the failure, and whether a retry is safe. If you retry, use a limit and an intentional delay or backoff rather than an unbounded immediate loop.

A handler may perform an external side effect and then fail before the queue can record success. Retrying can therefore perform that side effect again. Make such operations idempotent where possible, or use an appropriate idempotency key. Redis’s queue guidance also highlights idempotency and retry counts for non-idempotent actions.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose drain or cancellation at shutdown

The sample drains: closing the jobs channel lets workers complete every buffered job before returning. If a handler hangs, the wait can also hang. To support cancellation, pass a context to the handler and define whether shutdown cancels active work, abandons pending jobs, or waits for a grace period. Cancellation alone does not make queued work durable; any abandoned in-memory jobs disappear with the process.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep channel ownership simple: the component responsible for accepting submissions should coordinate producer shutdown, close the channel once, and wait for workers. Go’s official concurrency guidance expresses the underlying design principle as “Do not communicate by sharing memory; instead, share memory by communicating.” See Effective Go: Concurrency.

When to move beyond an in-process queue

Use the channel pattern when work only needs to be deferred within one process and losing it on process termination is acceptable. Move to a durable queue when jobs must survive restarts, be recovered after worker failure, be shared among instances, or have controlled at-least-once delivery. Those guarantees require persisted job state and recovery logic, not just a different channel capacity.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.