Skip to content
obsidian-rate-limits logo

Obsidian Rate Limits

obsidian-rate-limits

Handle Obsidian file system operations and throttling patterns.

SKILL.md

Full skill instructions

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.

More DevOps & CI/CD skills

Project scaffolding, deployment configuration, and CI/CD setup for Google ADK agents.

6K 472.6K
View

Set up tracing, logging, and monitoring for deployed ADK agents across Cloud Trace, BigQuery, and third-party platforms.

6K 472.6K
View

Enterprise Azure infrastructure architect generating Bicep or Terraform from workload descriptions.

1.5K 454.3K
View
azure-kubernetes logo
DevOps & CI/CD

azure-kubernetes

Plan and configure production-ready Azure Kubernetes Service clusters with Day-0 and Day-1 best practices.

1.5K 447.1K
View

Raw mechanical interfaces fusing Swiss typographic print with military terminal aesthetics. Rigid grids, extreme type scale contrast, utilitarian color, analog degradation effects. For data-heavy dashboards, portfolios, or editorial sites that need to feel like declassified blueprints.

92.7K 354.9K
View
just-scrape logo
DevOps & CI/CD

just-scrape

Web search, scraping, extraction, crawling, and monitoring via ScrapeGraph AI CLI.

69 245K
View

Skill for working with Firebase Hosting (Classic). Use this when you want to deploy static web apps, Single Page Apps (SPAs), or simple microservices. Do NOT use for Firebase App Hosting.

462 159.7K
View

Deploy and manage web apps with Firebase App Hosting. Use this skill when deploying Next.js/Angular apps with backends.

462 159.1K
View
deploy-to-vercel logo
DevOps & CI/CD

deploy-to-vercel

Deploy applications and websites to Vercel. Use when the user requests deployment actions like "deploy my app", "deploy and give me the link", "push this live", or "create a preview deployment".

31.9K 146.6K
View
programmatic-seo logo
DevOps & CI/CD

programmatic-seo

Build SEO-optimized pages at scale using templates, data, and proven playbook patterns.

53.3K 140.1K
View

Design and build isolated, reusable Convex backend components with clear boundaries and app-facing wrappers.

63 123.5K
View

Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".

31.9K 116.1K
View

Coding & apps AI tools

Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View
Imagica logo
Coding & apps

Imagica

Build a no-code AI app in minutes.

Freemium
View
E
Coding & apps

Emergent.sh

An IDE for code migration from legacy to modern frameworks through coding agents.

Freemium
View
Wonder Dynamics logo
Coding & apps

Wonder Dynamics

Automate CGI animation in live-action scenes

Paid
View
M
Coding & apps

Mixo

Launch a website in seconds with AI.

Paid
View
AI Code Convert logo
Coding & apps

AI Code Convert

Streamline Your Coding Experience with AI Code Helper

Free
View