MP-301e · Module 3

Polling Optimization

3 min read

Not every data source supports push notifications. REST APIs, legacy databases without change tracking, and file systems without watch support require polling — periodically checking for changes and emitting MCP notifications when the data differs from the last check. Polling is the fallback strategy, and it works well when tuned correctly. The goal is to minimize latency (time between a change and the notification) while minimizing load (number of unnecessary checks).

Adaptive polling adjusts the interval based on observed change frequency. When the data source changes frequently, the poller shortens its interval (more checks, lower latency). When the source is stable, the poller lengthens its interval (fewer checks, less load). A simple algorithm: start at 30 seconds, halve the interval each time a change is detected (minimum 1 second), double it each time no change is found (maximum 5 minutes). This converges quickly to the optimal frequency for any data source.

class AdaptivePoller {
  private intervalMs: number;
  private minIntervalMs: number;
  private maxIntervalMs: number;
  private lastHash: string | null = null;
  private timer: NodeJS.Timeout | null = null;

  constructor(
    private uri: string,
    private fetchFn: () => Promise<string>,
    options: { min?: number; max?: number; initial?: number } = {}
  ) {
    this.minIntervalMs = options.min ?? 1_000;
    this.maxIntervalMs = options.max ?? 300_000;
    this.intervalMs = options.initial ?? 30_000;
  }

  start(): void {
    this.poll();
  }

  stop(): void {
    if (this.timer) clearTimeout(this.timer);
  }

  private async poll(): Promise<void> {
    try {
      const data = await this.fetchFn();
      const hash = simpleHash(data);

      if (this.lastHash !== null && hash !== this.lastHash) {
        // Data changed — notify and speed up
        subscriptionManager.onChange(this.uri);
        this.intervalMs = Math.max(
          this.intervalMs / 2,
          this.minIntervalMs
        );
      } else {
        // No change — slow down
        this.intervalMs = Math.min(
          this.intervalMs * 2,
          this.maxIntervalMs
        );
      }

      this.lastHash = hash;
    } catch (err) {
      // On error, back off aggressively
      this.intervalMs = Math.min(
        this.intervalMs * 4,
        this.maxIntervalMs
      );
    }

    this.timer = setTimeout(() => this.poll(), this.intervalMs);
  }
}

function simpleHash(data: string): string {
  let h = 0;
  for (let i = 0; i < data.length; i++) {
    h = ((h << 5) - h + data.charCodeAt(i)) | 0;
  }
  return h.toString(36);
}

Do This

  • Use adaptive polling that speeds up when changes are frequent and slows down when stable
  • Compare data hashes instead of full content to detect changes efficiently
  • Use conditional HTTP requests (ETags, Last-Modified) to minimize bandwidth
  • Log polling intervals to tune min/max bounds based on real-world change patterns

Avoid This

  • Poll at a fixed interval regardless of change frequency — either too slow or too wasteful
  • Fetch and compare full response bodies for change detection — hash them instead
  • Poll from the MCP client — poll on the server and use MCP notifications for delivery
  • Forget to handle polling errors — a crashed poller silently stops all notifications