ThreadPage
All docs

Rate limits

ThreadPage limits how fast and how much a connection, and all of an account’s connections together, can call and write. Every plan has the same technical limits; only calls a minute and calls at once are lower on Free. Over a limit, nothing happens and the call answers 429.

View as Markdown

On this page
  1. Per connection and per account
  2. Free
  3. Images and files
  4. Webhooks, feeds and endpoint URLs
  5. What a 429 says
  6. Back off correctly
  7. Next

Per connection and per account

Per connection and per account
LimitPer connectionAll of the account’s connectionsdetails.reason
Calls a minute120300 (30 on Free)requests_minute
Calls running at once5 (2 on Free)busy
Writes a minute3060writes_minute
Writes a UTC day1,0002,000writes_day
Note content written a UTC day10 MiBcontent_day
Markdown notes converted a minute60markdown_minute
Subscriptions created a minute20subscriptions_minute
Answers a UTC day, webhook bodies included2 GiBdaily_response_volume
Changes to notes it did not add or that a person edited, and moves to Trash, a clock hour200changes_hour
The same, a UTC day1,000changes_day
Moves to Trash a clock hour100trash_hour

Adding notes doesn’t count toward the change limits. Each call counts as one change: moving a folder to Trash is one move, whatever it holds. A change limit’s Retry-After runs to the end of its clock hour or UTC day. A call that runs longer than 10 seconds answers 503 timed_out.

Everything the account writes, its own apps and the people it shares with included, also has a ceiling: 50 MiB a day, 300 writes a minute and 50,000 a day (daily_write_volume and write_rate, scope owner).

The owner gets an email when a connection’s activity is unusual, such as many changes to notes people wrote or many reads in 24 hours: at most one a day for each connection. The email never blocks a call; the limits above do.

Free

  • Connections add up to 3,000 notes a month (UTC) to the Inbox, all of them together. Then 429 quota_exhausted, without Retry-After; details.resets_at says when the count starts again.
  • Up to 1,000 notes or 20 MB wait for the owner’s browser at once. Then 409 inbox_full.

Pro has no monthly limit on notes.

Images and files

Images and files
LimitPer connectionAll of the account’s connectionsdetails.reason
One image5 MB, 20 megapixels, 300 frames413 / 422, not a 429
Image uploads a UTC day100 MB250 MBimage_bytes_day
Image and file previews a minute20image_previews_minute
Image and file previews a UTC day5001,000image_previews_day
Images decoded a minute (uploads and previews)30image_decodes_minute

Webhooks, feeds and endpoint URLs

Webhooks, feeds and endpoint URLs
LimitValue
Webhooks and pull feeds10 per connection
Live webhooks50 per account (409 webhook_limit)
Deliveries a UTC dayabout 20,000 per account; past that, webhooks show Paused until tomorrow and resume after midnight UTC
Endpoint URLs10 per connection
Events into endpoint URLs60 a minute per connection (events_minute)
Automations25 per account
Open requests100 per account; a request expires after 7 days
Owner’s tests (endpoint Test…, Test event, Test request, Send test)10 a minute, 100 a day (tests)

What a 429 says

JSON
{
  "code": "rate_limited",
  "message": "This connection has reached its current limit.",
  "retryable": true,
  "diagnostic_id": "c47a1e09-5f2b-4d86-a3c0-1e9b7d4f2a65",
  "details": {"retry_after": 12, "reason": "writes_minute", "scope": "connection"}
}
  • details.reason names the limit, from the tables above. details.scope says whose it is: connection, or owner for all of the account’s connections together.
  • details.retry_after and the Retry-After header give the seconds to wait.
  • inbox_minute and tests come only from the owner’s own actions in the app: the browsers that collect the Inbox, and the owner’s tests.

There are no RateLimit-* headers; wait Retry-After seconds.

quota_exhausted is the one 429 without Retry-After: Free’s notes for the month are used. Stop until details.resets_at.

Back off correctly

Wait as long as Retry-After says, then send the identical request with the same Operation-Id. A daily limit can mean waiting until midnight UTC; if your job can’t wait that long, stop and tell the person instead of looping.

import { setTimeout as sleep } from 'node:timers/promises';

// send() makes the identical request each time, with the same Operation-Id.
export async function withBackoff(send, { longestWait = 60 } = {}) {
  for (;;) {
    const answer = await send();
    if (answer.status !== 429 && answer.status !== 503) return answer;
    const wait = Number(answer.headers.get('Retry-After'));
    if (!wait || wait > longestWait) return answer; // quota_exhausted, or a wait you can't afford
    await sleep(wait * 1_000);
  }
}

const operationId = crypto.randomUUID();
const answer = await withBackoff(() => fetch(`https://thread.page/api/integrations/v1/threads/${process.env.THREAD_ID}/notes`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.THREADPAGE_TOKEN}`,
    'Operation-Id': operationId,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ content: { text: 'Build 412 passed.' } }),
}));
console.log(answer.status, await answer.json());

Next