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);