MP-301e · Module 2

Backpressure & Flow Control

3 min read

Backpressure is the mechanism that prevents a fast producer from overwhelming a slow consumer. In MCP real-time data, the producer is the data source (database changes, event streams, API webhooks) and the consumer is the AI model reading resources. If the producer generates 1000 events per second but the model processes 1 resource read per second, the intermediate MCP server must buffer, drop, or throttle — and the choice between these strategies determines whether the system degrades gracefully or collapses.

MCP's pull-on-notify pattern provides natural backpressure at the protocol level. The server emits a notification that data has changed, but the client decides when (and whether) to re-read the resource. If the client is busy processing previous data, it simply does not read — and the server continues emitting notifications without blocking. The debouncing layer coalesces rapid notifications into single events. This means the server never needs to buffer data for the client; it only needs to track that a change occurred, not what changed.

Server-side backpressure against the data source is more complex. When the MCP server tails an event stream faster than it can process and route notifications, it needs to slow down consumption. Strategies include: pausing the stream consumer when the notification queue exceeds a depth threshold, sampling (processing every Nth event for high-frequency metrics), and aggregation (batching multiple events into a single summary before notifying). The right strategy depends on whether every event matters (financial transactions — no sampling) or only the latest state matters (UI metrics — aggressive sampling).

// Backpressure-aware notification queue
class NotificationQueue {
  private pending = new Map<string, number>(); // URI → change count
  private maxDepth = 100;
  private processing = false;

  enqueue(uri: string): void {
    const count = this.pending.get(uri) ?? 0;
    this.pending.set(uri, count + 1);

    // Backpressure: if queue is full, drop oldest entries
    if (this.pending.size > this.maxDepth) {
      const oldest = this.pending.keys().next().value;
      if (oldest) this.pending.delete(oldest);
      console.warn(`Backpressure: dropped notification for ${oldest}`);
    }

    if (!this.processing) this.drain();
  }

  private async drain(): Promise<void> {
    this.processing = true;
    while (this.pending.size > 0) {
      const batch = new Map(this.pending);
      this.pending.clear();

      for (const [uri, changeCount] of batch) {
        server.notification({
          method: "notifications/resources/updated",
          params: { uri },
        });
        // Optional: include change count for client prioritization
      }

      // Yield to event loop between batches
      await new Promise((resolve) => setImmediate(resolve));
    }
    this.processing = false;
  }
}

// Sampling strategy for high-frequency metrics
class SampledStream {
  private sampleRate: number;
  private count = 0;

  constructor(sampleRate: number) {
    this.sampleRate = sampleRate; // e.g., 10 = process every 10th event
  }

  shouldProcess(): boolean {
    this.count++;
    return this.count % this.sampleRate === 0;
  }
}
  1. Measure producer vs. consumer rates Log the event production rate and the client re-read rate. If production is 10x or more than consumption, you need explicit backpressure strategies.
  2. Choose a strategy per data source Financial data: buffer everything, never drop. Metrics: sample aggressively. Logs: tail with a sliding window, oldest entries fall off.
  3. Monitor queue depth Expose the notification queue size as a health metric. Alert when depth consistently exceeds 50% of max — you are approaching the drop threshold.