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 clamscan process 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): clamd loads 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.
For any deployment expecting more than a handful of uploads per minute, the clamd daemon mode is strongly recommended. Use pompelmi's { 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