@robiscoding/orpc-worker-pool
v0.1.1
Published
oRPC plugin for offloading CPU-intensive procedures to a managed Node.js worker thread pool
Readme
oRPC Worker Pool
An oRPC plugin that offloads CPU-intensive procedures to a managed pool of Node.js worker threads, keeping your main thread responsive.
Use Cases
Node.js runs JavaScript on a single thread. Any procedure that does heavy computation — image processing, data parsing, cryptography, report generation — will block the event loop and stall all other requests while it runs.
orpc-worker-pool solves this by routing marked procedures to a pool of worker threads that run in parallel. The main thread stays free to handle incoming requests while workers churn through the expensive work.
Good candidates for offloading:
- Image/video processing and transformation
- Large file parsing (CSV, XML, JSON)
- Cryptographic operations (hashing, encryption)
- Report generation or PDF rendering
- Any tight loop or CPU-bound algorithm
Requirements
- Node.js >= 22
@orpc/server^1.13.0 (peer dependency)
Installation
npm install @robiscoding/orpc-worker-pool
# or
pnpm add @robiscoding/orpc-worker-poolGetting Started
1. Mark procedures for offloading
Add meta: { offload: true } to any procedure you want to run in a worker thread:
// router.ts
import { os } from '@orpc/server'
export const router = {
hashPassword: os
.meta({ offload: true }) // set offload to true to run in worker thread
.input(z.object({ password: z.string() }))
.handler(async ({ input }) => {
// This runs in a worker thread
return expensiveHash(input.password)
}),
greet: os
.input(z.object({ name: z.string() }))
.handler(async ({ input }) => {
// No offload meta — runs on the main thread as normal
return `Hello, ${input.name}`
}),
}2. Register the plugin
Pass a WorkerOffloadPlugin instance to your oRPC server. The routerPath must point to the compiled JS file that exports your router. Worker threads load it fresh each time.
// server.ts
import { createServer } from '@orpc/server'
import { WorkerOffloadPlugin } from '@robiscoding/orpc-worker-pool'
import { router } from './router'
import { fileURLToPath } from 'url'
import path from 'path'
const plugin = new WorkerOffloadPlugin({
routerPath: path.join(__dirname, 'router.js'), // path to the compiled router
pool: 4, // number of worker threads (default: 4)
queueLimit: 100, // max queued tasks before rejecting (default: 100); useful for adding backpressure
})
const handler = createServer(router, {
plugins: [plugin],
})
// Shut down workers gracefully on exit
process.on('SIGTERM', () => plugin.terminate())Usage Guide
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
| routerPath | string | required | Absolute path to the compiled JS file exporting your router |
| pool | number | 4 | Number of worker threads to spawn |
| queueLimit | number | 100 | Max number of tasks that can be queued when all workers are busy |
Error Handling
Errors thrown inside a worker are serialized across the thread boundary and wrapped in WorkerOffloadPluginError. Because postMessage uses structured clone, custom error properties are preserved via the .defined and .code fields.
- If the procedure threw an
ORPCError,.definedistrueand.codeholds the error code. - Otherwise
.definedisfalseand only the message is preserved.
import { WorkerOffloadPluginError } from '@robiscoding/orpc-worker-pool'
try {
await client.hashPassword({ password: '...' })
} catch (err) {
if (err instanceof WorkerOffloadPluginError) {
if (err.defined) {
// A known ORPCError — err.code has the oRPC error code
console.error('Procedure error:', err.code, err.message)
} else {
// An unexpected error from the worker
console.error('Worker error:', err.message)
}
}
}Graceful Shutdown
Call plugin.terminate() before your process exits to drain in-flight tasks and shut down worker threads cleanly:
process.on('SIGTERM', async () => {
await plugin.terminate()
process.exit(0)
})Any tasks still queued or in-flight when terminate() is called are immediately rejected with a WorkerOffloadPluginError.
License
MIT
