{"record":{"id":"e4ce47f62bcf6d56","repo":"chenhg5/cc-connect","slug":"2","errorCode":"-2","errorMessage":"weixin: sendMessage throttled by ilink (ret=-2); the bot is rate-limited and sending during the penalty escalates it, retry the message later: %w","messagePattern":"weixin: sendMessage throttled by ilink \\(ret=-2\\); the bot is rate-limited and sending during the penalty escalates it, retry the message later: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"platform/weixin/media_outbound.go","lineNumber":139,"sourceCode":"\t\treturn err\n\t}\n\tmsg := sendMessageReq{\n\t\tMsg: weixinOutboundMsg{\n\t\t\tFromUserID:   \"\",\n\t\t\tToUserID:     rc.peerUserID,\n\t\t\tClientID:     \"cc-\" + randomHex(8),\n\t\t\tMessageType:  messageTypeBot,\n\t\t\tMessageState: messageStateFinish,\n\t\t\tItemList:     []messageItem{item},\n\t\t\tContextToken: rc.contextToken,\n\t\t},\n\t}\n\terr := p.api.sendMessage(ctx, &msg)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif isSendThrottled(err) {\n\t\treturn fmt.Errorf(\"weixin: sendMessage throttled by ilink (ret=-2); \"+\n\t\t\t\"the bot is rate-limited and sending during the penalty escalates it, retry the message later: %w\", err)\n\t}\n\treturn err\n}\n\nfunc mediaFromUploadRef(ref *cdnUploadedRef) *cdnMedia {\n\treturn &cdnMedia{\n\t\tEncryptQueryParam: ref.downloadParam,\n\t\tAESKey:            formatAesKeyForAPI(ref.aesKey),\n\t\tEncryptType:       1,\n\t}\n}\n\nfunc buildVideoMessageItem(ref *cdnUploadedRef) messageItem {\n\treturn messageItem{\n\t\tType: messageItemVideo,\n\t\tVideoItem: &videoItem{\n\t\t\tMedia:     mediaFromUploadRef(ref),","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/weixin/media_outbound.go#L121-L157","documentation":"The ilink sendMessage backend returned ret=-2, meaning the bot is rate-limited. The library detects this with isSendThrottled and enriches the error: sending more messages during the penalty window escalates the penalty, so the send must be deferred.","triggerScenarios":"sendSingleItem calling p.api.sendMessage which returns an error matched by isSendThrottled (ret=-2) while sending an image/file/audio item.","commonSituations":"Burst media sends to the same peer; scheduled broadcasts hitting WeChat rate limits; a bot that kept retrying during a penalty and escalated it.","solutions":["Stop sending immediately and retry the message after a backoff (the penalty window must lapse).","Add exponential backoff + jitter with a dedicated rate limiter for weixin sends.","Do NOT retry in a tight loop — sending during the penalty escalates the restriction.","Throttle outbound media volume proactively to stay under ilink limits."],"exampleFix":"// before\nfor _, img := range images { go p.SendImage(ctx, rc, img) } // triggers ret=-2\n// after\nlimiter := rate.NewLimiter(rate.Every(2*time.Second), 1)\nfor _, img := range images {\n    limiter.Wait(ctx)\n    if err := p.SendImage(ctx, rc, img); err != nil && isSendThrottled(err) {\n        time.Sleep(5 * time.Minute) // back off hard\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := p.SendImage(ctx, rc, img); err != nil && isSendThrottled(err) {\n    time.Sleep(penaltyBackoff) // minutes, not seconds; sending now escalates\n    retry(ctx, img)\n}","preventionTips":["Rate-limit all weixin outbound sends with a token bucket","Never retry immediately on ret=-2 — it escalates the penalty","Use exponential backoff with jitter","Spread bulk media sends over time"],"tags":["weixin","rate-limit","throttle","retry"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}