Answers
How do I get past WeChat's 200013 freq control error when fetching Official Account articles?
Short answer
200013 is WeChat throttling the Official Account backend login your tool uses, so slowing down or rotating accounts only delays it. Fetching through an API key keeps your own account out of the picture: Official Account article history costs $0.04 per page of 10, and backfilling 40 accounts of 300 articles each takes 1,200 calls, $48.00.
You start a backfill of 40 Official Accounts, it runs fine for twenty minutes, and then every request answers ret=200013, freq control. From then on nothing works until the next day. The fix that lasts is to stop sending those requests from your own WeChat account. Through Official Account article history you page through each account's articles with an API key; there is no backend login on your side for WeChat to throttle.
What 200013 actually means
Scrapers that read article lists from mp.weixin.qq.com sign in with an Official Account you own and call the backend's article search as that account. WeChat counts those calls per account. Past a threshold it returns 200013 and blocks that function for hours. Changing your IP doesn't help because the limit sits on the account. The usual workarounds, longer sleeps between requests and several backend accounts in rotation, make a backfill take days and add more logins that expire.
The backfill, rewritten
Each history call returns 10 articles and a cursor for the next page. Pace calls to the rate limit of your key and retry the errors that say they can be retried:
import os, time, uuid, requests
API = "https://api.okdata.online"
H = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}
def call(path, **params):
key = str(uuid.uuid4()) # same key on every retry: never pay twice
for _ in range(5):
r = requests.get(API + path, params=params, timeout=90,
headers={**H, "Idempotency-Key": key})
body = r.json()
if body["code"] == "ok":
return body
if body.get("retry") in ("yes", "same_key"):
time.sleep(body.get("retry_after_seconds") or 5)
continue
raise RuntimeError(f"{body['code']}: {body['message']}")
raise RuntimeError("gave up after 5 tries")
def history(gh):
cursor = None
while True:
body = call("/v1/wechat_mp/accounts/articles/list", id=gh, **({"cursor": cursor} if cursor else {}))
yield from body["data"]["items"]
if not body["page"]["has_more"]:
return
cursor = body["page"]["next_cursor"]
What a backfill costs
| Job | Calls | Cost |
|---|---|---|
| 1 account, 300 articles, list only | 30 | $1.20 |
| 40 accounts, 300 articles each, list only | 1,200 | $48.00 |
| Same, plus full text of 2,000 of them | 1,200 + 2,000 | {{cost:wechat_mp.accounts.articles.list1200+wechat_mp.articles.get2000}} |
Errors are free, including the busy and timeout cases the loop above retries. Full text comes from article detail.
Limits that still apply
- Your key has its own rate limit. Going over it returns
rate_limitedwith a wait time; theX-RateLimit-Remainingheader tells you how close you are. The error reference lists every code. - If the data source is busy you get
upstream_busy, not charged, retry shortly. - Only public articles. Reads and likes are filled when available and null otherwise.
If you'd rather keep scraping yourself
Space requests several seconds apart, keep each backend account's daily volume modest, and stop for the day on the first 200013 instead of pushing on, which tends to lengthen the block. That works for a single account you archive slowly. For your own account, WeChat's official API returns your published articles without going through the backend search at all. For dozens of accounts on a deadline, an API is the quicker road; see competitor monitoring.
Updated