1. 라이브러리
  2. HTTP와 웹
  3. HTTP 상태 코드

업데이트됨 1개월 전

429 Too Many Requests 오류는 인터넷이 "나도 무한정 나눠줄 수는 없어"라고 말하는 방식입니다.

429가 발생했을 때, 서버는 여러분의 요청을 완벽히 이해했습니다. 단지 너무 짧은 시간에 너무 많은 요청을 보냈기 때문에 처리를 거부한 것입니다. 이것은 버그도 아니고 잘못된 설정도 아닙니다—경계선입니다. 서버는 공유 자원이고, 제한이 없다면 욕심 많은 클라이언트 하나가 모두의 경험을 망칠 수 있기 때문에 속도 제한이 존재합니다.

은행 번호표 창구를 생각해 보세요. 번호표를 뽑고 차례를 기다립니다. 누군가가 분당 100개의 업무를 처리하려 한다면, 결국 줄에 서 있는 다른 분들을 위해 "천천히 이용해 주세요"라는 말을 듣게 될 것입니다.

429 응답은 어떻게 생겼나요?

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1729512000

{
    "error": "Too Many Requests",
    "message": "Rate limit exceeded. Please retry after 60 seconds."
}

핵심 헤더는 Retry-After입니다. 언제 다시 시도할 수 있는지 정확히 알려줍니다. 잘 동작하는 클라이언트는 이것을 존중하지만, 무시하는 클라이언트는 완전히 차단될 위험이 있습니다.

현재 상태를 알려주는 헤더들

좋은 API는 무작정 거부하는 것이 아니라—왜 거부되었는지, 언제 다시 받아들여질지 알려줍니다:

헤더의미
X-RateLimit-Limit할당량 (예: 분당 100회 요청)
X-RateLimit-Remaining남은 요청 횟수
X-RateLimit-Reset할당량이 초기화되는 Unix 타임스탬프
Retry-After재시도까지 남은 시간 (초 또는 날짜)

스마트한 클라이언트는 이 헤더들을 미리 추적합니다. 미리 알 수 있는데 굳이 429를 기다릴 필요가 있을까요?

속도 제한을 받는 이유

요청을 너무 빠르게 보내고 있습니다. 대부분의 속도 제한은 "Y 시간당 X회 요청"입니다. 이를 초과하면 주기가 초기화될 때까지 이후 모든 요청이 거부됩니다.

버스트 요청을 하고 있습니다. 일부 API는 별도의 버스트 제한을 가집니다—전체적으로는 분당 100회지만 1초 내에는 10회 이하여야 합니다. 이는 한꺼번에 몰리는 것을 방지합니다.

동시 요청이 너무 많습니다. 일부 서비스는 동시에 처리 중인 요청 수를 제한합니다. 제한이 5개인데 6개의 병렬 요청을 보내면? 여섯 번째 요청은 429를 받습니다.

코드에서 429 처리하기

잘못된 방법:

// 속도 제한을 완전히 무시합니다
const response = await fetch('/api/data');

올바른 방법:

async function fetchWithRetry(url, maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url);
        
        if (response.status !== 429) {
            return response;
        }
        
        // 서버의 안내를 존중합니다
        const retryAfter = response.headers.get('Retry-After');
        const waitMs = retryAfter 
            ? parseInt(retryAfter) * 1000 
            : Math.pow(2, attempt) * 1000; // 지수 백오프 폴백
        
        await new Promise(resolve => setTimeout(resolve, waitMs));
    }
    
    throw new Error('Rate limit exceeded after retries');
}

한 단계 더 나아가—제한에 아예 걸리지 않도록 미리 추적하는 방법도 있습니다:

class ApiClient {
    constructor() {
        this.remaining = Infinity;
        this.resetTime = 0;
    }
    
    async fetch(url) {
        // 할당량이 소진된 것을 알고 있다면 대기합니다
        if (this.remaining === 0) {
            const waitMs = (this.resetTime - Date.now() / 1000) * 1000;
            if (waitMs > 0) await new Promise(r => setTimeout(r, waitMs));
        }
        
        const response = await fetch(url);
        
        // 제한 정보를 업데이트합니다
        this.remaining = parseInt(response.headers.get('X-RateLimit-Remaining') ?? Infinity);
        this.resetTime = parseInt(response.headers.get('X-RateLimit-Reset') ?? 0);
        
        return response;
    }
}

서버에서 속도 제한 구현하기

Express로 구현하는 가장 간단한 방법:

const rateLimit = require('express-rate-limit');

app.use('/api/', rateLimit({
    windowMs: 60 * 1000,  // 1분
    max: 100,              // 분당 100회 요청
    standardHeaders: true, // 헤더에 속도 제한 정보 반환
    handler: (request, response) => {
        response.status(429).json({
            error: 'Too Many Requests',
            retryAfter: Math.ceil((request.rateLimit.resetTime - Date.now()) / 1000)
        });
    }
}));

프로덕션 시스템에서는 Redis를 사용해 서버 재시작 후에도 제한이 유지되고 여러 인스턴스에 걸쳐 작동하도록 하세요:

const Redis = require('ioredis');
const redis = new Redis();

async function rateLimit(request, response, next) {
    const key = `ratelimit:${request.ip}`;
    const limit = 100;
    const windowSeconds = 60;
    
    const current = await redis.incr(key);
    if (current === 1) await redis.expire(key, windowSeconds);
    
    const ttl = await redis.ttl(key);
    
    response.setHeader('X-RateLimit-Limit', limit);
    response.setHeader('X-RateLimit-Remaining', Math.max(0, limit - current));
    response.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + ttl);
    
    if (current > limit) {
        response.setHeader('Retry-After', ttl);
        return response.status(429).json({ error: 'Too Many Requests' });
    }
    
    next();
}

사용자마다 다른 제한

모든 사용자가 동등하지는 않습니다. 유료 고객은 익명 스크래퍼보다 더 많은 용량을 받을 자격이 있습니다:

const limits = {
    anonymous: { requests: 10, window: 3600 },    // 시간당 10회
    free: { requests: 100, window: 3600 },        // 시간당 100회  
    premium: { requests: 1000, window: 3600 },    // 시간당 1,000회
    enterprise: { requests: 10000, window: 3600 } // 시간당 10,000회
};

이러한 제한을 명확하게 문서화하세요. 예상치 못한 상황은 답답합니다. 사용자는 제한에 도달하기 전에 자신의 할당량을 알고 있어야 합니다.

속도 제한 전략

고정 윈도우: 달력상의 분 단위로 요청을 계산합니다 (00:00-00:59, 01:00-01:59). 단순하지만 경계에서 버스트를 허용합니다—클라이언트가 00:59에 100개, 01:00에 100개를 요청할 수 있습니다.

슬라이딩 윈도우: 항상 최근 60초 내의 요청을 계산합니다. 더 균일하지만 구현이 복잡합니다.

토큰 버킷: 토큰이 일정 속도로 쌓이며, 각 요청은 토큰 하나를 소비합니다. 전체 제한을 유지하면서 제어된 버스트를 허용합니다.

리키 버킷: 요청이 대기열에 쌓이고 일정 속도로 처리됩니다. 가장 균일한 트래픽이지만 지연 시간이 추가됩니다.

대부분의 API에서는 슬라이딩 윈도우나 토큰 버킷이 공정성과 구현 복잡성 사이의 최적 균형을 제공합니다.

429 Too Many Requests 자주 묻는 질문

API를 거의 사용하지 않는데 왜 429 오류가 발생하나요?

다른 사용자와 속도 제한을 공유하고 있을 수 있습니다. 많은 API가 IP 주소별로 제한하므로, 기업 NAT나 공유 프록시 뒤에 있다면 같은 IP의 모든 사람과 경쟁하는 셈입니다. 요청에 인증을 추가하세요—대부분의 API는 인증된 사용자에게 개인 할당량을 부여합니다.

429가 발생했을 때 즉시 재시도해야 하나요?

아니요. 그것이 최악의 선택입니다. 서버가 방금 속도를 줄이라고 했는데 재시도를 계속 보내면 완전히 차단될 가능성이 높습니다. 항상 Retry-After 헤더에 지정된 시간 이상 기다리세요. 헤더가 없으면 지수 백오프를 사용하세요.

429와 503의 차이점은 무엇인가요?

429는 여러분이 너무 많은 요청을 보내고 있다는 뜻입니다. 503은 서버가 과부하 상태로 지금 당장 누구의 요청도 처리할 수 없다는 뜻입니다. 둘 다 나중에 재시도하라는 신호지만, 429는 여러분 자신의 문제입니다—속도를 줄여야 합니다. 503은 모두가 기다려야 한다는 뜻입니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃
429 Too Many Requests • 라이브러리 • Connected