- Published on
LangChain에서 OpenAI 429 폭주 막는 배치·백오프·캐시
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 LangChain으로 OpenAI 호출을 붙이다 보면, 어느 순간부터 429가 연쇄적으로 터지며 전체 파이프라인이 무너지는 경험을 하게 됩니다. 특히 RAG나 대량 요약처럼 요청 수가 급격히 늘어나는 워크로드에서는, 단일 요청 실패가 재시도 폭탄으로 번져 더 많은 429를 유발하는 악순환이 흔합니다.
이 글은 “왜 429가 폭주하는가”를 운영 관점에서 분해하고, 배치(batch), 백오프(backoff), 캐시(cache) 3축으로 LangChain 기반 애플리케이션을 안정화하는 실전 패턴을 정리합니다.
또한 검색/리트리벌이 엮인 구조라면 호출 수가 기하급수적으로 늘어날 수 있으니, RAG 튜닝 관점은 RAG 회수율 급락? 하이브리드+리랭커 튜닝도 함께 참고하면 좋습니다.
429 폭주의 본질: “요청 수”보다 “동시성·재시도·중복”
OpenAI의 429는 단순히 “많이 호출해서”가 아니라, 보통 아래 조합에서 폭주합니다.
- 동시성 폭주: 사용자 요청이 몰리거나 배치 작업이 동시에 돌아가면서 순간 QPS가 치솟음
- 재시도 증폭: 실패한 요청을 즉시/동시에 재시도하여 더 큰 트래픽을 유발
- 중복 호출: 동일 프롬프트/동일 문서에 대해 매번 새로 호출 (캐시 부재)
- 파이프라인 증식: RAG에서 쿼리 1번이
retrieval+rerank+generation등 여러 LLM 호출로 확장
따라서 해결도 “쿼터 늘리기” 이전에, (1) 동시성 상한, (2) 재시도 제어, (3) 중복 제거를 먼저 잡는 게 효과가 큽니다.
전략 1) 배치: 호출을 “모아서” 보내고 “폭”을 제한하기
배치 전략의 핵심은 두 가지입니다.
- 동시성 제한: 한 번에 날리는 요청 수를 제한해 순간 폭주를 막기
- 배치 크기 튜닝: 너무 작으면 오버헤드가 커지고, 너무 크면 실패 시 영향 반경이 커짐
비동기 동시성 제한(세마포어) + 배치 처리
아래 코드는 LangChain 호출을 세마포어로 동시성 제한하고, 입력을 청크 단위로 배치 처리하는 기본 골격입니다.
import asyncio
from typing import Iterable, List
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
async def _call_with_semaphore(sem: asyncio.Semaphore, text: str) -> str:
async with sem:
resp = await llm.ainvoke([HumanMessage(content=text)])
return resp.content
async def run_in_batches(
inputs: List[str],
batch_size: int = 20,
max_concurrency: int = 5,
) -> List[str]:
sem = asyncio.Semaphore(max_concurrency)
outputs: List[str] = []
for i in range(0, len(inputs), batch_size):
batch = inputs[i : i + batch_size]
tasks = [asyncio.create_task(_call_with_semaphore(sem, x)) for x in batch]
outputs.extend(await asyncio.gather(*tasks))
return outputs
운영에서 중요한 포인트는 batch_size와 max_concurrency를 환경 변수로 외부화하는 것입니다. 트래픽/쿼터/모델 변경에 따라 안전한 값이 달라지기 때문입니다.
배치 전략의 함정: “요청 단위”가 아니라 “토큰 단위”로 본다
429는 보통 “요청 수 제한”과 “토큰 처리량 제한”이 함께 걸립니다. 배치 크기를 늘리면 요청 수는 줄지만, 한 번에 처리하는 토큰이 커져 다른 한도를 치기 쉽습니다.
실무 팁:
- 긴 문서 요약은 문서 길이 기반으로 배치 분할
- RAG 생성은
top_k를 과하게 올리지 말고, 리랭커/필터로 입력 토큰을 줄이기
전략 2) 백오프: 재시도는 “분산”시키고 “상한”을 둔다
429가 발생했을 때 가장 위험한 패턴은 “즉시 재시도”입니다. 같은 순간에 실패한 요청들이 동시에 재시도하면, 서버는 더 큰 폭주를 맞습니다.
백오프의 목표는 다음입니다.
- 지수 백오프로 점점 더 기다리기
- **지터(jitter)**로 재시도 타이밍을 랜덤 분산
- 최대 재시도 횟수/최대 대기 시간으로 꼬리 지연을 제한
tenacity로 지수 백오프 + 지터 적용
import random
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
from openai import RateLimitError
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# wait_exponential은 (multiplier * 2^n) 형태로 증가
# 여기에 약간의 랜덤 지터를 더해 동시 재시도를 분산
def _jitter(seconds: float) -> float:
return seconds + random.uniform(0, 0.5)
@retry(
retry=retry_if_exception_type(RateLimitError),
stop=stop_after_attempt(6),
wait=wait_exponential(multiplier=1, min=1, max=20),
reraise=True,
)
async def invoke_with_backoff(text: str) -> str:
resp = await llm.ainvoke([HumanMessage(content=text)])
return resp.content
위 예시는 기본 형태이고, 실제 운영에서는 다음을 추가로 권장합니다.
429외에503, 네트워크 타임아웃도 동일 정책으로 묶기- 재시도 시 요청을 더 줄이는 폴백(예:
top_k감소, 출력 길이 축소) - 재시도 로그에
request_id, 입력 길이, 토큰 추정치 등을 남기기
재시도 폭탄 방지: “전역 레이트 리미터”를 둔다
백오프는 요청 단위로는 도움이 되지만, 시스템 전체로 보면 여전히 동시성이 높으면 폭주가 납니다. 따라서 애플리케이션 레벨에서 전역 레이트 리미터를 두는 게 좋습니다.
예를 들어 Redis 기반 토큰 버킷 또는 간단한 in-process 제한을 둘 수 있습니다(멀티 인스턴스면 Redis 권장).
import asyncio
import time
class SimpleRateLimiter:
def __init__(self, qps: float):
self.interval = 1.0 / qps
self._lock = asyncio.Lock()
self._next = 0.0
async def acquire(self):
async with self._lock:
now = time.monotonic()
if now < self._next:
await asyncio.sleep(self._next - now)
self._next = max(self._next, now) + self.interval
rate_limiter = SimpleRateLimiter(qps=3.0)
async def guarded_invoke(text: str) -> str:
await rate_limiter.acquire()
return await invoke_with_backoff(text)
이렇게 하면 “재시도 트래픽”까지 포함해 전체 QPS가 제어되므로, 429 연쇄를 끊는 데 매우 효과적입니다.
전략 3) 캐시: 중복 호출을 제거하면 429가 ‘구조적으로’ 사라진다
배치와 백오프는 “폭주를 완화”하지만, 캐시는 “호출 자체를 제거”합니다. 특히 다음 케이스에서 캐시 효율이 큽니다.
- 동일 문서/동일 질문을 여러 사용자가 반복
- 배치 작업 재실행(실패 후 재처리)
- RAG에서 문서 청크 요약/정규화 같은 전처리
LangChain LLM 캐시 적용 예시
LangChain은 LLM 호출 결과를 캐시할 수 있습니다. 운영에서는 in-memory보다 Redis 같은 외부 캐시가 안전합니다(프로세스 재시작/스케일 아웃 고려).
from langchain.globals import set_llm_cache
from langchain_community.cache import RedisCache
import os
redis_url = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
set_llm_cache(RedisCache(redis_url=redis_url))
캐시 적용 시 주의점:
- 프롬프트가 조금만 달라도 캐시 미스가 나므로, 프롬프트 템플릿/시스템 메시지 버전을 안정적으로 관리
- 개인정보/민감 데이터가 프롬프트에 들어가면 캐시 저장 정책을 별도로 설계(암호화/TTL/마스킹)
- 모델 버전이 바뀌면 캐시 키에 모델명을 포함하거나, 캐시를 분리
“의미 캐시”로 더 크게 줄이기(선택)
완전히 동일한 입력만 캐시하면 효과가 제한적일 수 있습니다. 질문이 유사한 경우까지 잡고 싶다면 임베딩 기반 의미 캐시를 고려합니다.
- 쿼리 임베딩을 만들고
- 코사인 유사도가 임계치 이상이면 기존 답을 재사용
다만 의미 캐시는 오답 재사용 리스크가 있으므로, 고객 응대/정책 답변처럼 정확성이 중요한 영역은 보수적으로 적용해야 합니다.
429를 더 줄이는 파이프라인 설계 체크리스트
1) RAG 입력 토큰을 줄여라
RAG에서 top_k를 크게 잡으면 컨텍스트가 길어지고 토큰 처리량 제한에 더 빨리 걸립니다.
- 하이브리드 검색 + 리랭커로
top_k를 낮추고도 품질 유지 - 문서 청크 길이/오버랩을 재설계
관련해서는 RAG 회수율 급락? 하이브리드+리랭커 튜닝에서 “호출 수와 토큰을 동시에 줄이는” 접근을 참고할 수 있습니다.
2) 장애를 키우는 건 오토스케일일 때가 많다
Kubernetes나 서버리스에서 오토스케일이 걸리면 인스턴스 수가 늘면서 동시 호출 총량이 증가합니다. 외부 API 쿼터는 그대로인데 애플리케이션만 스케일 아웃되면, 429가 더 자주 터질 수 있습니다.
- 워커 수/레플리카 수에 비례해 전역 레이트 리미터를 조정
- 큐 기반(예: SQS, Kafka, Redis queue)으로 흡수
GPU 서빙 오토스케일과 유사한 운영 함정이 많으니, 인프라 관점은 KServe+Knative로 GPU 모델 오토스케일 배포에서 “스케일과 트래픽 제어” 감각을 가져오면 도움이 됩니다.
3) 실패를 DB에 기록할 때도 폭주한다
429가 많이 나면 “실패 로그/재시도 상태”를 DB에 쓰는 트래픽이 늘어 DB 락 경합이 생기기도 합니다. 특히 작업 테이블에서 상태를 업데이트하는 패턴은 교착/락 대기를 만들 수 있습니다.
- 재시도 상태 업데이트를 배치로 합치기
- idempotency key로 중복 업데이트 줄이기
DB 락/교착 진단 감각은 PostgreSQL deadlock detected 진단·해결 9단계도 함께 보면 좋습니다.
운영에서 바로 쓰는 “안전한 기본값” 가이드
서비스마다 다르지만, 초기 안정화를 위한 보수적 기본값은 다음처럼 시작하는 경우가 많습니다.
max_concurrency: 2~8 (인스턴스당)batch_size: 10~50 (입력 길이 짧을 때), 긴 문서면 더 작게- 백오프: 최소 1초, 최대 20
60초, 최대 58회 재시도 - 캐시: Redis, TTL은 1시간~7일(데이터 민감도에 따라)
그리고 반드시 관측 가능성을 붙입니다.
429비율, 재시도 횟수 분포- 요청당 입력/출력 토큰 추정치
- 큐 길이(있다면), 처리 지연
결론: 429 대응은 “요청 제어”가 아니라 “시스템 설계”다
LangChain에서 OpenAI 429 폭주를 막는 가장 현실적인 해법은 다음 순서로 정리됩니다.
- 배치 + 동시성 제한으로 순간 폭을 깎고
- 지수 백오프 + 지터 + 전역 레이트 리미터로 재시도 폭탄을 막고
- **캐시(가능하면 Redis)**로 중복 호출을 구조적으로 제거
이 3가지를 적용하면, 쿼터 증설 없이도 체감 안정성이 크게 올라가고 비용도 함께 내려갑니다. 이후에야 모델 선택, RAG 튜닝, 오토스케일 정책 같은 최적화를 안전하게 진행할 수 있습니다.