tonapi.io 429 오류 해결: rate limit 제대로 없애는 법
tonapi.io는 rate limit을 넘으면 HTTP 429를 돌려줍니다. 429 오류를 없애는 방법, 「228」이 왜 커뮤니티 밈인지, TONNode MCP로 전용 키를 받는 법까지 정리했습니다.
tonapi 429: 저녁 8시 반, 프로덕션은 불타고 로그는 온통 빨간 벽
TON 위에 올린 봇이 방금 어느 추천 목록에 실렸습니다. 사용자가 몰려들었고, 애플리케이션은 갑자기 빈 잔액을 돌려주기 시작합니다. 로그를 열어보면 수백 줄이 이렇게 찍혀 있습니다:
HTTP 429 Too Many Requests
익숙한 장면인가요? 잔액과 트랜잭션 히스토리를 가져오려고 tonapi.io를 호출하고 있었고, 사용자가 열 명일 때는 전부 잘 돌아갔는데, 천 명이 되자 익명 접근이 천장에 부딪힌 겁니다. 전형적인 패턴입니다. 트래픽이 적을 때 공개 API는 공짜에 무한한 것처럼 보입니다. 부하가 올라가는 순간 병목으로 변하고, tonapi 429를 한 뭉치씩 받아내게 됩니다. 이건 여러분 코드의 버그도 아니고 「노드가 죽은」 것도 아닙니다. 공개 API의 rate limit이고, 예측 가능한 방식으로 고쳐집니다.
왜 429가 발생하는지, 그 유명한 「228」은 왜 아무 상관이 없는지, 어떤 처방이 실제로 효과가 있는지, 그리고 TONNode MCP 서버를 통해 공용 익명 풀에서 벗어나 TON 읽기용 전용 한도를 확보하는 방법까지 정리해보겠습니다.
tonapi.io가 429 Too Many Requests를 돌려주는 이유
tonapi.io는 TON 데이터에 접근하는 공개 HTTP API입니다. 모든 공개 서비스가 그렇듯 여기에도 tonapi rate limit, 즉 한 클라이언트가 전체 용량을 먹어치우지 못하도록 하는 요청 빈도 제한이 걸려 있습니다.
키 없이 호출하면 지구상의 모든 익명 사용자와 공용 익명 풀을 나눠 쓰게 됩니다. 실질적으로는 초당 요청 1회 수준의 한도입니다. 스크립트 하나라면 견딜 만합니다. 하지만 지갑마다 잔액을 조회하고, 이어서 Jetton 잔액을, 다시 히스토리를 당기는 병렬 워커가 생기는 순간 초당 한도를 즉시 넘어섭니다. 특히 콜드 스타트에서 많은 주소를 한꺼번에 인덱싱해야 할 때 뼈아픕니다.
서버는 이렇게 응답합니다:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
중요한 지점입니다. 429는 명세에 정의된 표준 HTTP 코드이지 tonapi 고유의 코드가 아닙니다. 요청 빈도를 넘기면 누구든 이 코드를 돌려줍니다 — GitHub, Stripe, Cloudflare, rate limit이 걸린 모든 서비스가 그렇습니다. toncenter도, 대부분의 RPC 제공자도 정확히 같은 메커니즘을 씁니다. 그렇다는 건 아래에서 다룰 표준적인 방법으로 치료된다는 뜻이기도 합니다.
「228 오류」는 API 코드가 아니라 커뮤니티 밈입니다 (진짜 코드는 429)
러시아어권 포럼에서 이 문제를 검색해봤다면 「228 오류」라는 표현을 거의 확실히 마주쳤을 겁니다. 입문자를 진짜로 헷갈리게 만드는 지점이니 확실히 정리하고 가겠습니다.
「228」은 tonapi의 공식 오류 코드도, toncenter의 공식 오류 코드도 아닙니다. TON 커뮤니티에서 오래 살아남아 채팅과 농담에 계속 튀어나오는 밈 숫자입니다. 한도를 넘겼다고 HTTP 상태 228이 날아오는 일은 없습니다 — 애초에 HTTP에 그런 상태 코드 자체가 존재하지 않습니다.
로그와 응답 헤더에서 실제로 보게 될 코드는 정확히 429입니다. 누군가 채팅에서 "tonapi한테 228 맞았다"라고 쓴다면, 실제로 그가 받은 건 429(아니면 그냥 타임아웃)이고 「228」은 일종의 말버릇일 뿐입니다. 로그에서는 「228」이 아니라 429를 grep하십시오 — 그래야 진짜 원인을 찾습니다. 한 줄이면 확인됩니다:
curl -s -o /dev/null -w "%{http_code}\n" https://tonapi.io/v2/blockchain/masterchain-head
# 부하 상태에서 키 없이 호출하면: 429
빠른 처방: 재시도, 백오프, 캐시, 그리고 내 키
429가 표준적인 이야기인 만큼 처방 세트도 표준적입니다. 쉬운 것부터 핵심으로 가보겠습니다.
1. Retry-After를 존중하는 지수 백오프
첫 거절을 받자마자 타이트한 루프로 엔드포인트를 두들기지 마십시오 — 상황만 악화시킵니다. 429일 때 서버는 몇 초를 기다려야 하는지 알려주는 Retry-After 헤더를 함께 보내는 경우가 많습니다. 이 값을 존중하고, 없으면 대기 시간을 지수적으로 늘리십시오.
async function fetchWithBackoff(url, opts = {}, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, opts);
if (res.status !== 429) return res;
// Retry-After를 존중하고, 없으면 대기 시간을 지수적으로 늘린다
const retryAfter = Number(res.headers.get('retry-after'));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(1000 * 2 ** attempt, 30_000); // 1s, 2s, 4s… 상한 30s
await new Promise(r => setTimeout(r, waitMs));
}
throw new Error('429가 사라지지 않음: 재시도 횟수 초과');
}
2. 동시 요청 수를 제한하십시오
429는 전체 요청량 때문이 아니라 Promise.all로 50개 요청을 한꺼번에 부채꼴로 쏟아내서 날아오는 경우가 많습니다. 동시 실행 한도(1~4)를 가진 세마포어를 앞에 세우면 스파이크가 평탄해지고, 초당 한도를 뚫지 않게 됩니다.
3. 변하지 않는 데이터는 캐시하십시오
Jetton 메타데이터(이름, 심볼, decimals), 주소 변환 결과, 오래된 트랜잭션 — 이것들은 변하지 않는 데이터입니다. 합리적인 TTL을 걸어 로컬 캐시에 넣어두고 API에 다시 묻지 마십시오. Jetton decimals 캐시 하나만 붙여도 전체 호출량이 눈에 띄게 줄어듭니다.
4. 핵심 처방 — 익명 대신 내 키
앞의 세 가지는 진통제입니다. 진짜 치료는 익명 풀에서 벗어나 자기 키를 갖는 것입니다. 전용 키를 쓰면 익명 상태의 ~1 req/s와는 자릿수가 아예 다른 한도를 받습니다. 인터넷 전체와 한도를 두고 경쟁할 일이 없어지고, 429는 평상시 운영에서 그냥 사라집니다. 이웃한 다른 제공자의 사례는 toncenter 429 해결법에서 다뤘는데, 논리는 완전히 동일합니다.
tonapi 한도로도 여전히 부족할 때
백오프, 캐시, 큐까지 전부 제대로 했다고 해봅시다. 그런데 애플리케이션이 계속 성장해서, 키를 가지고도 요금제 상한이나 「노드 위에 얹힌 HTTP 중간 계층」이라는 모델 자체에 부딪힙니다. 여기서 보통 두 가지 길을 검토하게 됩니다.
「내 라이트서버」 노선. "TON 글로벌 설정에 있는 공개 라이트서버를 그냥 가져다 ADNL로 직접 다니면 되지 않나"라는 유혹이 생깁니다. 정직하게 말씀드리면 이건 은탄환이 아닙니다. 글로벌 설정의 공개 라이트서버 역시 공용이고 한도가 걸려 있습니다 — 부하 상태에서는 not ready를 돌려주거나 ADNL 타임아웃으로 떨어지는 일이 잦고, 깊은 트랜잭션 히스토리도 보관하지 않습니다. 여기서 따로 자주 부딪히는 골칫거리가 바로 not ready인데, 이건 「liteserver not ready」 고치는 법에 정리했습니다. 즉 「공개 설정으로 갈아탄다」만으로는 한도 문제가 풀리지 않고, 때로는 더 나빠집니다.
「제공자나 인터페이스를 바꾼다」 노선. 문제는 tonapi.io라는 특정 서비스가 아니라, 여러분이 공용 자원 위에 앉아 있다는 사실입니다. HTTP API 대안 정리는 2026년 tonapi 대안에 모아뒀습니다. 그리고 AI 에이전트를 만들고 있다면 REST를 손으로 감싸는 대신 MCP를 통해 TON을 도구 세트로 붙이는 편이 낫습니다 — 그러면 블록체인 읽기는 재시도해야 하는 날것의 HTTP 요청이 아니라 도구 호출이 됩니다.
TONNode MCP: 공용 익명 풀 대신 전용 한도
TONNode(사이트 tonnode.io)는 TON용 hosted MCP 서버입니다. MCP(Model Context Protocol)는 AI 에이전트(Claude, Cursor, ChatGPT/Codex를 비롯한 모든 MCP 클라이언트)가 외부 도구를 호출하는 방식을 정한 표준입니다. 에이전트에게 https://tonapi.io/v2/...를 두들기고 429를 처리하는 법을 가르치는 대신, TON 읽기용 이름 붙은 도구 세트를 쥐여주는 겁니다.
핵심은 이것입니다. @tonnode/mcp 패키지는 HTTP 중간 계층 없이 TON의 네이티브 ADNL 프로토콜로 동작합니다. 오픈소스(MIT)이고 npm과 GitHub(tonnode/mcp)에 올라가 있습니다. tonapi 위에 덧씌운 또 하나의 REST 래퍼가 아니라, 네트워크와의 직접 대화입니다.
tonapi를 호출하던 전형적인 요청들은 읽기 도구로 일대일 대응됩니다:
- get_balance — 주소의 GRAM 잔액.
- get_jetton_balance — USDT를 비롯한 모든 Jetton 잔액. Jetton 지갑은 온체인에서 계산되므로 주소를 직접 유도할 필요가 없습니다. 이 도구 하나가 여러 요청의 조합을 어떻게 대체하는지는 TON USDT 잔액을 호출 한 번으로에 정리해뒀습니다.
- get_account_state — 계정 상태, 플래그, 마지막 트랜잭션.
- get_transactions — 트랜잭션 히스토리.
- run_get_method — 컨트랙트의 모든 읽기 전용 get 메서드.
- get_masterchain_info — 마스터체인 헤드(최신 블록).
- get_jetton_info — Jetton 메타데이터: 이름, 심볼, 발행량, 그리고 decimals(USDT는 6, 대부분의 Jetton은 9입니다. 이걸 모르면 raw 단위를 잘못 환산하게 됩니다).
- parse_address — EQ/UQ/raw 주소의 오프라인 변환과 검증. 네트워크 호출이 아예 없습니다.
네이밍에 관한 짧은 메모: GRAM은 2026년 6월에 이름이 바뀐 Toncoin입니다. 네트워크는 여전히 TON이고, 코인 이름만 바뀌었습니다.
get_balance의 잔액은 GRAM 단위입니다.
직접 작성한 HTTP가 아니라 도구 호출로 에이전트가 처리하게 될 프롬프트 예시입니다:
아래 주소의 GRAM 잔액과 USDT 잔액을 확인하고
UQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XF_wm
이 지갑의 최근 트랜잭션 5건도 보여줘.
에이전트는 알아서 get_balance를 호출하고, 이어서 get_jetton_balance(Jetton 지갑은 온체인에서 계산), 그다음 get_transactions를 부릅니다. 여러분이 직접 쓴 HTTP 요청도, Retry-After 처리도, 코드 안의 수동 백오프도 전혀 없습니다.
1분 만에 연결하기: 개발용 로컬, 또는 프로덕션용 hosted 키
두 가지 방법이 있고 둘 다 정직한 방법입니다. 다만 헷갈리지 마십시오. 키 없는 로컬 npx는 개발에 편리하지만 TON 공개 설정 위에서 돌아갑니다. 부하 상태에서 429의 통증을 실제로 없애주는 전용 한도는 hosted 키에서만 나옵니다.
방법 A — 로컬, 무료, 키 없이 (개발용)
읽기 도구 전체가 npx 한 줄로 올라옵니다 — 가입도 카드도 필요 없습니다. MCP 클라이언트 설정에 추가하십시오:
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
로컬 개발과 검증용으로는 이상적인 출발점입니다. 읽기 도구 풀세트, 설정 제로, 그 아래는 오픈소스. 다만 정직하게 단서를 답니다. 이 모드는 TON 공개 설정을 그대로 사용하므로, 여느 공개 라이트서버와 똑같은 공용 제약(not ready, 부하 시 ADNL 타임아웃)에 노출됩니다. 프로덕션 트래픽에서는 전용 한도의 대체재가 되지 못합니다 — 그건 방법 B로 가십시오.
방법 B — 자기 키가 붙은 hosted 엔드포인트 (프로덕션용)
보장된 처리량과 프로덕션 부하를 감당할 전용 한도가 필요하면, 자기 Bearer 키를 붙여 hosted 엔드포인트에 연결합니다:
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
요금제
모든 요금제에서 TONNode의 16개 도구를 전부 쓸 수 있습니다. 요금은 오직 처리량에 대해서만 냅니다:
- Hobby — 영원히 무료, 분당 60 요청. 로그인 즉시 키가 발급되고 카드는 필요 없습니다.
- Pro — 월 $29, 분당 300 요청.
- Scale — 월 $199, 분당 1200 요청.
익명 tonapi와의 차이는 분명합니다. 그쪽에서는 ~1 req/s를 인터넷 전체와 나눠 쓰지만, 여기서는 전용 상한이 생깁니다. 무료 hosted 키인 Hobby만 해도 오롯이 여러분만을 위한 분당 60 요청이고, 백오프와 씨름할 일도 예고 없이 튀어나오는 429도 없습니다. 이건 키 없는 로컬 npx와 같은 무료 경로가 아닙니다. Hobby hosted 키에는 공용 공개 풀이 아니라 자기 한도가 붙습니다.
정리
tonapi.io의 429는 버그도 아니고 신화 속의 「228」도 아니라, 정직한 신호입니다. 공용 익명 풀에 앉아 있다가 그 천장에 부딪혔다는 뜻이죠. 백오프를 곁들인 재시도, Retry-After 존중, 동시 요청 수 제한, 캐시가 급성 통증을 가라앉힙니다. 하지만 문제가 진짜로 사라지는 건 자기 용량이 생겼을 때뿐입니다. 게다가 AI 에이전트를 기반으로 만들고 있다면 MCP를 거치는 편이 더 편하기까지 합니다. TON 읽기가 HTTP 중간 계층이 아니라 네이티브 프로토콜로 흐르니까요.
무료 Hobby hosted 키(60 req/min, 카드 불필요)를 받고 429 수집을 멈추십시오 → tonnode.io/dashboard?plan=hobby
프로덕션 부하를 위한 여유가 필요하다면 Pro와 Scale 요금제를 비교해보십시오: tonnode.io/pricing.