Custom Serializers
By default, isolated-workers uses JSON for message serialization. You can provide a custom serializer to use binary formats like MessagePack, add compression, or implement encryption.
The Serializer Interface
Create a custom serializer by extending the Serializer base class:
import { Serializer } from 'isolated-workers';
class MySerializer extends Serializer {
serialize<T>(data: T): string | Uint8Array {
// Convert data to string or bytes
}
deserialize<T>(input: string | Uint8Array): T {
// Parse string or bytes back to data
}
// Required: custom message terminator
// JsonSerializer uses '\n' by default
terminator = '\n';
}
> **Note**: Terminator can also be `Uint8Array` for binary protocols.Example: Verbose JSON Serializer
Here's a serializer that wraps messages with metadata for debugging:
/**
* A verbose JSON serializer that adds metadata to each message.
* This is for demonstration - in production you might use a more
* efficient binary format.
*/
export class VerboseJsonSerializer extends Serializer {
private static messageCount = 0;
serialize<T>(data: T): string {
VerboseJsonSerializer.messageCount++;
const wrapped = {
_serializer: 'VerboseJsonSerializer',
_messageId: VerboseJsonSerializer.messageCount,
_timestamp: Date.now(),
data,
};
return JSON.stringify(wrapped);
}
deserialize<T>(input: string | Uint8Array): T {
const str =
typeof input === 'string' ? input : new TextDecoder().decode(input);
const wrapped = JSON.parse(str) as {
_serializer: string;
_messageId: number;
_timestamp: number;
data: T;
};
// Log metadata (in real code, you might use this for debugging/tracing)
console.log(
`[SERIALIZER] Message #${wrapped._messageId} from ${wrapped._serializer}`
);
return wrapped.data;
}
// Custom terminator - using double newline to avoid conflicts
terminator = '\n\n';
}Using Custom Serializers
Both host and worker must use the same serializer. Pass the serializer instance to both.
On the host:
const worker = await createWorker<Messages>({
script: join(__dirname, 'worker.ts'),
timeout: 10000,
serializer: verboseSerializer,
});On the worker:
const server = await startWorkerServer(handlers, {
serializer: verboseSerializer,
});Use Cases
Binary Formats (MessagePack, Protocol Buffers)
For better performance with large payloads:
import { encode, decode } from '@msgpack/msgpack';
class MessagePackSerializer extends Serializer {
serialize<T>(data: T): Uint8Array {
return encode(data);
}
deserialize<T>(input: string | Uint8Array): T {
const bytes =
typeof input === 'string' ? new TextEncoder().encode(input) : input;
return decode(bytes) as T;
}
}Compression
For bandwidth-sensitive applications:
import { gzipSync, gunzipSync } from 'zlib';
class CompressedSerializer extends Serializer {
serialize<T>(data: T): Uint8Array {
const json = JSON.stringify(data);
return gzipSync(json);
}
deserialize<T>(input: string | Uint8Array): T {
const bytes =
typeof input === 'string' ? new TextEncoder().encode(input) : input;
const json = gunzipSync(bytes).toString();
return JSON.parse(json) as T;
}
}Encryption
For sensitive data crossing process boundaries:
import crypto from 'crypto';
class EncryptedSerializer extends Serializer {
constructor(private key: Buffer, private iv: Buffer) {
super();
}
serialize<T>(data: T): string {
const json = JSON.stringify(data);
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv);
return cipher.update(json, 'utf8', 'base64') + cipher.final('base64');
}
deserialize<T>(input: string | Uint8Array): T {
const str =
typeof input === 'string' ? input : new TextDecoder().decode(input);
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv);
const json =
decipher.update(str, 'base64', 'utf8') + decipher.final('utf8');
return JSON.parse(json) as T;
}
}Custom Terminators
Messages are delimited by a terminator string. The default is '\n' (newline). If your serialized data might contain newlines, use a different terminator:
class MySerializer extends Serializer {
// Use double newline as terminator
terminator = '\n\n';
// Or use a unique sequence unlikely to appear in data
terminator = '\x00\x00END\x00\x00';
}Serializer Mismatch Detection
isolated-workers detects when host and worker use different serializers. Name your serializer class to enable this:
// Good - named class enables mismatch detection
export class VerboseJsonSerializer extends Serializer { ... }
// Bad - anonymous class won't be detected properly
export const serializer = new (class extends Serializer { ... })();Best Practices
- Create singleton instances - Serializers can maintain state (like message counters)
- Name your classes - Enables serializer mismatch detection
- Handle both string and Uint8Array - The
deserializemethod receives either - Keep serialization fast - It runs on every message
- Test with your actual payloads - Ensure your data survives round-trip serialization
See Also
- {% example-link custom-serializer %} - Complete custom serializer example
- Error Handling - How errors are serialized