nodejs/node · error · InvalidArgumentError

Invalid maxTTL. Must be a positive number

Error message

Invalid maxTTL. Must be a positive number

What it means

Thrown by the DNS interceptor's factory (an `InvalidArgumentError`, code `UND_ERR_INVALID_ARG`) when `maxTTL` is provided and is either not a number or is less than 0. The DNS interceptor caches resolved address records client-side to cut lookup latency; `maxTTL` (expressed in milliseconds, default 10000) caps how long a cached entry lives, overriding the record's own TTL when shorter. Note the code allows `0` despite the message saying 'positive' — only negatives and non-numbers are rejected.

Source

Thrown at deps/undici/src/lib/interceptor/dns.js:463

        break
      }
      case 'ENOTFOUND':
        this.#state.deleteRecords(this.#origin)
        super.onResponseError(controller, err)
        break
      default:
        super.onResponseError(controller, err)
        break
    }
  }
}

module.exports = interceptorOpts => {
  if (
    interceptorOpts?.maxTTL != null &&
    (typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
  ) {
    throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
  }

  if (
    interceptorOpts?.maxItems != null &&
    (typeof interceptorOpts?.maxItems !== 'number' ||
      interceptorOpts?.maxItems < 1)
  ) {
    throw new InvalidArgumentError(
      'Invalid maxItems. Must be a positive number and greater than zero'
    )
  }

  if (
    interceptorOpts?.affinity != null &&
    interceptorOpts?.affinity !== 4 &&
    interceptorOpts?.affinity !== 6
  ) {
    throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Pass `maxTTL` as a positive number in milliseconds, e.g. `maxTTL: 60_000` for one minute.
  2. If your config uses seconds, convert: `maxTTL: ttlSeconds * 1000`.
  3. Coerce env strings with `Number(...)` and validate finiteness before passing.
  4. To disable caching expiry, omit `maxTTL` (default 10000 ms) rather than passing a sentinel.

Example fix

// before
interceptor dns({ maxTTL: '60000' })       // string
dns({ maxTTL: 60 })                          // seconds, far too short
dns({ maxTTL: process.env.DNS_TTL })

// after
dns({ maxTTL: 60000 })                        // ms = 60s
dns({ maxTTL: Number(process.env.DNS_TTL) })
Defensive patterns

Strategy: validation

Validate before calling

function validateMaxTTL(v) {
  if (v == null) return undefined
  const n = Number(v)
  if (typeof n !== 'number' || n < 0) {
    throw new Error('maxTTL must be a non-negative number (milliseconds)')
  }
  return n // remember: milliseconds, not seconds
}

Type guard

function isNonNegativeNumber(v) {
  return typeof v === 'number' && v >= 0
}

Prevention

When it happens

Trigger: Passing `maxTTL` as a string (`'10000'`), a negative number, `NaN`, or an object. The guard checks `interceptorOpts?.maxTTL != null && (typeof !== 'number' || < 0)`, so `0` passes. Value is interpreted in milliseconds, not seconds.

Common situations: Configuring TTL in seconds while the library expects milliseconds (off by 1000×); reading TTL from env as a string; copying a value from a docs example that used seconds. The seconds-vs-milliseconds confusion is the dominant real-world cause.

Related errors


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/b5c5035c534b2cb3. Report an issue: GitHub.