Building a Distributed Print System with Node.js and WebSockets
# Building a Distributed Print System with Node.js and WebSockets
Enterprise label printing demands reliability at scale. This architecture handles thousands of print jobs across multiple locations with automatic printer discovery and intelligent failover.
## System Overview
The print system consists of three layers:
- API Gateway - Receives print requests
- Job Orchestrator - Routes and manages jobs
- Print Workers - Execute actual printing
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
│ Client │────▶│ Gateway │────▶│ Orchestrator│
└─────────────┘ └───────────────┘ └──────────────┘
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Worker 1 │ │ Worker 2 │ │ Worker N │
│ (Warehouse) │ │ (Shipping) │ │ (Office) │
└──────────────┘ └──────────────┘ └──────────────┘## Auto-Discovery Protocol
Workers announce themselves on startup via UDP broadcast:
const dgram = require('dgram');
const socket = dgram.createSocket('udp4');
function announceWorker(workerId, capabilities) {
const message = Buffer.from(JSON.stringify({
type: 'WORKERANNOUNCE',
workerId,
capabilities,
timestamp: Date.now()
}));
socket.setBroadcast(true);
socket.send(message, 0, message.length, 5000, '255.255.255.255');
}
### Discovery Response
The orchestrator listens for announcements and maintains a registry:
const workers = new Map();
socket.on('message', (msg, rinfo) => {
const data = JSON.parse(msg.toString());
if (data.type === 'WORKERANNOUNCE') {
workers.set(data.workerId, {
...data,
address: rinfo.address,
lastSeen: Date.now()
});
}
});
## WebSocket Communication
Real-time job dispatch uses WebSockets for bidirectional communication:
const WebSocket = require('ws');
class PrintWorker {
constructor(orchestratorUrl, workerId) {
this.ws = new WebSocket(orchestratorUrl);
this.workerId = workerId;
this.activeJobs = new Map();
this.ws.on('message', this.handleMessage.bind(this));
}
handleMessage(data) {
const message = JSON.parse(data);
switch (message.type) {
case 'JOBASSIGN':
this.processJob(message.job);
break;
case 'JOBCANCEL':
this.cancelJob(message.jobId);
break;
case 'STATUS_REQUEST':
this.reportStatus();
break;
}
}
}
## Smart Retry Logic
Print failures are inevitable. The retry system handles them gracefully:
| Failure Type | Retry Strategy | Max Attempts |
|---|---|---|
| Network Timeout | Exponential backoff | 5 |
| Paper Jam | Immediate retry | 3 |
| Offline Printer | Route to backup | 1 |
| Invalid Data | No retry (fail fast) | 0 |
async function executeWithRetry(job, options = {}) {
const { maxAttempts = 3, backoffMs = 1000 } = options;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await executePrintJob(job);
} catch (error) {
if (!isRetryable(error) || attempt === maxAttempts) {
throw error;
}
const delay = backoffMs * Math.pow(2, attempt - 1);
await sleep(delay);
}
}
}
## Brother Raster Protocol Integration
Label printing uses the Brother raster protocol for thermal printers:
const net = require('net');
function sendLabelToPrinter(printerIp, labelData) {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
socket.connect(9100, printerIp, () => {
// Initialize printer
socket.write(Buffer.from([0x1B, 0x40])); // ESC @
// Set media type
socket.write(Buffer.from([0x1B, 0x69, 0x7A, ...]));
// Send raster data
socket.write(labelData);
// End job
socket.write(Buffer.from([0x1A]));
socket.end();
});
socket.on('close', resolve);
socket.on('error', reject);
});
}
## Monitoring and Metrics
Every component emits metrics for observability:
- >Job Latency - Time from submission to completion
- >Queue Depth - Pending jobs per worker
- >Error Rates - Failures by type and printer
- >Throughput - Jobs per minute per location
## Conclusion
Distributed printing requires careful orchestration of discovery, routing, and failure handling. This event-driven architecture scales horizontally while maintaining reliability through smart retry logic and real-time communication.