obsidian-rate-limits
Handle Obsidian file system operations and throttling patterns. Use when processing many files, handling bulk operations, or preventing performance issues from excessive operations. Trigger with phrases like "obsidian rate limit", "obsidian bulk operations", "obsidian file throttling", "obsidian performance limits".
What this skill does
# Obsidian Rate Limits
## Overview
Obsidian has no traditional API rate limits, but it runs on Electron with a single-threaded UI. This skill covers debouncing, batching, throttling, and async queue patterns to keep plugins responsive and prevent UI freezes.
## Prerequisites
- Understanding of JavaScript event loop and `requestAnimationFrame`
- Familiarity with async/await and Promises
- Working Obsidian plugin with file operations
## Instructions
### Step 1: Debounce vault.on('modify') Events
`vault.on('modify')` fires on every keystroke when a user types in a note. Without debouncing, your handler runs hundreds of times per second.
```typescript
import { Plugin, TFile, debounce } from 'obsidian';
export default class ThrottledPlugin extends Plugin {
async onload() {
// Obsidian provides a built-in debounce utility
const debouncedHandler = debounce(
(file: TFile) => this.handleFileModified(file),
500, // wait 500ms after last keystroke
true // run on leading edge too (immediate first call)
);
this.registerEvent(
this.app.vault.on('modify', debouncedHandler)
);
}
private async handleFileModified(file: TFile) {
// This runs at most once per 500ms per burst of edits
const cache = this.app.metadataCache.getFileCache(file);
if (cache?.frontmatter?.tracked) {
await this.updateIndex(file);
}
}
}
```
If you need per-file debouncing (common when multiple files change simultaneously):
```typescript
private fileTimers = new Map<string, NodeJS.Timeout>();
private debouncedPerFile(file: TFile, fn: () => void, delay = 500) {
const existing = this.fileTimers.get(file.path);
if (existing) clearTimeout(existing);
const timer = setTimeout(() => {
this.fileTimers.delete(file.path);
fn();
}, delay);
// Use activeWindow for Obsidian's timeout tracking
this.fileTimers.set(file.path, timer);
}
```
### Step 2: Batch File Operations with UI Yielding
Processing hundreds of files synchronously locks the UI. Yield back to the main thread between batches.
```typescript
async processAllFiles(): Promise<void> {
const files = this.app.vault.getMarkdownFiles();
const BATCH_SIZE = 50;
const results: ProcessResult[] = [];
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const batch = files.slice(i, i + BATCH_SIZE);
// Process one batch
for (const file of batch) {
const content = await this.app.vault.cachedRead(file);
results.push(this.processContent(file.path, content));
}
// Yield to UI thread between batches
await sleep(0);
// Update progress if you have a status bar or notice
const pct = Math.round(((i + batch.length) / files.length) * 100);
this.statusBar?.setText(`Processing: ${pct}%`);
}
this.statusBar?.setText(`Done: ${results.length} files processed`);
}
// Obsidian exports sleep(), or use this:
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
```
### Step 3: Throttle UI Updates
Updating DOM elements on every event causes layout thrashing. Throttle to animation frames.
```typescript
class ThrottledStatusView {
private pendingUpdate = false;
private el: HTMLElement;
private data: { count: number; lastFile: string } = { count: 0, lastFile: '' };
constructor(el: HTMLElement) {
this.el = el;
}
// Call this as often as you want — it coalesces to one paint per frame
update(count: number, lastFile: string) {
this.data = { count, lastFile };
if (!this.pendingUpdate) {
this.pendingUpdate = true;
requestAnimationFrame(() => {
this.render();
this.pendingUpdate = false;
});
}
}
private render() {
this.el.empty();
this.el.createEl('span', { text: `${this.data.count} files` });
this.el.createEl('span', { text: this.data.lastFile, cls: 'nav-file-title' });
}
}
```
### Step 4: Async Queue for Write Operations
Concurrent writes to the same file corrupt data. Queue writes so only one runs at a time.
```typescript
class WriteQueue {
private queue: Array<() => Promise<void>> = [];
private running = false;
async enqueue(fn: () => Promise<void>): Promise<void> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
await fn();
resolve();
} catch (e) {
reject(e);
}
});
this.process();
});
}
private async process() {
if (this.running) return;
this.running = true;
while (this.queue.length > 0) {
const task = this.queue.shift()!;
await task();
// Small delay between writes to avoid overwhelming disk I/O
await sleep(10);
}
this.running = false;
}
}
// Usage in plugin
class MyPlugin extends Plugin {
private writeQueue = new WriteQueue();
async safeWrite(file: TFile, content: string) {
await this.writeQueue.enqueue(async () => {
await this.app.vault.modify(file, content);
});
}
}
```
### Step 5: Progress Notice for Long Operations
Give users feedback during operations that take more than a second.
```typescript
async bulkUpdateFrontmatter(
files: TFile[],
updater: (fm: any) => void
): Promise<{ success: number; failed: string[] }> {
const failed: string[] = [];
let success = 0;
// Use Notice with a timeout of 0 to create a persistent notice
const notice = new Notice(`Updating 0/${files.length} files...`, 0);
try {
for (let i = 0; i < files.length; i++) {
try {
await this.app.fileManager.processFrontMatter(files[i], updater);
success++;
} catch (e) {
failed.push(files[i].path);
}
// Update notice every 10 files to avoid DOM thrashing
if (i % 10 === 0 || i === files.length - 1) {
notice.setMessage(`Updating ${i + 1}/${files.length} files...`);
await sleep(0); // yield to UI
}
}
} finally {
// Replace persistent notice with a timed one
notice.hide();
new Notice(`Updated ${success} files. ${failed.length} failed.`);
}
return { success, failed };
}
```
### Step 6: registerInterval for Periodic Tasks
Use Obsidian's `registerInterval` instead of raw `setInterval` — it auto-clears on plugin unload.
```typescript
async onload() {
// Sync data every 5 minutes
this.registerInterval(
window.setInterval(() => {
this.syncData();
}, 5 * 60 * 1000)
);
}
private async syncData() {
// Guard against overlapping runs
if (this.syncing) return;
this.syncing = true;
try {
await this.performSync();
} finally {
this.syncing = false;
}
}
```
## Output
- Debounced event handlers that fire at most once per 500ms
- Batch file processor with UI yielding and progress feedback
- Throttled UI updates using `requestAnimationFrame`
- Serialized write queue preventing concurrent file corruption
- Periodic tasks with `registerInterval` and overlap guards
## Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| UI freezes during bulk operation | Processing all files synchronously | Batch with `await sleep(0)` between batches |
| Data corruption | Concurrent writes to same file | Use a write queue to serialize operations |
| Memory pressure on large vaults | Loading all file contents at once | Process in batches of 50, release references |
| Missed file changes | Debounce interval too long | Keep debounce under 500ms; use leading edge |
| Timers leak after disable | Using raw setInterval | Always use `this.registerInterval()` |
| Layout thrashing | Updating DOM on every event | Coalesce with `requestAnimationFrame` |
## Examples
### Vault Statistics Collector
```typescript
// Efficient vault scan that doesn't freeze UI
async getVaultStats(): Promise<{ total: number; words: number }> {
const files = this.app.vault.getMarkdownFiles();
let words = 0;
for (let i = 0; i < files.length; i += 50) {
const batch = files.slice(i, i + 50);
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.