关于 marketplace-rate-limiting
marketplace-rate-limiting 是指导 VTEX API 集成实现限流与韧性处理的技能,适用于目录、订单、价格、库存和履约操作。它涵盖退避重试、限流响应头监控、熔断器和请求队列。
主要功能
- 01对 429 响应采用带抖动的指数退避
- 02读取并遵循 Retry-After 响应头
- 03监控限流响应头以提前降速
- 04使用熔断器应对持续故障
- 05通过请求队列控制吞吐量
适用场景
- 01减少目录同步中的限流错误
- 02控制订单处理集成的请求速率
- 03可靠地执行价格与库存更新
- 04在履约 API 故障时限制连锁影响
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill marketplace-rate-limiting安装后在 Claude Code 中输入/marketplace-rate-limiting确认已加载。
详细介绍
API 速率限制与弹性
适用场景
在构建任何会调用 VTEX API 的集成时,请使用此技能,例如目录同步、订单处理、价格/库存更新或履约操作,并且需要在妥善处理速率限制的同时,避免数据丢失或性能下降。
- 使用指数退避和抖动实现重试逻辑
- 读取并响应 VTEX 速率限制标头(
Retry-After、X-RateLimit-Remaining、X-RateLimit-Reset) - 为高吞吐量集成构建熔断器
- 使用队列控制请求吞吐量
以下情况不要使用此技能:
- 目录专属同步逻辑(参见
marketplace-catalog-sync) - 订单事件的消费与处理(参见
marketplace-order-hook) - 发票和物流跟踪信息提交(参见
marketplace-fulfillment)
决策规则
- 收到 429 响应时,始终实施带抖动的指数退避。公式:
delay = min(maxDelay, baseDelay * 2^attempt) * (0.5 + random(0, 0.5))。 - 收到 429 响应时,始终读取
Retry-After标头。采用Retry-After值与计算所得退避延迟中的较大值。 - 服务持续发生故障时(例如连续失败 5 次后),使用熔断器,以防止故障级联并为服务争取恢复时间。
- 使用请求队列控制吞吐量,避免触发速率限制的突发流量。
- 在成功响应中主动监控
X-RateLimit-Remaining,并在触发 429 之前降低请求速率。 - VTEX 的速率限制因 API 而异:
- Pricing API:PUT/POST:每个账户每秒 40 个请求,突发额度为 1000。DELETE:每个账户每秒 16 个请求,突发额度为 300。
- Catalog API:因端点而异;没有公布固定限制。
- Orders API:受平台的一般限制约束;VTEX 建议在收到 429 后退避 1 分钟。
- 突发额度:超出速率限制时,超出的请求会消耗突发额度(每个超额请求消耗 1 个额度)。突发额度降至 0 时,请求会被 429 阻止。当路由未被使用时,额度会按照该路由的限制速率随时间补充。
速率限制响应标头:
| 标头 | 描述 |
|---|---|
Retry-After | 重试前需等待的秒数(在 429 响应中出现) |
X-RateLimit-Remaining | 当前窗口中剩余的请求数 |
X-RateLimit-Reset | 速率限制窗口重置时的时间戳(秒) |
架构/数据流:
Your Integration VTEX API
│ │
│── Request ──────────────────────────▶│
│◀── 200 OK ─────────────────────────│ (success)
│ │
│── Request ──────────────────────────▶│
│◀── 429 + Retry-After: 30 ──────────│ (rate limited)
│ │
│ [Wait: max(Retry-After, backoff)] │
│ [backoff = base * 2^attempt * jitter]│
│ │
│── Retry ───────────────────────────▶│
│◀── 200 OK ─────────────────────────│ (success)
硬性约束
约束:收到 429 响应时实施指数退避
收到 429 响应时,集成必须使用带抖动的指数退避,在重试前等待。存在 Retry-After 标头时,等待时间必须遵循该标头的值。
重要性
收到 429 后立即重试会再次被拒绝,并更快地消耗突发额度,从而导致长时间无法请求。如果没有抖动,所有客户端都会在窗口重置后同时重试,造成又一次速率限制峰值(惊群效应)。
检测
如果发现收到 429 后立即重试(没有延迟或退避),→ 停止并实施指数退避。如果发现重试逻辑没有读取 Retry-After 标头,→ 提醒应遵循该标头。如果发现 while(true) 重试循环,或间隔小于 5 秒的 setInterval,→ 提醒存在紧密循环问题。
正确
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 60000,
};
/**
* Calculates exponential backoff delay with full jitter.
*
* Formula: min(maxDelay, baseDelay * 2^attempt) * random(0.5, 1.0)
*
* The jitter prevents thundering herd when multiple clients
* are rate-limited simultaneously.
*/
function calculateBackoffWithJitter(
attempt: number,
baseDelayMs: number,
maxDelayMs: number
): number {
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
const boundedDelay = Math.min(maxDelayMs, exponentialDelay);
// Full jitter: random value between 50% and 100% of the bounded delay
const jitter = 0.5 + Math.random() * 0.5;
return Math.floor(boundedDelay * jitter);
}
/**
* Executes an API request with automatic retry on 429 responses.
* Respects the Retry-After header and applies exponential backoff with jitter.
*/
async function requestWithRetry<T>(
client: AxiosInstance,
config: AxiosRequestConfig,
retryConfig: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<AxiosResponse<T>> {
let lastError: AxiosError | undefined;
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
try {
return await client.request<T>(config);
} catch (error: unknown) {
if (!axios.isAxiosError(error)) {
throw error;
}
lastError = error;
const status = error.response?.status;
// Only retry on 429 (rate limited) and 503 (circuit breaker)
if (status !== 429 && status !== 503) {
throw error;
}
if (attempt === retryConfig.maxRetries) {
break; // Exhausted retries
}
// Respect Retry-After header if present (value is in seconds)
const retryAfterHeader = error.response?.headers?.["retry-after"];
const retryAfterMs = retryAfterHeader
? parseInt(retryAfterHeader, 10) * 1000
: 0;
// Use the greater of Retry-After or calculated backoff
const backoffMs = calculateBackoffWithJitter(
attempt,
retryConfig.baseDelayMs,
retryConfig.maxDelayMs
);
const delayMs = Math.max(retryAfterMs, backoffMs);
console.warn(
`Rate limited (${status}). Retry ${attempt + 1}/${retryConfig.maxRetries} ` +
`in ${delayMs}ms (Retry-After: ${retryAfterHeader ?? "none"}, ` +
`backoff: ${backoffMs}ms)`
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
throw lastError ?? new Error("Request failed after all retries");
}
错误
// WRONG: Immediate retry without backoff or Retry-After respect
async function retryImmediately<T>(
client: AxiosInstance,
config: AxiosRequestConfig,
maxRetries: number = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await client.request<T>(config);
return response.data;
} catch (error: unknown) {
// Retries immediately — will hit 429 again and drain burst credits
// Does not read Retry-After header — ignores server guidance
console.log(`Retry ${i + 1}...`);
// No delay at all — thundering herd when multiple instances retry
}
}
throw new Error("Failed after retries");
}
约束:遵守 Retry-After 标头
当 429 响应包含 Retry-After 标头时,集成 MUST 至少等待指定的秒数后再重试。退避延迟应取计算出的退避时间与 Retry-After 值中的较大者。
为何重要
Retry-After 标头是服务端明确告知何时会再次接受请求的指示。忽略它会导致请求在指定时间过去之前一直被拒绝,浪费带宽,还可能延长封锁时间。
检测
如果发现重试逻辑未读取或使用 Retry-After 标头的值 → 提醒应检查此标头。如果重试延迟始终是固定值,不受标头影响 → 提醒。
正确
function getRetryDelayMs(error: AxiosError, attempt: number): number {
const retryAfterHeader = error.response?.headers?.["retry-after"];
// Parse Retry-After (could be seconds or HTTP-date)
let retryAfterMs = 0;
if (retryAfterHeader) {
const seconds = parseInt(retryAfterHeader, 10);
if (!isNaN(seconds)) {
retryAfterMs = seconds * 1000;
} else {
// HTTP-date format
const retryDate = new Date(retryAfterHeader).getTime();
retryAfterMs = Math.max(0, retryDate - Date.now());
}
}
// Calculate backoff with jitter
const backoffMs = calculateBackoffWithJitter(attempt, 1000, 60000);
// Use the larger value — respect server guidance
return Math.max(retryAfterMs, backoffMs);
}
错误
// WRONG: Fixed 1-second retry ignoring Retry-After header
async function fixedRetry<T>(
client: AxiosInstance,
config: AxiosRequestConfig
): Promise<T> {
try {
const response = await client.request<T>(config);
return response.data;
} catch {
// Always waits 1 second regardless of Retry-After header
// If Retry-After says 60 seconds, this will fail again and again
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await client.request<T>(config);
return response.data;
}
}
约束:避免紧密重试循环
集成 MUST NOT 使用 while(true) 循环进行重试,也不得使用间隔小于 5 秒的 setInterval/setTimeout 轮询 VTEX API。
为何重要
紧密循环会产生过多请求,迅速耗尽速率限制额度,降低所有用户的 VTEX 平台性能,还可能导致该账户的 VTEX Admin 无法使用。VTEX 明确警告,过多的 429 错误可能导致 Admin 无法使用。
检测
如果发现没有足够延迟的 while(true) 或 for(;;) 重试模式 → 提醒存在紧密循环。如果发现用于 API 调用的 setInterval 间隔小于 5000ms → 提醒轮询频率过高。
正确
// Correct: Controlled polling with adequate intervals
async function pollWithBackpressure(
client: AxiosInstance,
intervalMs: number = 30000 // 30 seconds minimum
): Promise<void> {
const poll = async (): Promise<void> => {
try {
const response = await client.get("/api/orders/feed");
const events = response.data;
if (events.length > 0) {
await processEvents(events);
await commitEvents(
client,
events.map((e: { handle: string }) => e.handle)
);
}
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.status === 429) {
const retryAfter = parseInt(
error.response.headers["retry-after"] || "60",
10
);
console.warn(`Rate limited, waiting ${retryAfter}s`);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
return;
}
console.error("Polling error:", error);
}
// Schedule next poll
setTimeout(poll, intervalMs);
};
// Start polling
await poll();
}
async function processEvents(events: unknown[]): Promise<void> {
console.log(`Processing ${events.length} events`);
}
async function commitEvents(client: AxiosInstance, handles: string[]): Promise<void> {
await client.post("/api/orders/feed", { handles });
}
错误
// WRONG: Tight loop with no backpressure
async function tightLoop(client: AxiosInstance): Promise<void> {
while (true) {
try {
const response = await client.get("/api/orders/feed");
await processEvents(response.data);
} catch {
// Immediate retry — no delay, burns through rate limits
continue;
}
}
}
// WRONG: setInterval with 1-second polling
setInterval(async () => {
// 1 request/second = 3600/hour — will trigger rate limits quickly
const client = createClient();
await client.get("/api/catalog_system/pvt/sku/stockkeepingunitids");
}, 1000);
function createClient(): AxiosInstance {
return axios.create({ baseURL: "https://account.vtexcommercestable.com.br" });
}
首选模式
创建支持速率限制的 HTTP 客户端
为 HTTP 客户端封装自动重试逻辑。
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
interface RateLimitedClientConfig {
accountName: string;
appKey: string;
appToken: string;
maxRetries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
}
function createRateLimitedClient(config: RateLimitedClientConfig): {
client: AxiosInstance;
request: <T>(requestConfig: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
} {
const client = axios.create({
baseURL: `https://${config.accountName}.vtexcommercestable.com.br`,
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": config.appKey,
"X-VTEX-API-AppToken": config.appToken,
},
timeout: 30000,
});
const retryConfig: RetryConfig = {
maxRetries: config.maxRetries ?? 5,
baseDelayMs: config.baseDelayMs ?? 1000,
maxDelayMs: config.maxDelayMs ?? 60000,
};
return {
client,
request: <T>(requestConfig: AxiosRequestConfig) =>
requestWithRetry<T>(client, requestConfig, retryConfig),
};
}
实现熔断器
在服务持续故障时,防止故障级联。
enum CircuitState {
CLOSED = "CLOSED", // Normal operation — requests flow through
OPEN = "OPEN", // Service failing — requests blocked
HALF_OPEN = "HALF_OPEN", // Testing recovery — one request allowed
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount: number = 0;
private lastFailureTime: number = 0;
private successCount: number = 0;
constructor(
private readonly failureThreshold: number = 5,
private readonly recoveryTimeMs: number = 30000,
private readonly halfOpenSuccessThreshold: number = 3
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime < this.recoveryTimeMs) {
throw new Error(
`Circuit breaker is OPEN. Retry after ${this.recoveryTimeMs}ms.`
);
}
// Transition to half-open for a test request
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenSuccessThreshold) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
console.log("Circuit breaker: CLOSED (recovered)");
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
console.warn(
`Circuit breaker: OPEN after ${this.failureCount} failures`
);
}
}
getState(): CircuitState {
return this.state;
}
}
实现请求队列
通过请求排队来控制吞吐量并避免突发流量。
interface QueuedRequest<T> {
config: AxiosRequestConfig;
resolve: (value: AxiosResponse<T>) => void;
reject: (error: Error) => void;
}
class RequestQueue {
private queue: Array<QueuedRequest<unknown>> = [];
private processing: boolean = false;
private readonly requestsPerSecond: number;
private readonly circuitBreaker: CircuitBreaker;
constructor(
private readonly client: {
request: <T>(config: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
},
requestsPerSecond: number = 10,
circuitBreaker?: CircuitBreaker
) {
this.requestsPerSecond = requestsPerSecond;
this.circuitBreaker = circuitBreaker ?? new CircuitBreaker();
}
async enqueue<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return new Promise<AxiosResponse<T>>((resolve, reject) => {
this.queue.push({
config,
resolve: resolve as (value: AxiosResponse<unknown>) => void,
reject,
});
this.processQueue();
});
}
private async processQueue(): Promise<void> {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
const delayBetweenRequests = 1000 / this.requestsPerSecond;
while (this.queue.length > 0) {
const request = this.queue.shift()!;
try {
const result = await this.circuitBreaker.execute(() =>
this.client.request(request.config)
);
request.resolve(result);
} catch (error) {
request.reject(error instanceof Error ? error : new Error(String(error)));
}
// Throttle between requests
if (this.queue.length > 0) {
await new Promise((resolve) =>
setTimeout(resolve, delayBetweenRequests)
);
}
}
this.processing = false;
}
getQueueLength(): number {
return this.queue.length;
}
}
主动监控速率限制标头
读取速率限制标头,在触发 429 之前降低请求速率。
import { AxiosResponse } from "axios";
interface RateLimitInfo {
remaining: number | null;
resetAt: number | null;
retryAfter: number | null;
}
function parseRateLimitHeaders(response: AxiosResponse): RateLimitInfo {
return {
remaining: response.headers["x-ratelimit-remaining"]
? parseInt(response.headers["x-ratelimit-remaining"], 10)
: null,
resetAt: response.headers["x-ratelimit-reset"]
? parseInt(response.headers["x-ratelimit-reset"], 10) * 1000
: null,
retryAfter: response.headers["retry-after"]
? parseInt(response.headers["retry-after"], 10) * 1000
: null,
};
}
async function adaptiveRequest<T>(
client: AxiosInstance,
config: AxiosRequestConfig,
queue: RequestQueue
): Promise<AxiosResponse<T>> {
const response = await queue.enqueue<T>(config);
const rateInfo = parseRateLimitHeaders(response);
// Proactively slow down when remaining requests are low
if (rateInfo.remaining !== null && rateInfo.remaining < 10) {
console.warn(
`Rate limit approaching: ${rateInfo.remaining} requests remaining. ` +
`Slowing down.`
);
// Add extra delay to reduce pressure
await new Promise((resolve) => setTimeout(resolve, 2000));
}
return response;
}
完整示例
import axios from "axios";
async function buildResilientIntegration(): Promise<void> {
const { client, request } = createRateLimitedClient({
accountName: process.env.VTEX_ACCOUNT_NAME!,
appKey: process.env.VTEX_APP_KEY!,
appToken: process.env.VTEX_APP_TOKEN!,
maxRetries: 5,
baseDelayMs: 1000,
maxDelayMs: 60000,
});
const circuitBreaker = new CircuitBreaker(
5, // Open after 5 failures
30000, // Wait 30s before testing recovery
3 // Close after 3 successful half-open requests
);
const queue = new RequestQueue({ request }, 10, circuitBreaker);
// Example: Batch update prices with rate limiting
const skuIds = ["sku-1", "sku-2", "sku-3", "sku-4", "sku-5"];
for (const skuId of skuIds) {
try {
const response = await queue.enqueue({
method: "POST",
url: `/notificator/seller01/changenotification/${skuId}/price`,
});
const rateInfo = parseRateLimitHeaders(response);
if (rateInfo.remaining !== null && rateInfo.remaining < 5) {
console.warn("Approaching rate limit, adding delay");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
} catch (error) {
if (error instanceof Error && error.message.includes("Circuit breaker is OPEN")) {
console.error("Circuit breaker open — pausing all requests");
await new Promise((resolve) => setTimeout(resolve, 30000));
} else {
console.error(`Failed to update price for ${skuId}:`, error);
}
}
}
}
常见故障模式
- 重试延迟固定且没有抖动。 使用固定延迟(例如始终等待 5 秒),而不是带抖动的指数退避,会导致“惊群效应”:所有受到速率限制的客户端同时重试,形成新的请求突发,再次触发速率限制。使用带随机抖动的指数退避,让重试请求分散在一段时间内。
// Correct: Exponential backoff with jitter
function getRetryDelay(attempt: number): number {
const baseDelay = 1000;
const maxDelay = 60000;
const exponential = baseDelay * Math.pow(2, attempt);
const bounded = Math.min(maxDelay, exponential);
const jitter = 0.5 + Math.random() * 0.5;
return Math.floor(bounded * jitter);
}
// attempt 0: ~500-1000ms
// attempt 1: ~1000-2000ms
// attempt 2: ~2000-4000ms
// attempt 3: ~4000-8000ms
// attempt 4: ~8000-16000ms
- 没有主动管理速率。 只在触发速率限制后被动处理 429 错误,而不是监控速率限制标头并主动降低请求速率。收到 429 时,突发额度已经耗尽。监控成功响应中的
X-RateLimit-Remaining,并在剩余配额较低时降低请求速率。
// Correct: Proactive rate management
async function proactiveRateManagement(
client: AxiosInstance,
requests: AxiosRequestConfig[]
): Promise<void> {
let delayBetweenRequests = 100; // Start at 100ms between requests
for (const config of requests) {
const response = await requestWithRetry(client, config);
const rateInfo = parseRateLimitHeaders(response);
// Proactively adjust speed based on remaining quota
if (rateInfo.remaining !== null) {
if (rateInfo.remaining < 5) {
delayBetweenRequests = 5000; // Slow down significantly
} else if (rateInfo.remaining < 20) {
delayBetweenRequests = 1000; // Moderate slowdown
} else {
delayBetweenRequests = 100; // Normal speed
}
}
await new Promise((resolve) => setTimeout(resolve, delayBetweenRequests));
}
}
审查清单
- 是否针对 429 响应实现了带抖动的指数退避?
- 是否读取并遵守 429 响应中的
Retry-After标头? - 是否不存在紧密重试循环(
while(true)、间隔小于 5 秒的setInterval)? - 对于持续故障的服务,是否设置了熔断器?
- 是否主动监控
X-RateLimit-Remaining标头,在触及限制前降低请求速率? - 是否使用了正确的数值阈值(maxRetries: 5、baseDelayMs: 1000、maxDelayMs: 60000)?
- 是否遵守 Pricing API 限制(PUT/POST 为 40 req/s,DELETE 为 16 req/s,以及突发额度)?
参考资料
- 避免速率限制错误的最佳实践 — VTEX 官方指南,介绍速率限制管理和最佳实践
- 处理错误和异常 — VTEX 错误处理指南,涵盖 429 和 5xx 响应
- API 响应状态码 — VTEX API 响应代码及其含义的完整列表
- 定价 API 概览 - 速率限制 — 定价 API 的具体速率限制文档,涵盖突发额度
- Feed v3 - 最佳实践 — 订单 Feed 集成的速率限制建议
- 如何对店铺进行负载测试 — VTEX 关于速率限制行为、429 响应和熔断器的文档