OB

obsidian-rate-limits

Provides patterns for debouncing and throttling file operations in Obsidian to maintain plugin responsiveness.

Install

mkdir -p .claude/skills/obsidian-rate-limits && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/1205" && unzip -o skill.zip -d .claude/skills/obsidian-rate-limits && rm skill.zip

Installs to .claude/skills/obsidian-rate-limits

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Handle Obsidian file system operations and throttling patterns.
63 charsno explicit “when” trigger
Intermediate

Key capabilities

  • Debounce `vault.on('modify')` events to prevent excessive calls
  • Batch file operations to avoid UI freezes
  • Throttle UI updates using `requestAnimationFrame`
  • Queue write operations to prevent data corruption
  • Provide progress feedback for long-running tasks

How it works

The skill provides TypeScript code examples for implementing debouncing, batching, throttling, and async queue patterns to manage file system events and UI updates in Obsidian plugins.

Inputs & outputs

You give it
Obsidian plugin code performing file system operations or UI updates
You get back
An Obsidian plugin with improved responsiveness and performance during bulk operations

When to use obsidian-rate-limits

  • Throttle file system events
  • Manage bulk operations
  • Prevent UI freezes
  • Optimize plugin file handling

About this skill

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.

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):

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.

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.

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.

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.

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.

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

IssueCauseSolution
UI freezes during bulk operationProcessing all files synchronouslyBatch with await sleep(0) between batches
Data corruptionConcurrent writes to same fileUse a write queue to serialize operations
Memory pressure on large vaultsLoading all file contents at onceProcess in batches of 50, release references
Missed file changesDebounce interval too longKeep debounce under 500ms; use leading edge
Timers leak after disableUsing raw setIntervalAlways use this.registerInterval()
Layout thrashingUpdating DOM on every eventCoalesce with requestAnimationFrame

Examples

Vault Statistics Collector

// 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);
    for (const file of batch) {
      const content = await this.app.vault.cachedRead(file);
      words += content.split(/\s+/).length;
    }
    await sleep(0);
  }

  return { total: files.length, words };
}

Debounced Search Index Rebuild

// Rebuild search index at most once per 2 seconds
private rebuildIndex = debounce(async () => {
  const files = this.app.vault.getMarkdownFiles();
  this.index.clear();
  for (const file of files) {
    const cache = this.app.metadataCache.getFileCache(file);
    if (cache?.frontmatter) {
      this.index.set(file.path, cache.frontmatter);
    }
  }
}, 2000, true);

Resources

Next Steps

For event handling patterns that complement these throttling strategies, see obsidian-webhooks-events. For production deployment readiness, see obsidian-prod-checklist.

Prerequisites

Understanding of JavaScript event loop and `requestAnimationFrame`Familiarity with async/await and PromisesWorking Obsidian plugin with file operations

Limitations

  • Requires understanding of JavaScript event loop
  • Requires familiarity with async/await and Promises

How it compares

This skill offers specific code-based solutions for managing performance in Obsidian's single-threaded UI, unlike general JavaScript performance advice that may not address Obsidian's unique event handling.

Compared to similar skills

obsidian-rate-limits side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
obsidian-rate-limits (this skill)527dNo flagsIntermediate
obsidian-performance-tuning627dReviewAdvanced
obsidian-observability527dReviewIntermediate
codex-code-review18moReviewIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

More by jeremylongshore

View all by jeremylongshore

analyzing-logs

jeremylongshore

Analyze application logs to detect performance issues, identify error patterns, and improve stability by extracting key insights.

14123

ollama-setup

jeremylongshore

Configure auto-configure Ollama when user needs local LLM deployment, free AI alternatives, or wants to eliminate hosted API costs. Trigger phrases: "install ollama", "local AI", "free LLM", "self-hosted AI", "replace OpenAI", "no API costs". Use when appropriate context detected. Trigger with relevant phrases based on skill purpose.

1167

backtesting-trading-strategies

jeremylongshore

Backtest crypto and traditional trading strategies against historical data. Calculates performance metrics (Sharpe, Sortino, max drawdown), generates equity curves, and optimizes strategy parameters. Use when user wants to test a trading strategy, validate signals, or compare approaches. Trigger with phrases like "backtest strategy", "test trading strategy", "historical performance", "simulate trades", "optimize parameters", or "validate signals".

1071

generating-database-seed-data

jeremylongshore

Process this skill enables AI assistant to generate realistic test data and database seed scripts for development and testing environments. it uses faker libraries to create realistic data, maintains relational integrity, and allows configurable data volumes. u... Use when working with databases or data models. Trigger with phrases like 'database', 'query', or 'schema'.

1033

cursor-codebase-indexing

jeremylongshore

Execute set up and optimize Cursor codebase indexing. Triggers on "cursor index setup", "codebase indexing", "index codebase", "cursor semantic search". Use when working with cursor codebase indexing functionality. Trigger with phrases like "cursor codebase indexing", "cursor indexing", "cursor".

885

testing-mobile-apps

jeremylongshore

Execute mobile app testing on iOS and Android devices/simulators. Use when performing specialized testing. Trigger with phrases like "test mobile app", "run iOS tests", or "validate Android functionality".

810

Search skills

Search the agent skills registry