Skip to content

Request Batching & Debouncing Strategies

Request Batching

Basic Batching Implementation

Request batching is a critical technique for reducing network overhead and improving mobile app performance by combining multiple small requests into fewer larger ones.

Simple Request Batcher

javascript
class RequestBatcher {
  constructor(options = {}) {
    this.batchSize = options.batchSize || 10;
    this.batchTimeout = options.batchTimeout || 100; // ms
    this.pendingRequests = [];
    this.timer = null;
  }

  addRequest(request) {
    return new Promise((resolve, reject) => {
      this.pendingRequests.push({
        ...request,
        resolve,
        reject
      });

      if (this.pendingRequests.length >= this.batchSize) {
        this.flush();
      } else if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), this.batchTimeout);
      }
    });
  }

  flush() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }

    if (this.pendingRequests.length === 0) return;

    const requestsToProcess = this.pendingRequests.slice();
    this.pendingRequests = [];

    this.processBatch(requestsToProcess);
  }

  async processBatch(requests) {
    try {
      const batchPayload = {
        requests: requests.map(req => ({
          id: req.id || this.generateId(),
          method: req.method,
          url: req.url,
          data: req.data,
          headers: req.headers
        }))
      };

      const response = await fetch('/api/batch', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(batchPayload)
      });

      const batchResult = await response.json();
      
      // Distribute results to individual requests
      requests.forEach(request => {
        const result = batchResult.responses.find(r => r.id === request.id);
        if (result) {
          if (result.success) {
            request.resolve(result.data);
          } else {
            request.reject(new Error(result.error));
          }
        } else {
          request.reject(new Error('No response found for request'));
        }
      });
    } catch (error) {
      // If batch fails, reject all requests
      requests.forEach(request => request.reject(error));
    }
  }

  generateId() {
    return Math.random().toString(36).substring(2) + Date.now().toString(36);
  }
}

// Usage
const batcher = new RequestBatcher({ batchSize: 5, batchTimeout: 200 });

// Individual requests are automatically batched
batcher.addRequest({
  method: 'GET',
  url: '/api/users/1'
}).then(user => console.log(user));

batcher.addRequest({
  method: 'GET',
  url: '/api/users/2'
}).then(user => console.log(user));

Smart Batching with Request Types

javascript
class SmartRequestBatcher {
  constructor() {
    this.batchers = new Map();
    this.defaultConfig = {
      read: { batchSize: 20, timeout: 50 },
      write: { batchSize: 5, timeout: 100 },
      delete: { batchSize: 10, timeout: 200 }
    };
  }

  addRequest(request) {
    const requestType = this.getRequestType(request);
    
    if (!this.batchers.has(requestType)) {
      const config = this.defaultConfig[requestType] || this.defaultConfig.read;
      this.batchers.set(requestType, new RequestBatcher(config));
    }

    return this.batchers.get(requestType).addRequest(request);
  }

  getRequestType(request) {
    if (request.method === 'GET') return 'read';
    if (request.method === 'POST' || request.method === 'PUT') return 'write';
    if (request.method === 'DELETE') return 'delete';
    return 'read';
  }

  // Priority-based batching
  addPriorityRequest(request, priority = 'normal') {
    if (priority === 'high') {
      // High priority requests bypass batching
      return this.executeImmediately(request);
    }
    
    return this.addRequest(request);
  }

  async executeImmediately(request) {
    try {
      const response = await fetch(request.url, {
        method: request.method,
        headers: request.headers,
        body: request.data ? JSON.stringify(request.data) : null
      });
      
      return await response.json();
    } catch (error) {
      throw error;
    }
  }
}

GraphQL Batching

javascript
class GraphQLBatcher {
  constructor(endpoint, options = {}) {
    this.endpoint = endpoint;
    this.batchTimeout = options.batchTimeout || 10;
    this.maxBatchSize = options.maxBatchSize || 10;
    this.pendingQueries = [];
    this.batchTimer = null;
  }

  query(query, variables = {}) {
    return new Promise((resolve, reject) => {
      const queryRequest = {
        query: query.loc ? query.loc.source.body : query,
        variables,
        resolve,
        reject
      };

      this.pendingQueries.push(queryRequest);

      if (this.pendingQueries.length >= this.maxBatchSize) {
        this.executeBatch();
      } else if (!this.batchTimer) {
        this.batchTimer = setTimeout(() => this.executeBatch(), this.batchTimeout);
      }
    });
  }

  async executeBatch() {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
      this.batchTimer = null;
    }

    if (this.pendingQueries.length === 0) return;

    const currentBatch = this.pendingQueries.slice();
    this.pendingQueries = [];

    try {
      const batchedQuery = this.createBatchedQuery(currentBatch);
      
      const response = await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(batchedQuery)
      });

      const results = await response.json();
      
      // Handle batch response
      if (Array.isArray(results)) {
        results.forEach((result, index) => {
          const originalQuery = currentBatch[index];
          if (result.errors) {
            originalQuery.reject(new Error(result.errors[0].message));
          } else {
            originalQuery.resolve(result.data);
          }
        });
      } else {
        // Single response for batch
        currentBatch.forEach(query => {
          if (results.errors) {
            query.reject(new Error(results.errors[0].message));
          } else {
            query.resolve(results.data);
          }
        });
      }
    } catch (error) {
      currentBatch.forEach(query => query.reject(error));
    }
  }

  createBatchedQuery(queries) {
    if (queries.length === 1) {
      return {
        query: queries[0].query,
        variables: queries[0].variables
      };
    }

    // Create a single query with multiple operations
    const operations = queries.map((q, index) => {
      const operationName = `batch_${index}`;
      return q.query.replace('query', `query ${operationName}`);
    });

    return {
      query: operations.join('\n'),
      variables: queries.reduce((acc, q, index) => {
        Object.keys(q.variables).forEach(key => {
          acc[`${key}_${index}`] = q.variables[key];
        });
        return acc;
      }, {})
    };
  }
}

Debouncing Mechanisms

Input Debouncing

javascript
class InputDebouncer {
  constructor(delay = 300) {
    this.delay = delay;
    this.timers = new Map();
  }

  debounce(key, callback, customDelay) {
    const delay = customDelay || this.delay;
    
    // Clear existing timer for this key
    if (this.timers.has(key)) {
      clearTimeout(this.timers.get(key));
    }

    // Set new timer
    const timer = setTimeout(() => {
      callback();
      this.timers.delete(key);
    }, delay);

    this.timers.set(key, timer);
  }

  // Immediate execution with trailing debounce
  debounceLeading(key, callback, customDelay) {
    const delay = customDelay || this.delay;
    
    if (!this.timers.has(key)) {
      // Execute immediately on first call
      callback();
      
      // Set timer to prevent subsequent calls
      const timer = setTimeout(() => {
        this.timers.delete(key);
      }, delay);
      
      this.timers.set(key, timer);
    }
  }

  // Cancel specific debounced function
  cancel(key) {
    if (this.timers.has(key)) {
      clearTimeout(this.timers.get(key));
      this.timers.delete(key);
    }
  }

  // Cancel all debounced functions
  cancelAll() {
    this.timers.forEach(timer => clearTimeout(timer));
    this.timers.clear();
  }
}

// Search input debouncing example
class SearchDebouncer {
  constructor(searchFunction) {
    this.debouncer = new InputDebouncer(300);
    this.searchFunction = searchFunction;
    this.lastQuery = '';
  }

  search(query) {
    if (query === this.lastQuery) return;
    
    this.lastQuery = query;
    
    if (query.trim() === '') {
      this.debouncer.cancel('search');
      this.clearResults();
      return;
    }

    this.debouncer.debounce('search', () => {
      this.executeSearch(query);
    });
  }

  async executeSearch(query) {
    try {
      const results = await this.searchFunction(query);
      this.displayResults(results);
    } catch (error) {
      console.error('Search failed:', error);
      this.displayError(error);
    }
  }

  displayResults(results) {
    // Update UI with search results
    console.log('Search results:', results);
  }

  displayError(error) {
    // Show error message
    console.error('Search error:', error);
  }

  clearResults() {
    // Clear search results
    console.log('Clearing search results');
  }
}

API Call Debouncing

javascript
class APIDebouncer {
  constructor() {
    this.pendingCalls = new Map();
    this.debounceDelay = 250;
  }

  async debouncedCall(endpoint, options = {}, customDelay) {
    const delay = customDelay || this.debounceDelay;
    const callKey = this.generateCallKey(endpoint, options);

    // Cancel existing call
    if (this.pendingCalls.has(callKey)) {
      this.pendingCalls.get(callKey).cancel();
    }

    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(async () => {
        try {
          const result = await this.makeCall(endpoint, options);
          this.pendingCalls.delete(callKey);
          resolve(result);
        } catch (error) {
          this.pendingCalls.delete(callKey);
          reject(error);
        }
      }, delay);

      this.pendingCalls.set(callKey, {
        timeoutId,
        cancel: () => {
          clearTimeout(timeoutId);
          this.pendingCalls.delete(callKey);
          reject(new Error('Debounced call cancelled'));
        }
      });
    });
  }

  generateCallKey(endpoint, options) {
    // Create unique key based on endpoint and relevant options
    const keyData = {
      endpoint,
      method: options.method || 'GET',
      params: options.params || {},
      // Don't include things like timestamps in key
    };
    
    return JSON.stringify(keyData);
  }

  async makeCall(endpoint, options) {
    const response = await fetch(endpoint, {
      method: options.method || 'GET',
      headers: options.headers || {},
      body: options.body ? JSON.stringify(options.body) : null
    });

    if (!response.ok) {
      throw new Error(`API call failed: ${response.statusText}`);
    }

    return await response.json();
  }

  // Throttling variant - limits call frequency
  async throttledCall(endpoint, options = {}, throttleMs = 1000) {
    const callKey = this.generateCallKey(endpoint, options);
    const now = Date.now();
    
    const lastCall = this.lastCallTimes?.get(callKey) || 0;
    
    if (now - lastCall < throttleMs) {
      // Return cached result or throw error
      throw new Error('Call throttled');
    }

    if (!this.lastCallTimes) {
      this.lastCallTimes = new Map();
    }
    
    this.lastCallTimes.set(callKey, now);
    
    return await this.makeCall(endpoint, options);
  }
}

// Usage example
const apiDebouncer = new APIDebouncer();

// User types in search box
document.getElementById('search').addEventListener('input', async (e) => {
  const query = e.target.value;
  
  if (query.length < 2) return;
  
  try {
    const results = await apiDebouncer.debouncedCall('/api/search', {
      method: 'GET',
      params: { q: query }
    });
    
    displaySearchResults(results);
  } catch (error) {
    if (error.message !== 'Debounced call cancelled') {
      console.error('Search failed:', error);
    }
  }
});

Smart Optimization

Adaptive Batching

javascript
class AdaptiveBatcher {
  constructor() {
    this.metrics = {
      batchSizes: [],
      responseTimes: [],
      successRates: []
    };
    this.currentConfig = {
      batchSize: 10,
      timeout: 100
    };
    this.learningEnabled = true;
  }

  async addRequest(request) {
    const startTime = Date.now();
    
    try {
      const result = await this.batcher.addRequest(request);
      
      if (this.learningEnabled) {
        this.recordSuccess(Date.now() - startTime);
      }
      
      return result;
    } catch (error) {
      if (this.learningEnabled) {
        this.recordFailure(Date.now() - startTime);
      }
      throw error;
    }
  }

  recordSuccess(responseTime) {
    this.metrics.responseTimes.push(responseTime);
    this.metrics.successRates.push(1);
    this.metrics.batchSizes.push(this.currentConfig.batchSize);
    
    this.adaptConfiguration();
  }

  recordFailure(responseTime) {
    this.metrics.responseTimes.push(responseTime);
    this.metrics.successRates.push(0);
    this.metrics.batchSizes.push(this.currentConfig.batchSize);
    
    this.adaptConfiguration();
  }

  adaptConfiguration() {
    // Only adapt after collecting enough data
    if (this.metrics.responseTimes.length < 10) return;

    const recent = this.getRecentMetrics(20);
    const avgResponseTime = this.average(recent.responseTimes);
    const successRate = this.average(recent.successRates);
    
    // If performance is poor, reduce batch size
    if (avgResponseTime > 2000 || successRate < 0.9) {
      this.currentConfig.batchSize = Math.max(1, this.currentConfig.batchSize - 1);
      this.currentConfig.timeout = Math.min(1000, this.currentConfig.timeout + 50);
    }
    // If performance is good, try to increase efficiency
    else if (avgResponseTime < 500 && successRate > 0.95) {
      this.currentConfig.batchSize = Math.min(50, this.currentConfig.batchSize + 1);
      this.currentConfig.timeout = Math.max(50, this.currentConfig.timeout - 10);
    }

    // Update batcher configuration
    this.updateBatcher();
  }

  getRecentMetrics(count) {
    const sliceStart = Math.max(0, this.metrics.responseTimes.length - count);
    
    return {
      responseTimes: this.metrics.responseTimes.slice(sliceStart),
      successRates: this.metrics.successRates.slice(sliceStart),
      batchSizes: this.metrics.batchSizes.slice(sliceStart)
    };
  }

  average(arr) {
    return arr.reduce((sum, val) => sum + val, 0) / arr.length;
  }

  updateBatcher() {
    this.batcher = new RequestBatcher({
      batchSize: this.currentConfig.batchSize,
      batchTimeout: this.currentConfig.timeout
    });
  }
}

Network-Aware Optimization

javascript
class NetworkAwareOptimizer {
  constructor() {
    this.networkInfo = this.getNetworkInfo();
    this.adaptToNetwork();
    
    // Listen for network changes
    if ('connection' in navigator) {
      navigator.connection.addEventListener('change', () => {
        this.networkInfo = this.getNetworkInfo();
        this.adaptToNetwork();
      });
    }
  }

  getNetworkInfo() {
    if ('connection' in navigator) {
      const conn = navigator.connection;
      return {
        effectiveType: conn.effectiveType,
        downlink: conn.downlink,
        rtt: conn.rtt,
        saveData: conn.saveData
      };
    }
    return { effectiveType: '4g' }; // default
  }

  adaptToNetwork() {
    const config = this.getOptimalConfig();
    
    // Update batching configuration
    this.batchConfig = config.batching;
    this.debounceConfig = config.debouncing;
    
    console.log('Network adapted:', {
      network: this.networkInfo.effectiveType,
      config: config
    });
  }

  getOptimalConfig() {
    switch (this.networkInfo.effectiveType) {
      case 'slow-2g':
      case '2g':
        return {
          batching: { batchSize: 3, timeout: 1000 },
          debouncing: { delay: 800 }
        };
      
      case '3g':
        return {
          batching: { batchSize: 5, timeout: 500 },
          debouncing: { delay: 400 }
        };
      
      case '4g':
        return {
          batching: { batchSize: 10, timeout: 200 },
          debouncing: { delay: 200 }
        };
      
      case '5g':
        return {
          batching: { batchSize: 20, timeout: 50 },
          debouncing: { delay: 100 }
        };
      
      default:
        return {
          batching: { batchSize: 10, timeout: 200 },
          debouncing: { delay: 300 }
        };
    }
  }

  // Create optimized batcher based on network
  createOptimizedBatcher() {
    return new RequestBatcher(this.batchConfig);
  }

  // Create optimized debouncer based on network
  createOptimizedDebouncer() {
    return new InputDebouncer(this.debounceConfig.delay);
  }
}

Platform-Specific Optimizations

Android Optimizations

kotlin
class AndroidBatchingOptimizer(private val context: Context) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
    
    fun getOptimalBatchConfiguration(): BatchConfiguration {
        val networkInfo = connectivityManager.activeNetworkInfo
        val isLowPowerMode = powerManager.isPowerSaveMode
        val isMeteredConnection = connectivityManager.isActiveNetworkMetered
        
        return when {
            isLowPowerMode -> {
                // Conservative batching to save battery
                BatchConfiguration(
                    batchSize = 20,
                    timeout = 2000,
                    priority = "battery"
                )
            }
            isMeteredConnection -> {
                // Aggressive batching to save data
                BatchConfiguration(
                    batchSize = 15,
                    timeout = 1000,
                    priority = "data"
                )
            }
            networkInfo?.type == ConnectivityManager.TYPE_WIFI -> {
                // Optimal performance on WiFi
                BatchConfiguration(
                    batchSize = 5,
                    timeout = 100,
                    priority = "performance"
                )
            }
            else -> {
                // Default mobile configuration
                BatchConfiguration(
                    batchSize = 10,
                    timeout = 300,
                    priority = "balanced"
                )
            }
        }
    }
    
    fun scheduleOptimalBatching() {
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
            
        val batchingWork = OneTimeWorkRequestBuilder<BatchingWorker>()
            .setConstraints(constraints)
            .build()
            
        WorkManager.getInstance(context).enqueue(batchingWork)
    }
}

data class BatchConfiguration(
    val batchSize: Int,
    val timeout: Long,
    val priority: String
)

iOS Optimizations

swift
class iOSBatchingOptimizer {
    private let reachability = SCNetworkReachability.forInternet()
    
    func getOptimalBatchConfiguration() -> BatchConfiguration {
        let networkStatus = getCurrentNetworkStatus()
        let batteryLevel = UIDevice.current.batteryLevel
        let isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
        
        switch (networkStatus, isLowPowerMode) {
        case (.wifi, false):
            return BatchConfiguration(
                batchSize: 5,
                timeout: 0.1,
                priority: .performance
            )
            
        case (.cellular, _) where batteryLevel < 0.2:
            return BatchConfiguration(
                batchSize: 20,
                timeout: 2.0,
                priority: .battery
            )
            
        case (.cellular, true):
            return BatchConfiguration(
                batchSize: 15,
                timeout: 1.0,
                priority: .efficiency
            )
            
        default:
            return BatchConfiguration(
                batchSize: 10,
                timeout: 0.3,
                priority: .balanced
            )
        }
    }
    
    private func getCurrentNetworkStatus() -> NetworkStatus {
        // Implementation to detect current network
        // Using Network framework or Reachability
        return .wifi // placeholder
    }
}

struct BatchConfiguration {
    let batchSize: Int
    let timeout: TimeInterval
    let priority: Priority
    
    enum Priority {
        case performance, battery, efficiency, balanced
    }
}

enum NetworkStatus {
    case wifi, cellular, none
}

These batching and debouncing strategies provide robust foundation for efficient mobile networking, ensuring optimal performance across different network conditions and device capabilities.

Created by Eren Demir.