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

  1. Stop sending immediately and wait out the penalty window before retrying
  2. Enforce/lower the push quota (checkSendQuota) so ret=-2 is never reached
  3. Consolidate notifications into fewer, larger messages per window
  4. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/f76f7df38d0632e1. Report an issue: GitHub.