DEV Community

Taocarts跨境
Taocarts跨境

Posted on AI-assisted

How Taocarts Syncs Taobao Inventory in Real Time Without Hammering the API

How Taocarts Syncs Taobao Inventory in Real Time Without Hammering the API

Running a reverse-taobao shopping and consolidation service means one uncomfortable truth: your catalog lives on someone else's platform. At Taocarts, we pull product and stock data from Taobao, 1688, and a few other Chinese sourcing sites so our customers can browse them in one place. The naive approach — poll every SKU every few minutes — gets you rate-limited within an hour. Here is how we redesigned the sync to stay under the limit while still feeling real time.

The problem with naive polling

When we started, the sync worker was a simple cron that looped over every product and called the sourcing API. With about 40,000 active SKUs, that meant 40,000 requests per cycle. Taobao's anti-bot layers started throttling us, and 1688 returned 429 with a retry-after header we ignored. Orders began showing stale stock, and customers bought items that were already gone. That was the moment we admitted polling everything was wrong.

Switch to event-driven deltas

The first change was to stop asking and start listening. Taobao and 1688 both expose change notifications for shops you are authorized to access. Instead of pulling the whole catalog, we subscribe to item-change events and only fetch the delta. The worker now looks like this:

async function handleItemChange(event) {
  const { itemId, shopId } = event;
  const fresh = await sourcingClient.fetchItem(itemId, { fields: ['stock', 'price', 'title'] });
  await db.items.update({ itemId }, fresh);
  redis.publish('item:updated', JSON.stringify({ itemId, shopId }));
}
Enter fullscreen mode Exit fullscreen mode

A single item fetch costs one request instead of scanning the whole shop. Volume dropped by roughly 90%.

Cache with a short TTL and a stale-while-revalidate pattern

Even deltas can burst when a shop edits many items at once. We put a Redis layer in front with a 60-second TTL. On read, if the cached value is stale but not expired beyond a grace window, we serve it and trigger a background refresh. This keeps the API calls bounded even during flash sales on the source platform.

async function getItem(itemId) {
  const cached = await redis.get(`item:${itemId}`);
  if (cached) {
    if (isStale(cached)) refreshItem(itemId); // fire and forget
    return JSON.parse(cached);
  }
  const fresh = await fetchAndCache(itemId);
  return fresh;
}
Enter fullscreen mode Exit fullscreen mode

Respect the rate limit as a first-class citizen

We wrapped every outbound call in a token-bucket limiter keyed by source platform, not by process. That way, even if we scale the worker horizontally, the global request rate stays predictable:

const limiter = new TokenBucket({ capacity: 50, refillPerSec: 10 });
await limiter.take(); // blocks when empty
return sourcingClient.request(...);
Enter fullscreen mode Exit fullscreen mode

What we learned

Real-time is a feeling, not a number. Customers do not need millisecond accuracy; they need stock that is wrong less than once a week. By moving from blind polling to event-driven deltas, adding a short-TTL cache, and treating rate limits as a design constraint instead of an afterthought, we cut API usage by an order of magnitude and stopped getting blocked.

If you are building anything that depends on a platform you do not control, design for their limits first. Your sync will be healthier, and so will your relationship with their API team.

Top comments (0)