
백엔드 시스템을 운영하다보면, 특정 시간에 갑자기 요청이나 트래픽이 많아질 때가 있는데,
이를 잘 관리하지 못하면 서버 자원이 고갈되고 성능 저하가 발생할 수 있습니다.
이러한 급격한 트래픽 증가를 대비하기 위한 대비책인 Rate Limit 패턴에 대해서 같이 알아봅시다.
1. Rate Limit
Rate Limit을 한 문장으로 정리하면 다음과 같습니다.
"일정 시간 동안 허용 되는 요청 수를 제한한다."
이는 시스템의 과부화를 막고, 특정 사용자의 공격(DDos 등)이나 무한 루프 호출 등의 서버를 보호하는 역할을 맡습니다.
Rate Limit을 구현하는 방법은 여러가지가 있지만, 가장 쓰이는 방식으로는 세 가지가 있습니다.
- 고정 윈도우(Fixed Window Counter)
- 슬라이딩 윈도우 (Sliding Window)
- 토큰 버킷 (Token Bucket)
2. 예제 코드
2-1. 고정 윈도우(Fixed Window Counter)
가장 단순한 방식이며, 고정된 시간 단위 내에서 요청 수를 카운트 합니다.
구현은 가장 간단하지만, 단위 경계지점에서 요청이 몰릴 경우, 설정된 제한보다 더 많은 트래픽이 순간적으로 유입이 될 수도 있습니다.
( 단위 경계지점 : 1초 간격이라는 가정 하에, 0.9초,1.1초와 같이 제한 단위의 경계 지점)
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
defer cancel()
// 120ms마다 신호를 주는 티커 생성 (고정 간격)
t := time.NewTicker(120 * time.Millisecond)
defer t.Stop()
for i := 1; i <= 10; i++ {
select {
case <-ctx.Done():
fmt.Println("stop:", ctx.Err())
return
case <-t.C:
fmt.Println("allow request", i, "at", time.Now().Format("15:04:05.000"))
}
}
}
2-2. 슬라이딩 윈도우 (Sliding Window)
고정 윈도우의 경계 문제를 해결하기 위해, 현재 시점을 기준으로 과거의 N초간의 기록을 동적으로 계산합니다.
정말하지만, 매번 모든 요청을 관리해야 함으로 메모리와 연산 비용이 높습니다.
구현 방법은 다음과 같습니다.
우선 요청이 들어오면 요청이 들어온 시각을 저장하고, 슬라이스나 큐에 추가합니다.
그리고 현재 시간을 기준으로 앞서 설정한 N초 전부다 더 오래된 타임스탬프들을 확인하고, 꺼내서 처리합니다.
마지막으로 개수 체크를 하는데 , 이 꺼내서 처리한 요청들을 제외한 나머지 길이가 설정 한도보다 작으면 허용하고
현재 시간을 새로운 타임스탬프로 추가합니다.
반대로 제한 한도보다 크면, 허용 범위를 제외한 요청은 거부 처리 됩니다.
package main
import (
"fmt"
"time"
)
func main() {
limit := 3
windowSize := time.Second
var requests []time.Time
// 요청 시뮬레이션
testTimes := []int{0, 100, 200, 500, 1100, 1200, 1300} // 밀리초 단위
for _, t := range testTimes {
now := time.Now().Add(time.Duration(t) * time.Millisecond)
boundary := now.Add(-windowSize)
validIdx := 0
for i, reqTime := range requests {
if reqTime.After(boundary) {
validIdx = i
break
}
validIdx = i + 1
}
requests = requests[validIdx:]
if len(requests) < limit {
requests = append(requests, now)
fmt.Printf("[%vms] ALLOW: Current window count: %d\n", t, len(requests))
} else {
fmt.Printf("[%vms] DENY : Rate limit exceeded\n", t)
}
}
}
2-3. 토큰 버킷 (Token Bucket)
버킷에 토큰을 일정 속도로 채워 넣고, 요청이 올 때마다 토큰을 꺼내 쓰는 방식 입니다.
즉 토큰이 있으면 실행, 없으면 대기 하거나 요청을 거절 합니다.
순간적으로 트래픽이 확 높아졌을 경우 유연하게 트래픽이 처리되는 것이 장점이지만 토큰 생성 시간과 버킷의 개수(크기)를 잘
조절하는 것이 중요합니다.
package main
import (
"context"
"fmt"
"time"
)
type TokenBucket struct {
tokens chan struct{}
stop chan struct{}
}
func NewTokenBucket(ratePerSec int, burst int) *TokenBucket {
if ratePerSec <= 0 {
ratePerSec = 1
}
if burst <= 0 {
burst = 1
}
tb := &TokenBucket{
tokens: make(chan struct{}, burst),
stop: make(chan struct{}),
}
for i := 0; i < burst; i++ {
tb.tokens <- struct{}{}
}
go tb.refillLoop(ratePerSec)
return tb
}
func (tb *TokenBucket) refillLoop(ratePerSec int) {
ticker := time.NewTicker(time.Second / time.Duration(ratePerSec))
defer ticker.Stop()
for {
select {
case <-tb.stop:
return
case <-ticker.C:
select {
case tb.tokens <- struct{}{}:
default:
}
}
}
}
func (tb *TokenBucket) Wait(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-tb.tokens:
return nil
}
}
func (tb *TokenBucket) Allow() bool {
select {
case <-tb.tokens:
return true
default:
return false // default 있기 때문에 계속 기다리지 않음
}
}
func (tb *TokenBucket) Stop() {
select {
case <-tb.stop:
return // 이미 닫힘
default:
close(tb.stop)
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
lim := NewTokenBucket(5, 3) // 초당 5개 리필, 최대 3개 보관
defer lim.Stop()
for i := 1; i <= 10; i++ {
if err := lim.Wait(ctx); err != nil {
fmt.Println("Stop:", err)
break
}
fmt.Printf("[%d] Allow at %s\n", i, time.Now().Format("15:04:05.000"))
}
}
3. 전체 코드
https://github.com/reochoi109/go-handbook/tree/main/patterns/ratelimit
go-handbook/patterns/ratelimit at main · reochoi109/go-handbook
A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook
github.com
4. 참고 자료
https://gobyexample.com/rate-limiting
Go by Example: Rate Limiting
Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Go elegantly supports rate limiting with goroutines, channels, and tickers. package main import ( "fmt" "time" ) func main() { First we’ll lo
gobyexample.com
https://blog.cloudflare.com/counting-things-a-lot-of-different-things/
How we built rate limiting capable of scaling to millions of domains
Back in April we announced Rate Limiting of requests for every Cloudflare customer. Being able to rate limit at the edge of the network has many advantages: it’s easier for customers to set up and operate, their origin servers are not bothered by excessi
blog.cloudflare.com
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] 서버 종료 로직 구현 (Shutdown) (0) | 2026.03.21 |
|---|---|
| [Golang] 고정 메모리 기반 Rate Limit (0) | 2026.03.21 |
| [Go] 고루틴 워커 풀(Worker Pool) (0) | 2026.03.21 |
| [Golang] flag로 서브커맨드 만들기 (0) | 2026.03.21 |
| [Golang] Context란? (0) | 2026.03.21 |