chenhg5/cc-connect · error
-2
-2
Error message
weixin: sendMessage throttled by ilink (ret=-2); the bot is rate-limited and sending during the penalty escalates it, retry the message later: %w
What it means
sendChunk detects ilink's burst-throttle response (ret=-2) via isSendThrottled and converts it into a fail-fast error explaining that continuing to send during the penalty window escalates the rate limit. The original API error is wrapped for context.
Source
Thrown at platform/weixin/weixin.go:876
// isSendThrottled reports whether err is ilink sendmessage's burst-throttle
// response (ret=-2 "prepare failed"). This is a bot-wide rate-limit penalty, not a
// context_token problem: the gateway accepts any (or no) context_token on sends.
func isSendThrottled(err error) bool {
return err != nil && strings.Contains(err.Error(), "ret=-2")
}
// sendChunk sends a single chunk. If ilink throttles the send (ret=-2
// "prepare failed"), it fails fast instead of retrying: live testing showed the
// penalty is escalated by every send attempt made while it is active, so retrying
// (e.g. the old 3×500ms loop plus the extra notice send) only prolongs the outage.
func (p *Platform) sendChunk(ctx context.Context, rc *replyContext, chunk string) error {
clientID := "cc-" + randomHex(6)
err := p.api.sendText(ctx, rc.peerUserID, chunk, rc.contextToken, clientID)
if err == nil {
return nil
}
if isSendThrottled(err) {
return fmt.Errorf("weixin: sendMessage throttled by ilink (ret=-2); "+
"the bot is rate-limited and sending during the penalty escalates it, retry the message later: %w", err)
}
return err
}
func truncatePreview(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "..."
}
func splitUTF8(s string, maxRunes int) []string {
if maxRunes <= 0 || utf8.RuneCountInString(s) <= maxRunes {
return []string{s}
}
var out []string
runes := []rune(s)View on GitHub (pinned to 4000b2338a)
Solutions
- Stop sending immediately and wait out the penalty window before retrying
- Enforce/lower the push quota (checkSendQuota) so ret=-2 is never reached
- Consolidate notifications into fewer, larger messages per window
- Back off exponentially in any retry loop instead of retrying promptly
Defensive patterns
Strategy: retry
Validate before calling
// avoid triggering ret=-2: keep pushes under the quota window
if pushesInWindow >= 5 {
deferSendUntilWindowEnd(msg)
return nil
} Try / catch
if err := p.Send(ctx, rc, msg); err != nil && strings.Contains(err.Error(), "throttled by ilink") {
log.Warn("ilink penalty active; backing off long", "err", err)
time.Sleep(penaltyBackoff) // minutes, not seconds
return err // do NOT hot-retry; escalate the throttle
} Prevention
- Never retry immediately on ret=-2 — waiting escalates nothing, hot retries escalate the penalty
- Cap proactive pushes at 5-6 per window
- Use Reply instead of Send where possible
- Add long exponential backoff (minutes) to any retry path
When it happens
Trigger: p.api.sendText returns an error whose response contains ret=-2, typically after the bot exceeded roughly 5-6 pushes per window and ilink imposed a penalty period.
Common situations: Cron/timer pushes stacking up faster than the window allows; retry loops hammering the API during an active penalty and extending it; multiple sessions pushing to the same account concurrently.
Related errors
- weixin: push budget exhausted (%d push messages in the last
- -2
- weixin: convert %s to AMR: %w
- weixin: token is required (ilink bot Bearer token)
- weixin: create state dir: %w
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/f76f7df38d0632e1.
Report an issue: GitHub.