notion-cost-tuning
'Optimize Notion API usage to minimize rate-limit pressure, reduce engineering
Allowed Tools
Provided by Plugin
notion-pack
Claude Code skill pack for Notion (30 skills)
Installation
This skill is included in the notion-pack plugin:
/plugin install notion-pack@claude-code-plugins-plus
Click to copy
Instructions
Notion Cost Tuning
Overview
The Notion API is free with every workspace plan — there is no per-call pricing. The real "cost" is the 3 requests/second rate limit (per integration token) plus the engineering time wasted on inefficient patterns. Apply the six strategies below to reduce request volume by 80-95%.
Notion workspace pricing (for context — API access is included at every tier):
| Plan | Price | API Access | Rate Limit |
|---|---|---|---|
| Free | $0 | Full API | 3 req/sec |
| Plus | $12/user/mo | Full API | 3 req/sec |
| Business | $28/user/mo | Full API | 3 req/sec |
| Enterprise | Custom | Full API | 3 req/sec |
The rate limit is identical across all plans. Optimization is about staying within 3 req/sec, not reducing a bill.
Prerequisites
@notionhq/clientv2.x installed (npm install @notionhq/client)- Integration token from notion.so/my-integrations
- Token shared with target pages/databases via the Connections menu in Notion
- For queue patterns:
p-queuev8+ (npm install p-queue) - For caching:
node-cacheorlru-cache(npm install lru-cache)
Authentication
Every technique here uses the same auth as any Notion integration: a single bearer token passed to the client constructor. Store it in an environment variable (NOTION_TOKEN) — never hardcode it — and share it with each target page/database through the Connections menu. One token = one 3 req/sec budget; provision a separate integration token per service when you need independent budgets.
const notion = new Client({ auth: process.env.NOTION_TOKEN });
Instructions
Work the three steps in order. Each links to the full, copy-paste-ready code in references/implementation.md; the essentials are inlined below so you can follow the flow from here.
Step 1: Audit current request volume
Measure before optimizing. Wrap every Notion call in a tracker that records method, endpoint, timestamp, and duration, then print a per-minute report to find the operations eating > 50% of your budget (usually polling loops, redundant retrieves, and full-table scans).
async function tracked<T>(method: string, fn: () => Promise<T>): Promise<T> {
const start = Date.now();
try {
return await fn();
} finally {
requestLog.push({ method, timestamp: start, durationMs: Date.now() - start });
}
}
Full tracker + auditReport(): references/implementation.md, § Step 1.
Step 2: Eliminate redundant reads and shrink payloads
Three patterns cut reads immediately: (A) stop calling pages.retrieve on pages you already got from databases.query — the properties are already there; (B) pass filterproperties (property IDs, not names) to shrink responses 60-90%; (C) filter on lastedited_time to fetch only what changed since the last sync instead of re-scanning the whole database.
// Redundant: query already returned every property — skip the retrieve
const { results } = await notion.databases.query({ database_id: dbId });
for (const page of results) processPage(page); // 1 request total, not 1 + N
All three patterns with runnable code: references/implementation.md, § Step 2.
Step 3: Cache, batch, and replace polling
Cache read-heavy data with a TTL'd LRU; throttle writes through a p-queue sized to the 3 req/sec limit; batch up to 100 blocks per blocks.children.append; and replace polling loops with webhooks (a 10-second poll costs 360 requests/hour per database — webhooks cost zero).
// Rate-limited write queue — never exceeds 3 req/sec
const queue = new PQueue({ concurrency: 3, interval: 1000, intervalCap: 3 });
Caching, batching, and the webhook handler: references/implementation.md, § Step 3.
Output
After applying these optimizations:
- Audit report showing request volume baseline and hotspots
- Redundant reads eliminated — no duplicate
pages.retrieveafterdatabases.query - Payload sizes reduced 60-90% via
filter_properties - Incremental sync via
lasteditedtimefilter replacing full-table scans - Cache layer with TTL-based invalidation for reads
- Write throughput maximized via queue-based throttling and block batching
- Polling eliminated where webhooks are available (360 req/hr per database saved)
Typical impact: integrations drop from 500+ requests/hour to under 200 requests/hour (80-95% reduction), staying well within the 3 req/sec limit even with multiple databases.
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| 429 Too Many Requests despite optimization | Shared token across multiple services | Use separate integration tokens per service; each gets its own 3 req/sec budget |
| Stale cached data causing bugs | Cache TTL too long for the use case | Shorten TTL to 30-60s for volatile data, or use webhook-based cache invalidation |
| Webhook not triggering | Integration not connected to the page/database | Share via Connections menu in Notion; verify webhook URL is publicly accessible |
filter_properties returning empty results |
Using property names instead of IDs | Run databases.retrieve to get property IDs (short strings like abc1), not display names |
| Incremental sync missing updates | Clock skew between client and Notion server | Subtract 5-second buffer from lastSync timestamp to create overlap window |
blocks.children.append failing with 400 |
More than 100 children in single call | Chunk arrays into groups of 100 before appending |
Examples
Two complete worked examples live in references/examples.md:
- Request Reduction Calculator — estimate before/after request volume and rate-limit headroom for your own database count, page count, and poll interval before you commit to changes.
- Full Optimization Wrapper — a drop-in
createOptimizedClient(token)that bundles caching, request tracking, and a rate-limited write queue into one client.
Skeleton of the wrapper (full body in the reference):
export function createOptimizedClient(token: string) {
const notion = new Client({ auth: token });
const cache = new LRUCache<string, any>({ max: 1000, ttl: 5 * 60 * 1000 });
const writeQueue = new PQueue({ concurrency: 3, interval: 1000, intervalCap: 3 });
// getPage / queryDatabase / createPage / invalidate / stats — see references/examples.md
}
Resources
- Notion API Rate Limits — 3 req/sec per token,
Retry-Afterheader on 429 - Database Query Filter — push filtering server-side
- Filter Properties Parameter — reduce response payload size
- Notion Webhooks — event-driven updates replacing polling
- Block Children Append — batch up to 100 blocks per call
- Notion Pricing — API included at all tiers, no per-call charges
- references/implementation.md — full three-step walkthrough with complete code
- references/examples.md — request calculator and drop-in optimized client
Next Steps
For rate-limit retry patterns, see notion-rate-limits. For query and search patterns, see notion-search-retrieve. For overall architecture guidance, see notion-reference-architecture.