JSON-RPC Batch Requests Example

This example demonstrates how to send multiple JSON-RPC calls in a single HTTP request to optimize performance and reduce network overhead. Learn about batch processing patterns and implementation strategies.

What You'll Learn

  • How to structure batch requests according to JSON-RPC 2.0 specification
  • Performance benefits and use cases for batch processing
  • Handling partial failures in batch operations
  • Client-side implementation strategies
  • Server-side batch processing patterns

JSON-RPC Batch Requests

Master the art of batching multiple JSON-RPC calls to optimize network performance and reduce latency.

What are Batch Requests?

A batch request allows you to send multiple JSON-RPC calls in a single HTTP request. The server processes all calls and returns an array of responses. This significantly reduces network overhead and improves performance when making multiple related API calls.

Benefits

  • Reduced network latency
  • Lower connection overhead
  • Atomic transaction support
  • Better resource utilization
  • Simplified error handling

Use Cases

  • Fetching related data
  • Bulk operations
  • Form submissions with validation
  • Dashboard data loading
  • Migration and sync operations

Batch Request Structure

A batch request is simply an array of individual JSON-RPC request objects sent as the HTTP body.

Individual Requests

// Request 1
POST /jsonrpc
{
  "jsonrpc": "2.0",
  "method": "getUser",
  "params": {"id": 1},
  "id": 1
}

// Request 2  
POST /jsonrpc
{
  "jsonrpc": "2.0",
  "method": "getOrders", 
  "params": {"userId": 1},
  "id": 2
}

2 separate HTTP requests

Batch Request

// Single batch request
POST /jsonrpc
[
  {
    "jsonrpc": "2.0",
    "method": "getUser",
    "params": {"id": 1},
    "id": 1
  },
  {
    "jsonrpc": "2.0", 
    "method": "getOrders",
    "params": {"userId": 1},
    "id": 2
  }
]

1 HTTP request with multiple calls

Batch Response Handling

The server returns an array of responses corresponding to each request in the batch. Responses may not be in the same order as requests, so use IDs to match them.

Example Batch Response

[
  {
    "jsonrpc": "2.0",
    "result": {
      "id": 1,
      "name": "Alice",
      "email": "[email protected]"
    },
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "result": [
      {"orderId": 101, "amount": 299.99},
      {"orderId": 102, "amount": 149.50}
    ],
    "id": 2
  }
]

Handling Partial Failures

[
  {
    "jsonrpc": "2.0",
    "result": {
      "id": 1,
      "name": "Alice",
      "email": "[email protected]"  
    },
    "id": 1
  },
  {
    "jsonrpc": "2.0",
    "error": {
      "code": -32001,
      "message": "User not found",
      "data": {"userId": 999}
    },
    "id": 2
  }
]

Client Implementation

JavaScript Batch Client

class JsonRpcBatchClient {
  constructor(url) {
    this.url = url;
    this.requestId = 1;
    this.pendingBatch = [];
  }

  // Add request to current batch
  add(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      id: this.requestId++
    };
    
    if (params !== undefined) {
      request.params = params;
    }
    
    this.pendingBatch.push(request);
    return request.id; // Return ID for tracking
  }

  // Add notification to current batch
  addNotification(method, params) {
    const notification = {
      jsonrpc: '2.0',
      method
    };
    
    if (params !== undefined) {
      notification.params = params;
    }
    
    this.pendingBatch.push(notification);
  }

  // Execute the batch and return results
  async execute() {
    if (this.pendingBatch.length === 0) {
      throw new Error('No requests in batch');
    }

    const batch = [...this.pendingBatch];
    this.pendingBatch = []; // Clear for next batch

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

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      // Handle empty response (all notifications)
      if (response.status === 204) {
        return [];
      }

      const results = await response.json();
      
      // Convert array response to map for easier access
      const resultMap = new Map();
      
      if (Array.isArray(results)) {
        results.forEach(result => {
          if (result.id !== undefined) {
            resultMap.set(result.id, result);
          }
        });
      }

      return resultMap;
    } catch (error) {
      // Re-add requests to batch on network error
      this.pendingBatch = [...batch, ...this.pendingBatch];
      throw error;
    }
  }

  // Execute batch and extract specific result
  async executeAndGet(requestId) {
    const results = await this.execute();
    const result = results.get(requestId);
    
    if (!result) {
      throw new Error(`No result found for request ID ${requestId}`);
    }
    
    if (result.error) {
      const error = new Error(result.error.message);
      error.code = result.error.code;
      error.data = result.error.data;
      throw error;
    }
    
    return result.result;
  }

  // Convenience method: execute and return all successful results
  async executeAll() {
    const results = await this.execute();
    const successResults = [];
    const errors = [];
    
    for (const [id, result] of results) {
      if (result.error) {
        errors.push({ id, error: result.error });
      } else {
        successResults.push({ id, result: result.result });
      }
    }
    
    return { successes: successResults, errors };
  }

  // Get current batch size
  size() {
    return this.pendingBatch.length;
  }

  // Clear current batch
  clear() {
    this.pendingBatch = [];
  }
}

// Usage examples
async function batchExamples() {
  const client = new JsonRpcBatchClient('http://localhost:3000/jsonrpc');

  console.log('=== Basic Batch Usage ===');
  
  // Add multiple requests to batch
  const userReqId = client.add('getUser', { id: 1 });
  const ordersReqId = client.add('getOrders', { userId: 1 });
  const settingsReqId = client.add('getUserSettings', { userId: 1 });
  
  // Add a notification
  client.addNotification('logEvent', { 
    type: 'dashboard_view', 
    userId: 1 
  });

  // Execute batch
  const results = await client.execute();
  
  console.log('User data:', results.get(userReqId)?.result);
  console.log('Orders:', results.get(ordersReqId)?.result);
  console.log('Settings:', results.get(settingsReqId)?.result);

  console.log('\n=== Batch with Error Handling ===');
  
  // Create new batch with some invalid requests
  client.add('getUser', { id: 1 });
  client.add('getUser', { id: 999 }); // This will fail
  client.add('getTime');
  
  const { successes, errors } = await client.executeAll();
  
  console.log('Successful results:', successes);
  console.log('Errors:', errors);

  console.log('\n=== Performance Comparison ===');
  
  // Measure individual requests
  const startIndividual = Date.now();
  try {
    const singleClient = new JsonRpcClient('http://localhost:3000/jsonrpc');
    await Promise.all([
      singleClient.call('add', [1, 1]),
      singleClient.call('add', [2, 2]),
      singleClient.call('add', [3, 3]),
      singleClient.call('add', [4, 4]),
      singleClient.call('add', [5, 5])
    ]);
  } catch (e) {}
  const individualTime = Date.now() - startIndividual;

  // Measure batch request
  const startBatch = Date.now();
  client.add('add', [1, 1]);
  client.add('add', [2, 2]);
  client.add('add', [3, 3]);
  client.add('add', [4, 4]);
  client.add('add', [5, 5]);
  await client.execute();
  const batchTime = Date.now() - startBatch;

  console.log(`Individual requests: ${individualTime}ms`);
  console.log(`Batch request: ${batchTime}ms`);
  console.log(`Performance improvement: ${((individualTime - batchTime) / individualTime * 100).toFixed(1)}%`);
}

Python Batch Client

import requests
import json
from typing import List, Dict, Any, Optional, Tuple

class JsonRpcBatchClient:
    def __init__(self, url: str):
        self.url = url
        self.request_id = 1
        self.pending_batch = []
        self.session = requests.Session()

    def add(self, method: str, params: Any = None) -> int:
        """Add a request to the current batch and return its ID"""
        request = {
            'jsonrpc': '2.0',
            'method': method,
            'id': self.request_id
        }
        
        if params is not None:
            request['params'] = params
        
        self.pending_batch.append(request)
        current_id = self.request_id
        self.request_id += 1
        return current_id

    def add_notification(self, method: str, params: Any = None) -> None:
        """Add a notification to the current batch"""
        notification = {
            'jsonrpc': '2.0',
            'method': method
        }
        
        if params is not None:
            notification['params'] = params
        
        self.pending_batch.append(notification)

    def execute(self) -> Dict[int, Dict]:
        """Execute the batch and return results mapped by request ID"""
        if not self.pending_batch:
            raise ValueError('No requests in batch')

        batch = self.pending_batch.copy()
        self.pending_batch.clear()

        try:
            response = self.session.post(
                self.url,
                json=batch,
                timeout=30
            )
            response.raise_for_status()

            # Handle empty response (all notifications)
            if response.status_code == 204:
                return {}

            results = response.json()
            
            # Convert to dict for easier access
            result_map = {}
            if isinstance(results, list):
                for result in results:
                    if 'id' in result:
                        result_map[result['id']] = result
            
            return result_map

        except requests.RequestException as e:
            # Re-add requests on network error
            self.pending_batch = batch + self.pending_batch
            raise e

    def execute_all(self) -> Tuple[List[Dict], List[Dict]]:
        """Execute batch and separate successes from errors"""
        results = self.execute()
        successes = []
        errors = []
        
        for request_id, result in results.items():
            if 'error' in result:
                errors.append({'id': request_id, 'error': result['error']})
            else:
                successes.append({'id': request_id, 'result': result['result']})
        
        return successes, errors

    def size(self) -> int:
        """Get current batch size"""
        return len(self.pending_batch)

    def clear(self) -> None:
        """Clear current batch"""
        self.pending_batch.clear()

# Usage example
def batch_example():
    client = JsonRpcBatchClient('http://localhost:3000/jsonrpc')
    
    print('=== Dashboard Data Loading ===')
    
    # Load all dashboard data in one request
    user_id = client.add('getUser', {'id': 1})
    orders_id = client.add('getRecentOrders', {'userId': 1, 'limit': 5})
    stats_id = client.add('getUserStats', {'userId': 1})
    notifications_id = client.add('getNotifications', {'userId': 1})
    
    # Add analytics notification
    client.add_notification('logEvent', {
        'type': 'dashboard_load',
        'userId': 1
    })
    
    results = client.execute()
    
    dashboard_data = {
        'user': results[user_id]['result'] if user_id in results else None,
        'orders': results[orders_id]['result'] if orders_id in results else None,
        'stats': results[stats_id]['result'] if stats_id in results else None,
        'notifications': results[notifications_id]['result'] if notifications_id in results else None
    }
    
    print('Dashboard data loaded:', dashboard_data)

if __name__ == '__main__':
    batch_example()

Server Implementation

Server-side batch processing involves parsing the batch array and processing each request individually.

Node.js Batch Server

const express = require('express');
const app = express();

app.use(express.json());

// Method handlers
const methods = {
  add: (params) => params[0] + params[1],
  subtract: (params) => params[0] - params[1],
  getUser: (params) => ({ id: params.id, name: 'User ' + params.id }),
  getOrders: (params) => [
    { orderId: 101, amount: 100 },
    { orderId: 102, amount: 200 }
  ]
};

// Batch request handler
app.post('/jsonrpc', async (req, res) => {
  try {
    const request = req.body;
    
    // Handle batch requests
    if (Array.isArray(request)) {
      const responses = await processBatchRequest(request);
      
      // If all requests were notifications, return 204
      if (responses.length === 0) {
        return res.status(204).end();
      }
      
      return res.json(responses);
    }
    
    // Handle single request
    const response = await processSingleRequest(request);
    if (response === null) {
      return res.status(204).end();
    }
    
    res.json(response);
    
  } catch (error) {
    console.error('Batch processing error:', error);
    res.status(500).json({
      jsonrpc: '2.0',
      error: { code: -32603, message: 'Internal error' },
      id: null
    });
  }
});

async function processBatchRequest(batch) {
  const responses = [];
  const promises = [];
  
  // Process requests in parallel
  for (const request of batch) {
    const promise = processSingleRequest(request)
      .then(response => {
        // Only add non-null responses (notifications return null)
        if (response !== null) {
          responses.push(response);
        }
      })
      .catch(error => {
        console.error('Error processing request:', error);
        responses.push({
          jsonrpc: '2.0',
          error: { code: -32603, message: 'Internal error' },
          id: request.id || null
        });
      });
    
    promises.push(promise);
  }
  
  // Wait for all requests to complete
  await Promise.all(promises);
  
  return responses;
}

async function processSingleRequest(request) {
  // Validate request format
  if (!request || request.jsonrpc !== '2.0' || !request.method) {
    return {
      jsonrpc: '2.0',
      error: { code: -32600, message: 'Invalid Request' },
      id: request?.id || null
    };
  }
  
  const { method, params, id } = request;
  const isNotification = id === undefined;
  
  try {
    // Check if method exists
    if (!methods[method]) {
      const error = {
        code: -32601,
        message: 'Method not found',
        data: { method, available: Object.keys(methods) }
      };
      return isNotification ? null : {
        jsonrpc: '2.0',
        error,
        id
      };
    }
    
    // Execute method
    const result = await methods[method](params);
    
    // Return response (null for notifications)
    return isNotification ? null : {
      jsonrpc: '2.0',
      result,
      id
    };
    
  } catch (error) {
    if (isNotification) {
      console.error(`Error in notification ${method}:`, error);
      return null;
    }
    
    return {
      jsonrpc: '2.0',
      error: {
        code: -32603,
        message: 'Internal error',
        data: process.env.NODE_ENV === 'development' ? error.message : undefined
      },
      id
    };
  }
}

app.listen(3000, () => {
  console.log('Batch-enabled JSON-RPC server running on port 3000');
});

Performance Optimization

Optimal Batch Size

Finding the right batch size depends on your specific use case, but here are some guidelines:

class OptimalBatchClient {
  constructor(url, options = {}) {
    this.url = url;
    this.maxBatchSize = options.maxBatchSize || 50;
    this.batchTimeout = options.batchTimeout || 100; // ms
    this.pendingBatch = [];
    this.batchTimer = null;
  }

  add(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      params,
      id: Date.now() + Math.random()
    };
    
    this.pendingBatch.push(request);
    
    // Auto-execute when batch is full
    if (this.pendingBatch.length >= this.maxBatchSize) {
      this.executeNow();
      return;
    }
    
    // Set timer for automatic execution
    this.scheduleBatchExecution();
    
    return request.id;
  }

  scheduleBatchExecution() {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
    }
    
    this.batchTimer = setTimeout(() => {
      if (this.pendingBatch.length > 0) {
        this.executeNow();
      }
    }, this.batchTimeout);
  }

  async executeNow() {
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
      this.batchTimer = null;
    }
    
    if (this.pendingBatch.length === 0) return [];
    
    const batch = [...this.pendingBatch];
    this.pendingBatch = [];
    
    // Execute batch request
    const response = await fetch(this.url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(batch)
    });
    
    return response.json();
  }
}

Connection Pooling

// Server-side connection pooling for database operations
const { Pool } = require('pg');

const dbPool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

const methods = {
  async getUsersBatch(params) {
    const userIds = params.userIds;
    
    // Single query to fetch multiple users
    const query = 'SELECT * FROM users WHERE id = ANY($1)';
    const result = await dbPool.query(query, [userIds]);
    
    // Return users mapped by ID
    const userMap = {};
    result.rows.forEach(user => {
      userMap[user.id] = user;
    });
    
    return userMap;
  },
  
  async getOrdersBatch(params) {
    const userIds = params.userIds;
    
    // Single query with JOIN to get all user orders
    const query = `
      SELECT o.*, u.name as user_name 
      FROM orders o 
      JOIN users u ON o.user_id = u.id 
      WHERE o.user_id = ANY($1)
      ORDER BY o.created_at DESC
    `;
    
    const result = await dbPool.query(query, [userIds]);
    
    // Group orders by user ID
    const ordersByUser = {};
    result.rows.forEach(order => {
      if (!ordersByUser[order.user_id]) {
        ordersByUser[order.user_id] = [];
      }
      ordersByUser[order.user_id].push(order);
    });
    
    return ordersByUser;
  }
};

Best Practices

✅ Do

  • Keep batch sizes reasonable (10-100 requests)
  • Use timeouts to prevent hanging requests
  • Handle partial failures gracefully
  • Implement request deduplication
  • Use connection pooling on the server
  • Consider implementing request prioritization
  • Monitor batch performance metrics

❌ Don't

  • Send extremely large batches (>1000 requests)
  • Include long-running operations in batches
  • Ignore error handling for individual requests
  • Assume responses are in the same order as requests
  • Use batches for real-time operations
  • Mix critical and non-critical operations in the same batch

Real-world Use Cases

Dashboard Loading

// Load all dashboard data at once
const client = new JsonRpcBatchClient('/api');

client.add('getUser', { id: userId });
client.add('getStats', { userId, period: '30d' });
client.add('getRecentActivity', { userId, limit: 10 });
client.add('getNotifications', { userId });
client.add('getTeamMembers', { userId });

const results = await client.executeAll();

// Update UI with all data
updateDashboard(results.successes);

Form Validation

// Validate all form fields at once
const client = new JsonRpcBatchClient('/api');

client.add('validateEmail', { email: formData.email });
client.add('checkUsernameAvailable', { username: formData.username });
client.add('validatePhone', { phone: formData.phone });
client.add('validateAddress', { address: formData.address });

const { successes, errors } = await client.executeAll();

// Show validation results
displayValidationResults(successes, errors);