本文详细介绍了 Soroban Registry API 的速率限制政策,包括滑动窗口算法原理、匿名与授权用户的配额差异,以及 429 错误的处理机制。文章提供了 Python、TypeScript 和 Rust 的指数退避重试代码示例,并给出了缓存、连接池和请求分批等最佳实践建议。
Soroban Registry API 实现了速率限制,以确保公平使用、防止滥用,并为所有用户维持服务质量。速率限制使用基于身份的配额和滑动窗口算法。
| Tier | 限制 | 描述 |
|---|---|---|
| Anonymous(按 IP) | 100 requests/min | 没有 Authorization header 的请求 |
| Authenticated(按 token) | 1,000 requests/min | 带有 Authorization header 的请求 |
GET /api/contracts 的分页大小感知限制GET /api/contracts 除了标准读层级外,还使用分页大小感知速率限制。基础读限制假设 page size 为 50。请求更大的页面会按比例降低允许的请求速率。
使用默认 100 requests/min 读层级的示例:
请求的 limit |
有效速率限制 |
|---|---|
1-50 |
100 requests/min |
51-100 |
50 requests/min |
101-150 |
33 requests/min |
951-1000 |
5 requests/min |
该端点的分页校验也会强制执行:
limit 必须在 1 和 1000 之间(默认 50)offset 必须为非负数400 Bad RequestAPI 使用滑动窗口请求日志:
429。Retry-After 和 X-RateLimit-Reset 根据窗口中仍然存在的最早请求计算。这避免了固定窗口实现中常见的边界突增。
每个 API 响应都包含以下 header 中的速率限制信息:
| Header | 描述 | 示例 |
|---|---|---|
X-RateLimit-Limit |
当前窗口允许的最大请求数 | 100 |
X-RateLimit-Remaining |
当前窗口剩余请求数 | 73 |
X-RateLimit-Reset |
距离速率限制窗口重置还有多少秒 | 42 |
Retry-After |
(仅在 429 时) 重试前需要等待的秒数 | 42 |
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 42
Content-Type: application/json
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 42
Retry-After: 42
Content-Type: application/json
{
"error_code": "RATE_LIMITED",
"message": "请求过多。请在指定时间后重试。",
"details": {
"retry_after_seconds": 42,
"correlation_id": "550e8400-e29b-41d4-a716-446655440000"
},
"timestamp": "2026-02-24T12:34:56Z"
}
当你收到 429 Too Many Requests 响应时,实施带抖动的指数退避以避免惊群问题:
公式:wait_time = min(max_wait, base_delay * (2 ^ attempt)) + random_jitter
import time
import random
import requests
from typing import Optional
def call_api_with_retry(
url: str,
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0
) -> Optional[requests.Response]:
"""
使用指数退避重试策略调用 API。
Args:
url: API 端点 URL
max_retries: 最大重试次数
base_delay: 初始延迟(秒)
max_delay: 重试之间的最大延迟
Returns:
Response 对象;如果所有重试都耗尽则返回 None
"""
for attempt in range(max_retries + 1):
try:
response = requests.get(url)
# 主动检查速率限制 header
remaining = int(response.headers.get('X-RateLimit-Remaining', 1))
if remaining <= 5:
print(f"Warning: 当前窗口仅剩 {remaining} 次请求")
if response.status_code == 429:
# 如果可用,使用 Retry-After header
retry_after = int(response.headers.get('Retry-After', 0))
if retry_after > 0:
wait_time = retry_after
else:
# 计算带抖动的指数退避
wait_time = min(max_delay, base_delay * (2 ** attempt))
jitter = random.uniform(0, wait_time * 0.1)
wait_time += jitter
print(f"Rate limited. 在重试 {attempt + 1}/{max_retries} 前等待 {wait_time:.2f}s")
time.sleep(wait_time)
continue
# 成功或不可重试错误
response.raise_for_status()
return response
except requests.RequestException as e:
if attempt == max_retries:
print(f"Max retries exhausted: {e}")
return None
# 网络错误的指数退避
wait_time = min(max_delay, base_delay * (2 ** attempt))
jitter = random.uniform(0, wait_time * 0.1)
wait_time += jitter
print(f"Request failed: {e}. 正在 {wait_time:.2f}s 后重试...")
time.sleep(wait_time)
return None
## 使用
response = call_api_with_retry('https://registry.soroban.example/api/contracts/search?query=token')
if response:
data = response.json()
print(f"Found {len(data['contracts'])} contracts")
interface RetryConfig {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
}
interface RateLimitHeaders {
limit: number;
remaining: number;
reset: number;
}
async function callApiWithRetry(
url: string,
options: RequestInit = {},
config: RetryConfig = {}
): Promise<Response> {
const {
maxRetries = 5,
baseDelay = 1000,
maxDelay = 60000
} = config;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// 解析速率限制 header
const rateLimitHeaders: RateLimitHeaders = {
limit: parseInt(response.headers.get('X-RateLimit-Limit') || '0'),
remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '0'),
reset: parseInt(response.headers.get('X-RateLimit-Reset') || '0')
};
// 主动速率限制警告
if (rateLimitHeaders.remaining <= 5) {
console.warn(
`Rate limit warning: 剩余 ${rateLimitHeaders.remaining} 次请求。` +
`将在 ${rateLimitHeaders.reset}s 后重置`
);
}
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '0');
let waitTime: number;
if (retryAfter > 0) {
waitTime = retryAfter * 1000; // 转换为毫秒
} else {
// 带抖动的指数退避
waitTime = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
const jitter = Math.random() * waitTime * 0.1;
waitTime += jitter;
}
console.log(
`Rate limited. 在重试 ${attempt + 1}/${maxRetries} 前等待 ${(waitTime / 1000).toFixed(2)}s`
);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
// 成功或不可重试错误
return response;
} catch (error) {
if (attempt === maxRetries) {
throw new Error(`Max retries exhausted: ${error}`);
}
// 网络错误的指数退避
const waitTime = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
const jitter = Math.random() * waitTime * 0.1;
const totalWait = waitTime + jitter;
console.log(`Request failed: ${error}. 正在 ${(totalWait / 1000).toFixed(2)}s 后重试...`);
await new Promise(resolve => setTimeout(resolve, totalWait));
}
}
throw new Error('Max retries exhausted');
}
// 使用
try {
const response = await callApiWithRetry(
'https://registry.soroban.example/api/contracts/search?query=token',
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
console.log(`Found ${data.contracts.length} contracts`);
} catch (error) {
console.error('API call failed:', error);
}
use reqwest::{Client, Response, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
pub struct RetryConfig {
pub max_retries: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 5,
base_delay_ms: 1000,
max_delay_ms: 60000,
}
}
}
pub async fn call_api_with_retry(
client: &Client,
url: &str,
config: RetryConfig,
) -> Result<Response, Box<dyn std::error::Error>> {
for attempt in 0..=config.max_retries {
let response = client.get(url).send().await?;
// 解析速率限制 header
let remaining: u32 = response
.headers()
.get("x-ratelimit-remaining")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(1);
if remaining <= 5 {
tracing::warn!(
remaining,
"Rate limit warning: 剩余请求较少"
);
}
if response.status() == StatusCode::TOO_MANY_REQUESTS {
let retry_after: u64 = response
.headers()
.get("retry-after")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let wait_ms = if retry_after > 0 {
retry_after * 1000
} else {
// 带抖动的指数退避
let base_wait = config.base_delay_ms * 2u64.pow(attempt);
let wait = std::cmp::min(config.max_delay_ms, base_wait);
let jitter = (rand::random::<f64>() * wait as f64 * 0.1) as u64;
wait + jitter
};
tracing::info!(
attempt,
wait_ms,
max_retries = config.max_retries,
"Rate limited, waiting before retry"
);
sleep(Duration::from_millis(wait_ms)).await;
continue;
}
// 成功或不可重试错误
response.error_for_status_ref()?;
return Ok(response);
}
Err("Max retries exhausted".into())
}
// 使用
##[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let config = RetryConfig::default();
let response = call_api_with_retry(
&client,
"https://registry.soroban.example/api/contracts/search?query=token",
config,
).await?;
let data: serde_json::Value = response.json().await?;
println!("Found contracts: {}", data["contracts"].as_array().unwrap().len());
Ok(())
}
不要等到 429 响应才处理。对每个成功响应检查 X-RateLimit-Remaining,并在接近限制时降低速度:
remaining = int(response.headers.get('X-RateLimit-Remaining', 100))
reset_seconds = int(response.headers.get('X-RateLimit-Reset', 60))
if remaining <= 10:
# 降低速度:在请求之间增加延迟
delay = reset_seconds / remaining if remaining > 0 else reset_seconds
time.sleep(delay)
复用 HTTP 连接以减少开销:
## Python
session = requests.Session()
session.headers.update({'User-Agent': 'MyApp/1.0'})
## JavaScript/Node.js
const agent = new https.Agent({ keepAlive: true });
在可能的情况下,使用批量端点减少请求次数:
## 而不是多个请求:
GET /api/contracts/{id1}
GET /api/contracts/{id2}
GET /api/contracts/{id3}
## 使用批量端点:
POST /api/contracts/batch
{
"contract_ids": ["id1", "id2", "id3"]
}
在本地缓存 API 响应以避免重复请求:
from functools import lru_cache
import time
@lru_cache(maxsize=1000)
def get_contract(contract_id: str, cache_key: int) -> dict:
"""使用基于时间的失效机制缓存合约数据"""
response = requests.get(f'/api/contracts/{contract_id}')
return response.json()
## 使用每 5 分钟变化一次的 cache key
cache_key = int(time.time() / 300)
contract = get_contract('contract_abc123', cache_key)
对于批量操作,将请求分散到一段时间内:
import asyncio
async def process_contracts_gradually(contract_ids: list[str]):
"""以受控速率处理合约"""
for batch in chunks(contract_ids, size=10):
tasks = [fetch_contract(cid) for cid in batch]
await asyncio.gather(*tasks)
# 批次之间等待以遵守速率限制
await asyncio.sleep(6) # 10 requests per minute = 每 6 秒 1 个批次
可以使用环境变量自定义速率限制:
## 全局限制(每分钟)
RATE_LIMIT_ANON_PER_MINUTE=100 # 默认:100
## 向后兼容的回退:RATE_LIMIT_READ_PER_MINUTE
RATE_LIMIT_AUTH_PER_MINUTE=1000 # 默认:1000
## 时间窗口(秒)
RATE_LIMIT_WINDOW_SECONDS=60 # 默认:60
A: 匿名请求的速率限制是按 IP 地址。认证请求的速率限制是按 token,来自 Authorization header。
A: 你会收到一个带有 Retry-After header 的 429 Too Many Requests 响应,指示你何时可以重试。你的请求不会被处理。
A: 对于生产部署或大流量集成,请联系 registry 运营方,讨论带有自定义限制的 enterprise tier 访问。
A: 会,所有请求(成功或失败)都会计入你的速率限制,以防止通过故意构造畸形请求进行滥用。
A: 在认证已实现时,包含一个带有有效 API key 的 Authorization header。认证请求会自动获得更高的认证层级限制(1,000 req/min)。
A: WebSocket 连接目前不受支持。速率限制仅适用于 HTTP/REST API 请求。
A: 速率限制器使用滑动窗口算法,因此突发流量会在最近的 RATE_LIMIT_WINDOW_SECONDS 区间内持续平滑处理。
可能原因:
解决方案:
X-RateLimit-Remaining header可能原因:
解决方案:
解决方案:
如需提高速率限制或遇到问题:
api, rate-limiting
- 原文链接: github.com/ALIPHATICHYD/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!