RedNote API Best Practices: Rate Limits, Retries, Pagination & Dedup
Taking RedNote (Xiaohongshu) data collection from "it runs" to "stable at scale" comes down to a few engineering details: rate limiting, retries, pagination, deduplication, and cost control. Here's a copy-paste-ready set of best practices for use with the Rnote API.
1. Rate limiting & concurrency
Cap concurrency and throttle your requests instead of maxing out immediately. On 429 (rate limited), back off and retry — a smooth request curve is more stable and less stressful than bursty floods.
2. Retries & backoff
Retry only on network errors and 5xx, using exponential backoff (1s, 2s, 4s…). The good news: the Rnote API bills only successful (HTTP 2xx) requests, so retries on failures cost nothing — add retry logic with confidence.
3. Pagination: cursors differ by endpoint
| Endpoint | Pagination |
|---|---|
search/notes |
increment page |
user/posted |
use the cursor from the previous page |
note/comments |
cursor object cursor + index |
topic/feed |
cursor_score + last_note_id + last_note_ct |
Always carry the cursor fields from the previous response into the next request to keep pagination consistent.
4. Deduplication
Across pages, sort orders, and jobs you'll see duplicates. Deduplicate by note_id / user_id (a set or a unique DB index) — it saves credits and keeps your data clean.
5. Cost & budget control
- Only successful requests are billed — failures are free.
- Validate your logic and fields on a small batch first, then scale.
- Set a budget cap per job so a misbehaving script can't run away.
A robust paginated collection skeleton (Python)
import time, requests
API = "https://rnote.dev/api/v2/crawler"
H = {"X-API-Key": "YOUR_API_KEY"}
def get(path, params, retries=3):
for i in range(retries):
r = requests.get(f"{API}/{path}", params=params, headers=H, timeout=30)
if r.status_code == 200:
return r.json()
if r.status_code == 429:
time.sleep(2 ** i) # rate-limit backoff
elif r.status_code >= 500:
time.sleep(2 ** i) # server-error backoff
else:
r.raise_for_status() # don't retry 4xx
raise RuntimeError("retries exhausted")
def search_all(keyword, max_pages=10):
seen = set()
for page in range(1, max_pages + 1):
data = get("search/notes", {"keyword": keyword, "page": page})
items = extract_items(data) # pull the list per response shape
if not items:
break
for it in items:
nid = it["note_id"]
if nid not in seen: # dedup
seen.add(nid)
yield it
Get started
Drop these practices into your collection jobs and they'll run reliably at volume with little change. Sign up free for an API key, get going with the Python / Go tutorials, and see the API docs and pricing.