속도 제한은 클라이언트가 윈도우당 보낼 수 있는 요청 수에 상한을 두는 제어 방식이며, 스로틀링은 초과 요청을 거부하지 않고 늦추는 방식입니다. 여기에 세 번째 형제인 할당량(quota), 즉 과금 주기 단위의 허용량을 더하면, 대부분의 설명이 한 단어로 뭉뚱그리는 어휘가 모두 갖춰집니다. 이 셋이 함께 API를 공정하고, 수지가 맞고, 중단 없이 유지합니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 속도 제한(rate limit) | 짧은 윈도우당 엄격한 상한. 초과분은 429를 받음 |
| 스로틀링(throttle) | 초과분은 거부되지 않고 늦춰지거나 큐에 들어감 |
| 할당량(quota) | 장기적인 허용량. 일/월 단위, 요금제별 |
| 서버의 언어 | 429 + Retry-After + 속도 제한 헤더 |
| 클라이언트의 예절 | Retry-After 준수, 지터를 더한 지수 백오프 |
429 응답과 속도 제한 헤더의 의미
HTTP/1.1 429 Too Many Requests ← RFC 6585
Retry-After: 30 ← 이만큼 대기(초 또는 날짜)
X-RateLimit-Limit: 1000 ← 이번 윈도우의 예산
X-RateLimit-Remaining: 0 ← 남은 양
X-RateLimit-Reset: 1767024000 ← 다시 채워지는 시점
RateLimit: "default";r=0;t=30 ← 새롭게 자리 잡는 IETF 표준 형식
계약의 클라이언트 쪽 절반, 즉 모든 SDK 사용자가 결국 필요로 하는 코드입니다:
// JavaScript / Node.js — Back4app JS SDK
// The client half of rate limiting: back off, with jitter, then retry
async function withBackoff(fn, attempt = 0) {
try {
return await fn();
} catch (e) {
if (e.code !== 155 || attempt >= 4) throw e; // 155: request limit hit
const wait = 2 ** attempt * 500 + Math.random() * 200;
await new Promise((r) => setTimeout(r, wait)); // exponential + jitter
return withBackoff(fn, attempt + 1);
}
}
const results = await withBackoff(() => query.find()); // Flutter / Dart — Back4app Flutter SDK
// The client half of rate limiting: back off, with jitter, then retry
Future<ParseResponse> withBackoff(Future<ParseResponse> Function() fn,
[int attempt = 0]) async {
final response = await fn();
if (response.success || attempt >= 4) return response;
final wait = (1 << attempt) * 500 + Random().nextInt(200);
await Future.delayed(Duration(milliseconds: wait)); // exponential + jitter
return withBackoff(fn, attempt + 1);
}
final response = await withBackoff(() => query.query()); // iOS / Swift — Back4app Swift SDK
// The client half of rate limiting: back off, with jitter, then retry
func withBackoff<T>(_ attempt: Int = 0,
_ fn: @escaping () async throws -> T) async throws -> T {
do { return try await fn() }
catch {
guard attempt < 4 else { throw error }
let wait = pow(2.0, Double(attempt)) * 0.5 + .random(in: 0...0.2)
try await Task.sleep(for: .seconds(wait)) // exponential + jitter
return try await withBackoff(attempt + 1, fn)
}
}
let results = try await withBackoff { try await query.find() } // Android / Kotlin — Back4app Android SDK
// The client half of rate limiting: back off, with jitter, then retry
suspend fun <T> withBackoff(attempt: Int = 0, fn: suspend () -> T): T {
return try {
fn()
} catch (e: ParseException) {
if (attempt >= 4) throw e
val wait = (1L shl attempt) * 500 + Random.nextLong(200)
delay(wait) // exponential + jitter
withBackoff(attempt + 1, fn)
}
}
val results = withBackoff { query.find() } 토큰 버킷 vs. 리키 버킷 vs. 고정 윈도우 vs. 슬라이딩 윈도우
| 알고리즘 | 버스트 | 정확도 | 메모리 | 판정 |
|---|---|---|---|---|
| 토큰 버킷 | 버킷 크기까지 허용 | 좋음 | 매우 작음 | API의 기본값. 몰려드는 사람의 요청, 안정적인 평균 |
| 리키 버킷 | 일정한 물방울처럼 평탄화 | 좋음 | 작음 | 트래픽 셰이핑. 안정적인 출력, 지연 시간 추가 |
| 고정 윈도우 | 경계 버스트: 경계에서 제한의 2배 | 약함 | 매우 작음 | 단순하지만 악용하기 쉽기로 유명 |
| 슬라이딩 윈도우 | 통제됨 | 최고 | 보통 | 대부분의 플랫폼이 규모에 맞춰 택하는 절충안 |
다이어그램이 건너뛰는 분산 환경의 각주가 하나 있습니다. API 서버가 여러 대라면 버킷은 공유된 어딘가, 보통 원자적 증가 연산을 수행하는 인메모리 저장소에 있어야 하며, 엄격한 정확도(모든 검사가 저장소를 거침)와 속도(로컬 카운터, 느슨한 동기화) 중 하나를 선택해야 합니다. 대부분의 플랫폼은 지연 시간을 지키는 대가로 약간 느슨한 제한을 받아들이고, 계약상의 할당량에만 엄격한 처리를 적용합니다.
범위 지정: 정확히 누구를 제한할까?
IP별 제한은 익명 트래픽 폭주와 DDoS 흡수 과정에서 새어 나오는 트래픽을 막지만, 사무실 NAT 뒤에서는 수백 명의 사용자가 주소 하나가 됩니다. 키별 제한은 제한을 애플리케이션과 요금제에 대응시키며, 가장 많이 쓰이는 범위입니다. 사용자별 제한은 계정 하나가 공유 앱의 키를 독점하지 못하게 합니다. 엔드포인트별 제한은 비싼 작업에 그 비용에 걸맞은 한도를 매깁니다. 검색과 내보내기 엔드포인트는 헬스 체크보다 빠듯한 예산을 받아야 합니다. 프로덕션 시스템은 이 범위들을 겹쳐 쓰며, 종합적인 질문은 언제나 “이 요청은 어느 예산을 소비했는가?”입니다.
일반적인 사용 사례
- 공개 API 보호 – 정석적인 사례입니다. 키별로 공정한 예산을 두고, 헤더로 공개하고, 엣지에서 강제합니다.
- 요금제 등급 강제 – 무료 요금제와 유료 요금제는 정확히 할당량과 버스트 허용량에서 차이가 납니다.
- 남용과 스크래핑 방어 – 익명 요청에는 빠듯한 제한을, 인증된 요청에는 넉넉한 제한을 둡니다.
- 비싼 경로의 비용 통제 – LLM 호출, 내보내기, 검색. 실제 비용을 반영한 엔드포인트별 예산입니다.
- 클라이언트 측 자기 방어 – 다른 서비스의 제한에 맞춘 백오프와 요청 병합입니다. 차단당하지 않으려면 연동 코드가 그 제한을 지켜야 합니다.
거부할까, 스로틀링할까? 결정 매트릭스
| 속도 제한(거부)을 쓸 때 | 스로틀링(지연/큐)을 쓸 때 |
|---|---|
| 클라이언트가 지능적으로 재시도할 수 있을 때 | 작업이 결국 반드시 수행되어야 할 때 |
| 대화형 처리 용량을 보호할 때 | 배치와 백그라운드 부하를 평탄화할 때 |
| 계약이 윈도우당 요청 수일 때 | 계약이 서비스의 공정성일 때 |
| 빠른 피드백이 늦은 성공보다 나을 때 | 늦은 성공이 오류보다 나을 때 |
| 익명 또는 신뢰할 수 없는 트래픽을 상대할 때 | 자체 내부 생산자를 상대할 때 |
그리고 메타 규칙이 하나 있습니다. 무엇을 선택하든 공개해야 합니다. 문서와 헤더에 명시된 제한은 답답한 벽을, 클라이언트가 그에 맞춰 개발할 수 있는 엔지니어링 계약으로 바꿉니다.
한계와 트레이드오프
- 제한은 무딘 도구입니다. 요청 하나는 비용 단위가 아닙니다. 값싼 읽기 하나와 거대한 내보내기 하나가 똑같이 “1”을 소비합니다. 비용 인지형 제한(작업당 포인트)이 개선책이지만, 복잡성이라는 대가가 따릅니다.
- 분산 환경의 정확도는 지연 시간을 대가로 치릅니다. 엄격한 전역 카운터는 공유 저장소에서 직렬화되고, 느슨한 로컬 카운터는 경계에서 허용치보다 많이 받아들입니다. 유행이 아니라 필요한 보장 수준에 따라 선택해야 합니다.
- 429는 무고한 재시도 루프를 벌합니다. 지터가 없는 클라이언트는 서로 동기화되어 썬더링 허드가 됩니다. 제한기 설계는 최악의 클라이언트를 가정해야 합니다. 언젠가는 반드시 그런 클라이언트를 만나기 때문입니다.
- 스로틀링은 과부하를 숨깁니다. 큐에 들어간 요청은 그래프를 매끄럽게 만들면서 보이지 않는 백로그를 쌓습니다. 큐에는 상한과 부하 차단 정책이 필요하며, 그렇지 않으면 큐 자체가 장애 수준의 지연이 됩니다.
- 제한은 운영 설정처럼 보이지만 실은 제품 결정입니다. 예산, 요금제 등급, 버스트 허용량은 사용자 경험과 매출을 좌우합니다. 대시보드만 보지 말고 요금 페이지를 열어 둔 채 설정해야 합니다.
Back4app에서의 속도 제한
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 보호는 별도로 진행할 프로젝트가 아니라 플랫폼의 기본값입니다. 속도 제한은 엣지에서 앱과 요금제별로 적용되고, 비싼 작업은 자체 검사를 갖춘 Cloud Code 함수 뒤에 둘 수 있으며, 클라이언트는 의미가 명확한 429 응답을 받습니다. 위 코드 탭의 SDK 측 백오프 패턴은 이를 실패한 화면이 아니라 복원력 있는 동작으로 바꿉니다. 사용자는 정책을 조정할 뿐, 제한기를 직접 만들지 않습니다.
자주 묻는 질문
API 속도 제한이란 무엇입니까?
클라이언트가 일정한 시간 윈도우 안에 보낼 수 있는 요청 수에 상한을 두는 제어 방식입니다. 예를 들어 키당 시간당 1,000건입니다. 상한을 넘는 요청은 거부되며, 전통적으로 HTTP 429 Too Many Requests가 반환됩니다. 속도 제한은 과부하와 남용으로부터 서비스를 보호하고, 요청이 많은 클라이언트 하나가 다른 모든 사용자의 품질을 떨어뜨리지 못하게 하며, 용량 계획을 가능하게 합니다.
속도 제한, 스로틀링, 할당량의 차이는 무엇입니까?
흔히 한 단어로 뭉뚱그려지는 세 가지 도구입니다. 속도 제한은 초과 요청을 그 자리에서 거부합니다. 스로틀링은 대신 요청을 늦추거나 큐에 넣으므로, 요청은 나중에라도 결국 완료됩니다. 할당량은 장기적인 허용량으로, 요금제나 청구서에 묶인 일별 또는 월별 요청 수입니다. 짧은 윈도우는 인프라를 보호하고, 할당량은 비즈니스 조건을 정의하며, 스로틀링은 그 경계를 매끄럽게 다듬습니다.
429 오류는 무엇을 의미하며 어떻게 해결합니까?
현재 윈도우에서 허용된 요청 수를 초과했다는 뜻입니다. 해결책은 클라이언트 측의 규율입니다. Retry-After 헤더가 있으면 읽고 그만큼 기다리며, 없으면 지터를 더한 지수 백오프로 재시도합니다. 잘못된 해결책, 즉 즉각적이고 맹목적인 재시도는 자신과 다른 모든 사용자의 상황을 악화시키며, 백오프는 바로 이를 막기 위해 존재합니다.
토큰 버킷 알고리즘은 어떻게 동작합니까?
버킷에는 일정한 속도로 다시 채워지는 토큰이 담겨 있고, 요청마다 토큰을 하나씩 소비합니다. 가득 찬 버킷은 버스트를 통과시키고(버킷 크기가 곧 버스트 허용량입니다), 보충 속도는 지속적인 평균을 강제합니다. 버스트에 관대한 이 구조 때문에 토큰 버킷은 사용자 대면 API의 기본 알고리즘입니다.
고정 윈도우 방식의 속도 제한기는 왜 문제가 됩니까?
경계 버스트 때문입니다. 분당 100건 제한이라면 클라이언트는 11:59:59에 100건, 12:00:01에 다시 100건을 보낼 수 있습니다. 2초 동안 200건이며, 모두 규칙상 허용됩니다. 슬라이딩 윈도우 알고리즘은 이전 윈도우에 가중치를 두어 이 허점을 막으며, 그 대가로 기록 관리가 약간 늘어납니다. "분당 요청 수"에 "분"의 정의가 필요한 고전적인 이유입니다.
속도 제한 헤더에는 무엇이 있습니까?
먼저 관례가 있습니다. X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset은 클라이언트에게 예산, 남은 양, 다시 채워지는 시점을 알려 주며, 주요 API 제공업체들이 널리 퍼뜨렸습니다. 표준화는 IETF의 RateLimit 및 RateLimit-Policy 헤더로 진행 중입니다. 어느 쪽이든 헤더로 제한을 공개하는 것이 속도 제한을 벽에서 계약으로 바꿉니다.
지터를 더한 지수 백오프란 무엇입니까?
예의 바른 재시도 방식입니다. 연속된 실패 후 1초, 2초, 4초, 8초를 기다리고, 각 대기 시간에 무작위 값(지터)을 더합니다. 지터는 보기보다 중요합니다. 지터가 없으면 함께 실패한 모든 클라이언트가 함께 재시도해, 복구 중인 서비스를 동기화된 파도처럼 두들깁니다. 무작위성이 썬더링 허드(thundering herd)를 깨뜨립니다.
제한은 IP별, API 키별, 사용자별 중 어느 기준으로 걸어야 합니까?
여러 겹으로 걸어야 합니다. 각 범위는 단독으로는 한계가 있기 때문입니다. IP별 제한은 익명 트래픽 폭주를 막지만 주소 하나를 공유하는 사무실에 불이익을 주고, 키별 제한은 제한을 애플리케이션과 요금제에 대응시키며, 사용자별 제한은 계정 하나가 공유 앱을 독점하지 못하게 하고, 엔드포인트별 제한은 비싼 작업을 콕 집어 보호합니다. 프로덕션 시스템은 보통 최소한 키 범위와 엔드포인트 범위의 제한을 결합합니다.