Notion cost tuning
425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill notion-cost-tuningAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Optimize Notion API usage to minimize rate-limit pressure, reduce engineering overhead, and maximize throughput. Use when auditing request volume, eliminating redundant API calls, implementing caching, or restructuring queries for efficiency. Trigger with "notion cost", "notion optimize", "notion API usage", "reduce notion requests", "notion rate limit budget", "notion efficient", "notion caching".
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.3 KB, as published. Nobody here has run it
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 filter_properties (property IDs, not names) to shrink responses 60-90%; (C) filter on last_edited_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
last_edited_timefilter 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.