Tencent/WeKnora · warning

rate limited: %s

Error message

rate limited: %s

What it means

The Notion API returned HTTP 429 (Too Many Requests). The client honors the Retry-After header (defaulting to 1s) and retries up to maxRetries=3 times. This error only escapes when all retry attempts are exhausted; it is then wrapped as `fmt.Errorf("%w: %v", datasource.ErrFetchFailed, lastErr)` before returning, so callers see the wrapped message. The body of the 429 response is embedded in the message.

Source

Thrown at internal/datasource/connector/notion/client.go:126

		switch {
		case resp.StatusCode >= 200 && resp.StatusCode < 300:
			return respBody, nil

		case resp.StatusCode == 401 || resp.StatusCode == 403:
			return nil, fmt.Errorf("%w: %s", datasource.ErrInvalidCredentials, string(respBody))

		case resp.StatusCode == 404:
			return nil, fmt.Errorf("%w: %s", datasource.ErrResourceNotFound, path)

		case resp.StatusCode == 429:
			retryAfter := resp.Header.Get("Retry-After")
			wait := 1 * time.Second
			if secs, err := strconv.ParseFloat(retryAfter, 64); err == nil && secs > 0 {
				wait = time.Duration(secs * float64(time.Second))
			}
			logger.Warnf(ctx, "[Notion] rate limited, retry after %v (attempt %d/%d)", wait, attempt+1, maxRetries)
			lastErr = fmt.Errorf("rate limited: %s", string(respBody))
			if attempt < maxRetries {
				if sErr := sleepWithContext(ctx, wait); sErr != nil {
					return nil, sErr
				}
				continue
			}

		case resp.StatusCode >= 500:
			lastErr = fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBody))
			if attempt < maxRetries {
				if sErr := sleepWithContext(ctx, time.Duration(1<<attempt)*time.Second); sErr != nil {
					return nil, sErr
				}
				continue
			}

		default:
			return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Wait and retry the sync later — the error means the token's Notion rate budget is exhausted for now.
  2. Reduce concurrency: run one sync at a time per integration token, and avoid sharing one token across multiple jobs or services.
  3. Lower the client's rate limit (rate.NewLimiter(rate.Limit(3), 3) in newClient, client.go:40) to leave headroom under Notion's ~3 req/s limit.
  4. Increase maxRetries (client.go:45) if sustained bursts are expected, so long Retry-After waits are honored.
  5. For repeatedly-hit limits on large workspaces, paginate with smaller page_size and add backoff between pages in the caller.

Example fix

// before: shared token across parallel workers hitting 429
// after: single-flight sync + reduced limiter
limiter: rate.NewLimiter(rate.Limit(2), 2), // was rate.Limit(3), 3
Defensive patterns

Strategy: retry

Validate before calling

// Check client-side budget before starting a large sync
func canSync(limiter *rate.Limiter) bool { return limiter.Allow() }

Try / catch

if err := client.Ping(ctx); err != nil {
    var fetchErr *datasource.FetchError
    if strings.Contains(err.Error(), "rate limited") {
        time.Sleep(30 * time.Second)
        // re-enqueue the job instead of failing the sync
    }
    _ = fetchErr
}

Prevention

When it happens

Trigger: Any doRequest call (Ping, GetPage, GetDatabaseInfo, GetDataSourceInfo, GetBlockChildrenFlat, getBlockChildrenRecursive) that receives 429 on attempt 0, 1, 2, AND 3 — i.e. Notion keeps rate limiting for longer than the Retry-After durations (~1s each, plus the 3 req/s local limiter).

Common situations: Bulk initial sync of a large workspace where many pages/databases are fetched back-to-back; multiple WeKnora workers sharing the same Notion integration token (limits are per-token per-workspace); Retry-After headers of several seconds or minutes exceeding the retry budget.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/c39a72d3af79823b. Report an issue: GitHub.