ClamAV in Node.js: pompelmi vs clamscan vs node-clam
There are three actively maintained npm packages that wrap ClamAV for Node.js. Each takes a fundamentally different architectural approach. Choosing the wrong one for your use case means working around its assumptions rather than with them. This article compares all three with working code examples.
pompelmi
pompelmi is a minimal wrapper around the clamscan binary.
It maps ClamAV exit codes directly to Symbol-based verdicts and has zero
runtime dependencies.
const { scan, Verdict } = require('pompelmi');
const verdict = await scan('/tmp/upload.pdf');
switch (verdict) {
case Verdict.Clean:
console.log('File is clean');
break;
case Verdict.Malicious:
throw new Error('Malware detected');
case Verdict.ScanError:
console.warn('Scan could not complete');
break;
}
Architecture: spawns one clamscan process per
file, reads the exit code, maps it to a Symbol. No parsing of stdout.
No daemon. No socket management.
Typed verdicts: Verdict.Clean,
Verdict.Malicious, and Verdict.ScanError are
Symbol constants — strict equality comparison, no string typos possible.
TCP/clamd support: pass { host, port }
to connect to a remote clamd instance for production deployments.
// Remote clamd (e.g. Docker sidecar)
const verdict = await scan('/tmp/upload.pdf', {
host: 'clamav',
port: 3310
});
clamscan
clamscan (npm) is a feature-rich wrapper that supports both
the clamscan binary and the clamd daemon via a
unified API. It is configured through a large options object.
const NodeClam = require('clamscan');
const clamScan = await new NodeClam().init({
clamscan: {
path: '/usr/bin/clamscan',
active: true
},
preference: 'clamscan'
});
const { isInfected, file, viruses } = await clamScan.isInfected('/tmp/upload.pdf');
if (isInfected) {
console.error('Viruses found:', viruses);
} else {
console.log('Clean:', file);
}
Architecture: spawns clamscan or connects
to clamd depending on configuration. Parses stdout to extract
virus names. Returns a structured object with isInfected and
viruses fields.
Virus name extraction: clamscan parses the
raw stdout output of clamscan to return the detected virus name(s)
as a string array. This is useful for logging but depends on the stability of
ClamAV's output format.
Stream scanning: supports scanning a readable stream directly by sending it to clamd via the INSTREAM protocol.
const stream = fs.createReadStream('/tmp/upload.pdf');
const { isInfected } = await clamScan.scanStream(stream);
node-clam
node-clam is focused exclusively on communication with the
clamd daemon. It does not use the clamscan binary
at all. You must have clamd running and accessible.
const NodeClam = require('node-clam');
const clam = await new NodeClam().init({
clamdscan: {
socket: false,
host: '127.0.0.1',
port: 3310,
timeout: 60000,
active: true
}
});
const { isInfected, viruses } = await clam.isInfected('/tmp/upload.pdf');
if (isInfected) {
console.error('Infected with:', viruses.join(', '));
}
Architecture: sends the file path to clamd over a TCP socket. clamd reads the file from the filesystem and responds with a verdict. Requires both clamd to be running and the scanned file to be accessible from clamd's process.
Stream support via INSTREAM: can send a buffer or stream directly to clamd without writing a temp file, using the INSTREAM protocol:
const { isInfected } = await clam.scanBuffer(buffer);
Side-by-side comparison
| Feature | pompelmi | clamscan (npm) | node-clam |
|---|---|---|---|
| Runtime dependencies | Zero | Several | Few |
| Requires clamd daemon | No (optional) | No (optional) | Yes (required) |
| Verdict type | Symbol (typed) | Boolean + string[] | Boolean + string[] |
| Virus name extraction | No | Yes (stdout parse) | Yes (clamd response) |
| Stream / buffer scanning | No (file path only) | Yes (via clamd) | Yes (INSTREAM) |
| TCP / remote clamd | Yes | Yes | Yes (primary mode) |
| stdout parsing | No (exit codes only) | Yes | No (protocol response) |
| TypeScript types | Yes (bundled) | Partial (@types) | Yes (bundled) |
| Active maintenance | Yes | Intermittent | Yes |
Performance considerations
All three packages ultimately ask ClamAV to scan a file. The performance difference is mostly about startup overhead, not scanning speed.
-
clamscan-mode (pompelmi, clamscan npm): each scan spawns a
new
clamscanprocess that loads the full virus database (~200 MB) into memory before scanning. On a modern server this takes 1–3 seconds per file. Scanning many files in parallel spawns many processes simultaneously, which can cause memory pressure. -
clamd-mode (node-clam, pompelmi TCP, clamscan npm clamd mode):
clamdloads the database once and keeps it in memory. Each scan is a TCP round-trip, typically 10–100 ms for small files. Suitable for production upload endpoints that scan hundreds of files per minute.
{ host, port } option or node-clam to connect to a shared
clamd instance rather than spawning a new clamscan process
per upload.
Decision guide
| Situation | Recommendation |
|---|---|
| Small project, infrequent uploads, minimal deps | pompelmi — zero deps, simple API, works without clamd |
| Need detected virus name for logging or alerting | clamscan (npm) — returns virus name from stdout |
| Production, high upload volume, clamd already running | node-clam or pompelmi with TCP option |
| Scan a stream or buffer without a temp file | node-clam — INSTREAM protocol support |
| Containerized with clamd sidecar | pompelmi ({ host, port }) or node-clam |
| TypeScript, want compile-time safety on verdict comparisons | pompelmi — Symbol verdicts prevent string comparison bugs |