Enforcing a 5‑QPS Limit on Go Jobs with Rate Limiter and Semaphore
This article explains how to restrict a third‑party API to five requests per second in Go by combining a rate limiter to pace job starts and a semaphore to cap concurrent executions, complete with a full, runnable code example and detailed implementation notes.
Requirement: a third‑party API is limited to 5 QPS, so the number of jobs that access the interface must not exceed five per second, including both running and newly started tasks.
Solution: use a rate limiter to control the frequency of job starts and a semaphore to bound the number of jobs running concurrently. The limiter guarantees that no more than five jobs begin each second; the semaphore ensures that at most five jobs are executing at any moment.
Implementation
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
"golang.org/x/time/rate"
)
func RateLimit() {
const maxJobsPerSecond = 5
const numJobs = 22
var wg sync.WaitGroup
var runningJobs int32 // currently executing jobs
var startedJobs int32 // jobs that have been started
var finishedJobs int32 // jobs that have just finished
limiter := rate.NewLimiter(rate.Every(time.Second/time.Duration(maxJobsPerSecond)), maxJobsPerSecond)
semaphore := make(chan struct{}, maxJobsPerSecond)
for i := 1; i <= numJobs; i++ {
wg.Add(1)
go func(jobID int) {
defer wg.Done()
limiter.Wait(context.Background()) // wait for limiter permission
semaphore <- struct{}{} // acquire semaphore
atomic.AddInt32(&startedJobs, 1)
atomic.AddInt32(&runningJobs, 1)
executeJob(jobID) // perform the task
atomic.AddInt32(&finishedJobs, 1)
atomic.AddInt32(&runningJobs, -1)
<-time.After(time.Second) // wait one second before releasing
<-semaphore // release semaphore
printStatus(&runningJobs, &startedJobs, &finishedJobs)
}(i)
}
wg.Wait()
fmt.Println("All jobs completed")
}Key Points
The rate.NewLimiter call controls the launch rate so that at most maxJobsPerSecond jobs start each second.
A semaphore channel limits the number of jobs that can run concurrently.
After a job finishes, the code waits one second before releasing the semaphore to prevent immediate new starts, ensuring the per‑second limit is respected even for fast‑completing tasks.
Atomic counters ( startedJobs, runningJobs, finishedJobs) safely track job states in a concurrent environment.
Supporting Functions
func executeJob(jobID int) {
startTime := time.Now()
fmt.Printf("%v Job %d started
", time.Now().Format("2006-01-02 15:04:05.000"), jobID)
rand.Seed(time.Now().UnixNano())
duration := time.Duration(rand.Intn(5000-1+1)+1) * time.Millisecond
time.Sleep(duration)
fmt.Printf("%v Job %d finished Cost:%v
", time.Now().Format("2006-01-02 15:04:05.000"), jobID, time.Since(startTime))
}
func printStatus(runningJobs, startedJobs, finishedJobs *int32) {
fmt.Printf("Current status - Running: %d, Started: %d, Finished: %d
",
atomic.LoadInt32(runningJobs),
atomic.LoadInt32(startedJobs),
atomic.LoadInt32(finishedJobs))
}This implementation ensures that even if tasks complete quickly, no more than five new tasks are launched per second, while accurately tracking running, started, and finished jobs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
