Overview

pompelmi can connect to clamd in two ways: via TCP (host:port) or via a UNIX domain socket (socket path). Both use the ClamAV INSTREAM protocol: data is sent in 64 KB chunks, each prefixed with a 4-byte big-endian length. The stream is terminated by a four-zero-byte sequence (\x00\x00\x00\x00).

UNIX sockets have lower latency than TCP because there is no loopback networking overhead. TCP is more flexible for multi-container and remote setups. Both modes produce identical Verdict results.

TCP: Docker sidecar

The simplest production setup is running clamd as a Docker sidecar on the same host as your application.

docker-compose.yml
services:
  clamav:
    image: clamav/clamav:stable
    ports:
      - "3310:3310"
    volumes:
      - clamav_db:/var/lib/clamav   # definitions survive restarts
    healthcheck:
      test: ["CMD", "clamdcheck.sh"]
      interval: 60s
      retries: 3
      start_period: 120s            # first-boot download runway

  app:
    build: .
    depends_on:
      clamav:
        condition: service_healthy
    environment:
      CLAMAV_HOST: clamav
      CLAMAV_PORT: "3310"

volumes:
  clamav_db:
Application code
const { scan, Verdict } = require('pompelmi');

const result = await scan('/path/to/upload.zip', {
  host: process.env.CLAMAV_HOST || '127.0.0.1',
  port: Number(process.env.CLAMAV_PORT) || 3310,
});

if (result === Verdict.Malicious) { /* reject */ }

For quick local development without Compose:

docker run -d --name clamav -p 3310:3310 clamav/clamav:stable

First boot — expect 1–2 minutes

The very first time the ClamAV container starts, freshclam downloads the virus definition database (~300 MB). clamd will not accept scan requests until the download is complete.

The start_period: 120s in the healthcheck gives it that runway. During this window the container reports starting, not healthy.

Do not restart the container while it is still in starting state — that will cancel the download and force it to restart from scratch. If clamd refuses connections during this window, pompelmi will throw ECONNREFUSED. Use the healthcheck or a retry loop in your application startup path.
On every subsequent start the definitions load from the clamav_db named volume, so startup is fast (a few seconds). Only the very first cold start, or a start after wiping the volume, triggers the long download.

UNIX socket: local clamd daemon

If you run clamd as a system daemon (rather than in Docker), it exposes a UNIX socket at /run/clamav/clamd.sock by default.

Install on Debian/Ubuntu
sudo apt-get install -y clamav clamav-daemon
sudo freshclam          # download definitions
sudo systemctl enable --now clamav-daemon
Application code
const { scan, Verdict } = require('pompelmi');

const result = await scan('/path/to/upload.pdf', {
  socket: '/run/clamav/clamd.sock',
});

if (result === Verdict.Clean) {
  // safe to proceed
}
The socket path can vary by distribution. Check clamd.conf (usually /etc/clamav/clamd.conf) for the LocalSocket directive to confirm the path on your system.

UNIX socket: Docker with socket mount

You can share a UNIX socket between the ClamAV container and your app container using a named volume that holds only the socket file.

docker-compose.yml
services:
  clamav:
    image: clamav/clamav:stable
    volumes:
      - clamav_db:/var/lib/clamav
      - clamav_socket:/run/clamav     # share socket directory
    healthcheck:
      test: ["CMD", "clamdcheck.sh"]
      interval: 60s
      retries: 3
      start_period: 120s

  app:
    build: .
    depends_on:
      clamav:
        condition: service_healthy
    volumes:
      - clamav_socket:/run/clamav:ro  # mount read-only in app
    environment:
      CLAMAV_SOCKET: /run/clamav/clamd.sock

volumes:
  clamav_db:
  clamav_socket:
Application code
const { scanBuffer, Verdict } = require('pompelmi');

const result = await scanBuffer(req.file.buffer, {
  socket: process.env.CLAMAV_SOCKET || '/run/clamav/clamd.sock',
});

App integration

Always use depends_on with condition: service_healthy so your application container waits for clamd to be fully ready before it starts accepting traffic.

  app:
    build: .
    depends_on:
      clamav:
        condition: service_healthy
Inside a Compose network, use the service name as the hostname. In the examples above, use host: 'clamav' (not '127.0.0.1') as the value of CLAMAV_HOST. Docker's embedded DNS resolves service names automatically within the same Compose project.

scanBuffer and scanStream — no disk I/O

In TCP or UNIX socket mode, both scanBuffer() and scanStream() pipe data directly to clamd without writing anything to disk. This makes them ideal for serverless and memory-only upload pipelines.

scanBuffer — zero disk I/O in clamd mode
const { scanBuffer, Verdict } = require('pompelmi');

// req.file.buffer from multer memoryStorage — never written to disk
const result = await scanBuffer(req.file.buffer, {
  host: 'clamav',
  port: 3310,
});

if (result !== Verdict.Clean) {
  return res.status(400).json({ error: 'Upload rejected' });
}
scanStream — pipe S3 body directly to clamd
const { scanStream, Verdict } = require('pompelmi');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');

const { Body } = await s3.send(new GetObjectCommand({
  Bucket: 'my-uploads',
  Key: event.key,
}));

// Body is a Readable — piped straight to clamd, no temp file
const result = await scanStream(Body, {
  socket: '/run/clamav/clamd.sock',
  timeout: 30000,
});
In local clamscan mode (no host, port, or socket option), scanBuffer and scanStream write a temporary file to os.tmpdir() and delete it in a finally block. Switch to clamd mode to avoid the disk write.

Timeout

The timeout option (default 15000 ms) controls how long pompelmi waits for socket activity before giving up. Increase it for large files or when clamd is under heavy load.

const result = await scan('/uploads/large-archive.zip', {
  host: '127.0.0.1',
  port: 3310,
  timeout: 60_000,   // 60 seconds for large files
});

When the timeout is exceeded, pompelmi throws an error with the message: clamd connection timed out after <N>ms.

The timeout option applies to socket idle time, not total scan time. Clamd streams the response incrementally; as long as data keeps flowing, the timer resets. Very large files should still complete even with a moderate timeout.

Retry & healthcheck

New in v1.10.0: use retries and retryDelay to automatically retry on connection errors without extra wrapper code.

const result = await scan('/path/to/upload.pdf', {
  host: '127.0.0.1',
  port: 3310,
  retries:    3,
  retryDelay: 2000,   // wait 2s between attempts
});

For application startup, use a manual retry loop until clamd is healthy before accepting traffic:

const { scan, Verdict } = require('pompelmi');

async function waitForClamd(host, port, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      // EICAR test string as a buffer — always returns Malicious when clamd is live
      await scan('/dev/null', { host, port, timeout: 3000 });
      console.log('clamd is ready');
      return;
    } catch {
      console.log(`clamd not ready, attempt ${i + 1}/${maxAttempts}...`);
      await new Promise(r => setTimeout(r, 3000));
    }
  }
  throw new Error('clamd did not become ready in time');
}

await waitForClamd(process.env.CLAMAV_HOST, 3310);

Troubleshooting

Symptom Likely cause Fix
ECONNREFUSED clamd is not yet running or still downloading definitions. Wait for the healthcheck to report healthy, then retry.
Container stays starting for more than 5 minutes Definition download stalled or start_period is too short. Check docker compose logs clamav. Increase start_period if on a slow connection.
clamd connection timed out The file is very large, or the container is under heavy load. Increase timeout in options, or check container CPU/memory limits.
Always returns ScanError Definitions are present but corrupted, or clamd restarted mid-stream. Run docker compose restart clamav and wait for healthy.
ENOENT on socket path The UNIX socket file does not exist yet (clamd still starting). Wait for the container healthcheck, or retry with backoff until the socket file appears.